本文的代碼基於.NET Core 3.0和EF Core 3.0
有時候在數據庫設計中,一個表自己會和自己是多對多關系。
在SQL Server數據庫中,現在我們有Person表,代表一個人,建表語句如下:
CREATE TABLE [dbo].[Person]( [PersonID] [int] IDENTITY(1,1) NOT NULL, [Name] [nvarchar](50) NULL, [Age] [int] NULL, CONSTRAINT [PK_Person] PRIMARY KEY CLUSTERED ( [PersonID] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] ) ON [PRIMARY]
其中PersonID列是Person表的主鍵。
因為一個人會有多個朋友,所以實際上這種人與人之間的朋友關系,是Person表自己和自己的多對多關系,所以我們還要建立一張FriendRelation表,來表示Person表自身的多對多關系,FriendRelation表的建表語句如下:
CREATE TABLE [dbo].[FriendRelation]( [FriendRelationID] [int] IDENTITY(1,1) NOT NULL, [FromPerson] [int] NULL, [ToPerson] [int] NULL, [Remark] [nvarchar](100) NULL, CONSTRAINT [PK_FriendRelation] PRIMARY KEY CLUSTERED ( [FriendRelationID] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] ) ON [PRIMARY] GO ALTER TABLE [dbo].[FriendRelation] WITH CHECK ADD CONSTRAINT [FK_FriendRelation_Person_From] FOREIGN KEY([FromPerson]) REFERENCES [dbo].[Person] ([PersonID]) GO ALTER TABLE [dbo].[FriendRelation] CHECK CONSTRAINT [FK_FriendRelation_Person_From] GO ALTER TABLE [dbo].[FriendRelation] WITH CHECK ADD CONSTRAINT [FK_FriendRelation_Person_To] FOREIGN KEY([ToPerson]) REFERENCES [dbo].[Person] ([PersonID]) GO ALTER TABLE [dbo].[FriendRelation] CHECK CONSTRAINT [FK_FriendRelation_Person_To] GO
其中FriendRelationID列是FriendRelation表的主鍵,我們可以看到在FriendRelation表中有兩個外鍵關系:
- 外鍵關系[FK_FriendRelation_Person_From],通過FriendRelation表的外鍵列[FromPerson],關聯到Person表的主鍵列PersonID
- 外鍵關系[FK_FriendRelation_Person_To],通過FriendRelation表的外鍵列[ToPerson],關聯到Person表的主鍵列PersonID
因此Person表每行數據之間的多對多關系,就通過FriendRelation表的[FromPerson]列和[ToPerson]列建立起來了。
接下來,我們使用EF Core的DB First模式,通過Scaffold-DbContext指令,來生成實體類和DbContext類。
生成Person實體類如下:
using System; using System.Collections.Generic; namespace EFCoreSelfMany.Entities { public partial class Person { public Person() { FriendRelationFromPersonNavigation = new HashSet<FriendRelation>(); FriendRelationToPersonNavigation = new HashSet<FriendRelation>(); } public int PersonId { get; set; } public string Name { get; set; } public int? Age { get; set; } public virtual ICollection<FriendRelation> FriendRelationFromPersonNavigation { get; set; } public virtual ICollection<FriendRelation> FriendRelationToPersonNavigation { get; set; } } }
可以看到EF Core在實體類Person中生成了兩個屬性:
- FriendRelationFromPersonNavigation屬性,對應了FriendRelation表的外鍵列[FromPerson]
- FriendRelationToPersonNavigation屬性,對應了FriendRelation表的外鍵列[ToPerson]
所以通過這兩個屬性我們就能知道一個人有哪些朋友。
生成FriendRelation實體類如下:
using System; using System.Collections.Generic; namespace EFCoreSelfMany.Entities { public partial class FriendRelation { public int FriendRelationId { get; set; } public int? FromPerson { get; set; } public int? ToPerson { get; set; } public string Remark { get; set; } public virtual Person FromPersonNavigation { get; set; } public virtual Person ToPersonNavigation { get; set; } } }
可以看到EF Core在實體類FriendRelation中也生成了兩個屬性:
- FromPersonNavigation屬性,對應了FriendRelation表的外鍵列[FromPerson]
- ToPersonNavigation屬性,對應了FriendRelation表的外鍵列[ToPerson]
所以通過這兩個屬性,我們可以知道一個朋友關系中的兩個人(Person表)到底是誰。
最后我們來看看,生成的DbContext類DemoDBContext:
using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata; namespace EFCoreSelfMany.Entities { public partial class DemoDBContext : DbContext { public DemoDBContext() { } public DemoDBContext(DbContextOptions<DemoDBContext> options) : base(options) { } public virtual DbSet<FriendRelation> FriendRelation { get; set; } public virtual DbSet<Person> Person { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { if (!optionsBuilder.IsConfigured) { optionsBuilder.UseSqlServer("Server=localhost;User Id=sa;Password=Dtt!123456;Database=DemoDB"); optionsBuilder.UseLoggerFactory(new EFLoggerFactory()); } } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<FriendRelation>(entity => { entity.Property(e => e.FriendRelationId).HasColumnName("FriendRelationID"); entity.Property(e => e.Remark).HasMaxLength(100); entity.HasOne(d => d.FromPersonNavigation) .WithMany(p => p.FriendRelationFromPersonNavigation) .HasForeignKey(d => d.FromPerson) .HasConstraintName("FK_FriendRelation_Person_From"); entity.HasOne(d => d.ToPersonNavigation) .WithMany(p => p.FriendRelationToPersonNavigation) .HasForeignKey(d => d.ToPerson) .HasConstraintName("FK_FriendRelation_Person_To"); }); modelBuilder.Entity<Person>(entity => { entity.Property(e => e.PersonId).HasColumnName("PersonID"); entity.Property(e => e.Name).HasMaxLength(50); }); OnModelCreatingPartial(modelBuilder); } partial void OnModelCreatingPartial(ModelBuilder modelBuilder); } }
可以看到在實體類FriendRelation的Fluent API中(黃色高亮部分),設置了Person實體類自己與自己的多對多關系。
然后我們在.NET Core控制台項目中,寫了幾個方法來做測試:
- ClearTables方法,用於清空Person表和FriendRelation表的數據
- InsertPersonAndFriend方法,用於插入數據到Person表和FriendRelation表
- ShowFriend方法,用於顯示Person表數據"張三"的朋友
- DeleteFriend方法,用於刪除FriendRelation表數據
代碼如下所示:
using EFCoreSelfMany.Entities; using System; using Microsoft.EntityFrameworkCore; using System.Linq; namespace EFCoreSelfMany { class Program { //清空Person表和FriendRelation表的數據 public static void ClearTables() { using (var dbContext = new DemoDBContext()) { string sql = @"DELETE FROM [dbo].[FriendRelation]; DELETE FROM [dbo].[Person];"; //注意在EF Core 3.0中ExecuteSqlCommand方法已經過時,請用下面的ExecuteSqlRaw方法替代 dbContext.Database.ExecuteSqlRaw(sql); } } //插入數據到Person表和FriendRelation表 public static void InsertPersonAndFriend() { using (var dbContext = new DemoDBContext()) { //插入Person表數據"張三" Person personZhangSan = new Person() { Name = "張三", Age = 30 }; //插入Person表數據"李四" Person personLiSi = new Person() { Name = "李四", Age = 30 }; //插入FriendRelation表數據,設置"張三"和"李四"為朋友,注意"張三"是FriendRelation實體類的FromPersonNavigation屬性,"李四"是FriendRelation實體類的ToPersonNavigation屬性 FriendRelation friendRelation = new FriendRelation() { FromPersonNavigation = personZhangSan, ToPersonNavigation = personLiSi }; dbContext.Person.Add(personZhangSan); dbContext.Person.Add(personLiSi); dbContext.FriendRelation.Add(friendRelation); dbContext.SaveChanges(); } Console.WriteLine("張三 和 李四 已經添加到數據庫"); } //顯示Person表數據"張三"的朋友 public static void ShowFriend() { using (var dbContext = new DemoDBContext()) { //從數據庫Person表中找出"張三",並且使用EF Core的預加載(Eager Loading),通過Person實體類的FriendRelationFromPersonNavigation屬性查詢出FriendRelation表的數據,從而找出"張三"的朋友 //注意,因為"張三"是通過FriendRelation實體類的FromPersonNavigation屬性添加到數據庫FriendRelation表的,所以這里使用EF Core的預加載(Eager Loading)方法Include時,要使用Person實體類的FriendRelationFromPersonNavigation屬性,最后通過FriendRelation實體類的ToPersonNavigation屬性從Person表中找出"李四" var personZhangSan = dbContext.Person.Where(p => p.Name == "張三").Include(p => p.FriendRelationFromPersonNavigation).ThenInclude(f => f.ToPersonNavigation).First(); //判斷"張三"是否有朋友 if (personZhangSan.FriendRelationFromPersonNavigation.Count > 0) { Console.WriteLine($"{personZhangSan.Name} 的朋友是 {personZhangSan.FriendRelationFromPersonNavigation.First().ToPersonNavigation.Name}"); } else { Console.WriteLine($"{personZhangSan.Name} 沒有朋友"); } } } //刪除FriendRelation表數據 public static void DeleteFriend() { using (var dbContext = new DemoDBContext()) { //從數據庫Person表中找出"張三",並且使用EF Core的預加載(Eager Loading),通過Person實體類的FriendRelationFromPersonNavigation屬性查詢出FriendRelation表的數據 var personZhangSan = dbContext.Person.Where(p => p.Name == "張三").Include(p => p.FriendRelationFromPersonNavigation).First(); var friendRelation = personZhangSan.FriendRelationFromPersonNavigation.First(); //從FriendRelation表中刪除數據,也就是刪除"張三"和"李四"的朋友關系 dbContext.FriendRelation.Remove(friendRelation); dbContext.SaveChanges(); Console.WriteLine($"{personZhangSan.Name} 刪除了朋友"); } } static void Main(string[] args) { ClearTables(); InsertPersonAndFriend(); ShowFriend(); DeleteFriend(); ShowFriend(); Console.WriteLine("按任意鍵結束..."); Console.ReadKey(); } } }
當代碼執行完Program類Main方法中的InsertPersonAndFriend方法后,EF Core后台生成的日志如下:
=============================== EF Core log started =============================== Executed DbCommand (123ms) [Parameters=[@p0='?' (DbType = Int32), @p1='?' (Size = 50)], CommandType='Text', CommandTimeout='30'] SET NOCOUNT ON; INSERT INTO [Person] ([Age], [Name]) VALUES (@p0, @p1); SELECT [PersonID] FROM [Person] WHERE @@ROWCOUNT = 1 AND [PersonID] = scope_identity(); =============================== EF Core log finished =============================== =============================== EF Core log started =============================== Executed DbCommand (18ms) [Parameters=[@p0='?' (DbType = Int32), @p1='?' (Size = 50)], CommandType='Text', CommandTimeout='30'] SET NOCOUNT ON; INSERT INTO [Person] ([Age], [Name]) VALUES (@p0, @p1); SELECT [PersonID] FROM [Person] WHERE @@ROWCOUNT = 1 AND [PersonID] = scope_identity(); =============================== EF Core log finished =============================== =============================== EF Core log started =============================== Executed DbCommand (19ms) [Parameters=[@p2='?' (DbType = Int32), @p3='?' (Size = 100), @p4='?' (DbType = Int32)], CommandType='Text', CommandTimeout='30'] SET NOCOUNT ON; INSERT INTO [FriendRelation] ([FromPerson], [Remark], [ToPerson]) VALUES (@p2, @p3, @p4); SELECT [FriendRelationID] FROM [FriendRelation] WHERE @@ROWCOUNT = 1 AND [FriendRelationID] = scope_identity(); =============================== EF Core log finished ===============================
可以看到InsertPersonAndFriend方法中,EF Core一共執行了三段SQL語句,前面兩段SQL就是在Person表中插入了"張三"和"李四"兩行數據,最后一段SQL就是在FriendRelation表中插入了"張三"和"李四"的朋友關系數據。
執行完Program類Main方法中的InsertPersonAndFriend方法后,數據庫Person表記錄如下:
數據庫FriendRelation表記錄如下:
控制台輸出結果如下:
當代碼執行完Program類Main方法中的第一個ShowFriend方法后,EF Core后台生成的日志如下:
=============================== EF Core log started =============================== Executed DbCommand (13ms) [Parameters=[], CommandType='Text', CommandTimeout='30'] SELECT [t].[PersonID], [t].[Age], [t].[Name], [t0].[FriendRelationID], [t0].[FromPerson], [t0].[Remark], [t0].[ToPerson], [t0].[PersonID], [t0].[Age], [t0].[Name] FROM ( SELECT TOP(1) [p].[PersonID], [p].[Age], [p].[Name] FROM [Person] AS [p] WHERE ([p].[Name] = N'張三') AND [p].[Name] IS NOT NULL ) AS [t] LEFT JOIN ( SELECT [f].[FriendRelationID], [f].[FromPerson], [f].[Remark], [f].[ToPerson], [p0].[PersonID], [p0].[Age], [p0].[Name] FROM [FriendRelation] AS [f] LEFT JOIN [Person] AS [p0] ON [f].[ToPerson] = [p0].[PersonID] ) AS [t0] ON [t].[PersonID] = [t0].[FromPerson] ORDER BY [t].[PersonID], [t0].[FriendRelationID] =============================== EF Core log finished ===============================
可以看到EF Core生成了SQL語句,將"張三"和其朋友的數據都從Person表和FriendRelation表查詢出來了。
控制台輸出結果如下:
當代碼執行完Program類Main方法中的DeleteFriend方法后,EF Core后台生成的日志如下:
=============================== EF Core log started =============================== Executed DbCommand (28ms) [Parameters=[], CommandType='Text', CommandTimeout='30'] SELECT [t].[PersonID], [t].[Age], [t].[Name], [f].[FriendRelationID], [f].[FromPerson], [f].[Remark], [f].[ToPerson] FROM ( SELECT TOP(1) [p].[PersonID], [p].[Age], [p].[Name] FROM [Person] AS [p] WHERE ([p].[Name] = N'張三') AND [p].[Name] IS NOT NULL ) AS [t] LEFT JOIN [FriendRelation] AS [f] ON [t].[PersonID] = [f].[FromPerson] ORDER BY [t].[PersonID], [f].[FriendRelationID] =============================== EF Core log finished =============================== =============================== EF Core log started =============================== Executed DbCommand (15ms) [Parameters=[@p0='?' (DbType = Int32)], CommandType='Text', CommandTimeout='30'] SET NOCOUNT ON; DELETE FROM [FriendRelation] WHERE [FriendRelationID] = @p0; SELECT @@ROWCOUNT; =============================== EF Core log finished ===============================
可以看到EF Core生成了兩段SQL語句,第一段SQL是通過"張三"找出FriendRelation表的數據,第二段SQL是將找出的FriendRelation表數據進行了刪除。
執行完Program類Main方法中的DeleteFriend方法后,數據庫FriendRelation表記錄如下:
控制台輸出結果如下:
當代碼執行完Program類Main方法中的第二個ShowFriend方法后,控制台輸出結果如下:
所以我們可以看到,EF Core是支持數據庫表自己與自己多對多關系的實體類映射的,當實體類生成好后,其使用方法和普通的多對多關系差不多,沒有太大的區別。