【EF Core】再談普通實體關系與 Owned 關系的區別
在很多個世紀前,老周曾寫過實體之間普通關系(一對一,一對多,多對多)與 Owned 關系的區別。不過,那次寫得比較粗淺,逼格不夠高,于是,老周厚著臉皮地決定重新寫一下。
首先,為什么這次老周用原單詞 Owned 呢,官方文檔目前的翻譯(懷疑是機器干的)為“從屬”,這種說法與普通關系數據庫中一對多、多對多等關系描述不太 好區分。其實老周覺得應該把 Owned 翻譯為“獨占”關系——你完全屬于我的。普通關系中的廁所是公共廁所,我可以用,鄰居A、B、C也可以用;而 Owned 關系中的廁所是私人的,我用我家的廁所,A用A家自己的廁所,B不能用A家的廁所。
這種玩意兒比某少年馬戲團的粉絲還抽象,要理解最好的方法是比較。本文老周就對這兩類關系做一輪大比拼。
One and One
首先我們來看“一”和“一”的方式。為了保持數據結構的一致,咱們用這三個實體來實驗。
public class HardwareInfo { public int HwID { get; set; } // 主鍵 public long MemorySize { get; set; } // 內存大小 public int HarddiskNum { get; set; } // 硬盤數量 public long HDDSize { get; set; } // 硬盤大小 public bool InteGrp { get; set; } // 是否有集顯 } public class Desktop { public int ID { get; set; } // 主鍵 public HardwareInfo HWInfo { get; set; } // 硬件信息 } public class Laptop { public int ID { get; set; } // 主鍵 public HardwareInfo HWInfo { get; set; } // 硬件信息 }
HardwareInfo 表示硬件參數,不管是臺式機(Desktop)還是筆記本(Laptop)都可以共用這樣的數據結構。
先定義用在普通關系的上下文類——MyContextR,R結尾表示 Relational。
public class MyContextR : DbContext { public DbSet<Desktop> PCs { get; set; } public DbSet<Laptop> Laps { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer(@"server=<你的服務器>;database=rdb") .LogTo(m => Debug.WriteLine(m)); } protected override void OnModelCreating(ModelBuilder mb) { // 配置主鍵 mb.Entity<HardwareInfo>().HasKey(m => m.HwID); mb.Entity<Laptop>(ent => { ent.HasKey(k => k.ID); ent.HasOne(x => x.HWInfo); }); mb.Entity<Desktop>(eb => { eb.HasKey(a => a.ID); eb.HasOne(y => y.HWInfo); }); } }
由于老周在定義實體類時“粗心大意”,主鍵屬性的命名無法讓 EF Core 自動識別,所以要在 OnModelCreating 方法中顯式配置一下。注意,HasOne 讓它們建立一對一的關系,即PC有一個HardwareInfo 實例,筆記本也有。
第二個上下文類是面向“獨占”關系的 MyContextO,O 結尾表示 Owned。
public class MyContextO : DbContext { public DbSet<Laptop> Laps { get; set; } public DbSet<Desktop> PCs { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer(@"server=<你的服務器>;database=odb") .LogTo(g => Debug.WriteLine(g)); } protected override void OnModelCreating(ModelBuilder mb) { mb.Entity<Laptop>().HasKey(m => m.ID); mb.Entity<Desktop>().HasKey(n => n.ID); mb.Entity<Laptop>().OwnsOne(x => x.HWInfo); mb.Entity<Desktop>().OwnsOne(w => w.HWInfo); } }
OwnsOne 表示一占一,PC占用一個HardwareInfo實例,筆記本也占用一個,兩者不相干。這種情形 HardwareInfo 是不需要主鍵的,為什么?往下看你就懂了。
咱們依次實例化這兩個上下文對象,然后讓它自己創建數據庫。
static void Main(string[] args) { using MyContextR c1 = new(); c1.Database.EnsureCreated(); using MyContextO c2 = new(); c2.Database.EnsureCreated(); }
實驗結果發現,普通一對一關系中,創建了三個表:
CREATE TABLE [HardwareInfo] ( [HwID] int NOT NULL IDENTITY, [MemorySize] bigint NOT NULL, [HarddiskNum] int NOT NULL, [HDDSize] bigint NOT NULL, [InteGrp] bit NOT NULL, CONSTRAINT [PK_HardwareInfo] PRIMARY KEY ([HwID]); CREATE TABLE [Laps] ( [ID] int NOT NULL IDENTITY, [HWInfoHwID] int NULL, CONSTRAINT [PK_Laps] PRIMARY KEY ([ID]), CONSTRAINT [FK_Laps_HardwareInfo_HWInfoHwID] FOREIGN KEY ([HWInfoHwID]) REFERENCES [HardwareInfo] ([HwID]) ); CREATE TABLE [PCs] ( [ID] int NOT NULL IDENTITY, [HWInfoHwID] int NULL, CONSTRAINT [PK_PCs] PRIMARY KEY ([ID]), CONSTRAINT [FK_PCs_HardwareInfo_HWInfoHwID] FOREIGN KEY ([HWInfoHwID]) REFERENCES [HardwareInfo] ([HwID]) );
EF Core 這貨還挺聰明的,把外鍵分別放在 Desktop 和 Laptop 中,這樣可避免在 HardwareInfo 中出現兩個外鍵,不好約束。畢竟這是一對一關系,外鍵放在哪一端都可以。
然后看看“獨占”關系中的一對一,它創建了兩個表:
CREATE TABLE [Laps] ( [ID] int NOT NULL IDENTITY, [HWInfo_HwID] int NULL, [HWInfo_MemorySize] bigint NULL, [HWInfo_HarddiskNum] int NULL, [HWInfo_HDDSize] bigint NULL, [HWInfo_InteGrp] bit NULL, CONSTRAINT [PK_Laps] PRIMARY KEY ([ID]) ); CREATE TABLE [PCs] ( [ID] int NOT NULL IDENTITY, [HWInfo_HwID] int NULL, [HWInfo_MemorySize] bigint NULL, [HWInfo_HarddiskNum] int NULL, [HWInfo_HDDSize] bigint NULL, [HWInfo_InteGrp] bit NULL, CONSTRAINT [PK_PCs] PRIMARY KEY ([ID]) );
你沒看錯,只有兩個表,HardwareInfo 直接被拆開了,Desktop和Laptop各擁有一份。現在你明白了吧,為什么 HardwareInfo 在這種關系下不需要主鍵,因為它們不獨成表。
那么,如果讓 HardwareInfo 獨立建表呢,又會怎樣?咱們把 MyContextO 類的代碼改一下,為 HardwareInfo 類單獨建表。
public class MyContextO : DbContext { public DbSet<Laptop> Laps { get; set; } public DbSet<Desktop> PCs { get; set; } …… protected override void OnModelCreating(ModelBuilder mb) { mb.Entity<Desktop>(et => { et.HasKey(a => a.ID); et.OwnsOne(b => b.HWInfo, ob => { ob.ToTable("Desktop_HW"); ob.WithOwner(); }); }); mb.Entity<Laptop>(et => { et.HasKey(a => a.ID); et.OwnsOne(m => m.HWInfo, ob => { ob.ToTable("Laptop_HW"); ob.WithOwner(); }); }); } }
這個地方,WithOwner 方法可以不調用,因為 HardwareInfo 類沒有定義指向 Laptop 或 Desktop 的反向導航屬性。
這一次,會創建四個表:
CREATE TABLE [Desktop_HW] ( [DesktopID] int NOT NULL, [HwID] int NOT NULL, [MemorySize] bigint NOT NULL, [HarddiskNum] int NOT NULL, [HDDSize] bigint NOT NULL, [InteGrp] bit NOT NULL, CONSTRAINT [PK_Desktop_HW] PRIMARY KEY ([DesktopID]), CONSTRAINT [FK_Desktop_HW_PCs_DesktopID] FOREIGN KEY ([DesktopID]) REFERENCES [PCs] ([ID]) ON DELETE CASCADE ); CREATE TABLE [Laptop_HW] ( [LaptopID] int NOT NULL, [HwID] int NOT NULL, [MemorySize] bigint NOT NULL, [HarddiskNum] int NOT NULL, [HDDSize] bigint NOT NULL, [InteGrp] bit NOT NULL, CONSTRAINT [PK_Laptop_HW] PRIMARY KEY ([LaptopID]), CONSTRAINT [FK_Laptop_HW_Laps_LaptopID] FOREIGN KEY ([LaptopID]) REFERENCES [Laps] ([ID]) ON DELETE CASCADE ); CREATE TABLE [PCs] ( [ID] int NOT NULL IDENTITY, CONSTRAINT [PK_PCs] PRIMARY KEY ([ID]) ); CREATE TABLE [Laps] ( [ID] int NOT NULL IDENTITY, CONSTRAINT [PK_Laps] PRIMARY KEY ([ID]) );
EF Core 很有才,咱們沒有為 HardwareInfo 定義主鍵,于是它自己生成了,在 Laptop_HW 表中生成 LaptopID 列作為主鍵,同時也作為外鍵,引用 Laptop.ID;在 Desktop_HW 表中生成了 DesktopID 列作為主鍵,同時作為外鍵,引用 Desktop.ID。
還要補充解釋一下模型配置代碼。
mb.Entity<Laptop>(et => { et.HasKey(a => a.ID); et.OwnsOne(m => m.HWInfo, ob => { ob.ToTable("Laptop_HW"); //ob.WithOwner(); }); });
ToTable 的調用在此處是必須的,否則按默認約定,它會使用表名 Laps,即和 Laptop 保持一致,這會導致出錯。而且,Laptop 和 Desktop 不能共享一個 HardwareInfo 實體。這樣配置也會報錯:
protected override void OnModelCreating(ModelBuilder mb) { mb.Entity<Desktop>(et => { et.HasKey(a => a.ID); et.OwnsOne(b => b.HWInfo, ob => { ob.ToTable("HW_info"); }); }); mb.Entity<Laptop>(et => { et.HasKey(a => a.ID); et.OwnsOne(m => m.HWInfo, ob => { ob.ToTable("HW_info"); }); }); }
這就等于 Desktop 和 Laptop 同時占有相同的 HardwareInfo 實例,運行時也會報錯。
One and Many
這里咱們已經沒有必要再與普通的一對多關系對比了,上面的對比已經明確 Owned 關系是獨占性的,不共享實例。下面咱們看看實體獨占多個實例的情況。這種情況下,被占有的對象不會與主對象共用一個表了——拆分的列無法表示多個實例。
舉個例子。
public class AddressInfo { /// <summary> /// 這里有主鍵 /// </summary> public int AddrID { get; set; } /// <summary> /// 省 /// </summary> public string Province { get; set; } = ""; /// <summary> /// 市 /// </summary> public string City { get; set; } = ""; /// <summary> /// 鎮 /// </summary> public string Town { get; set; } = ""; /// <summary> /// 路 /// </summary> public string Road { get; set; } = ""; /// <summary> /// 街道 /// </summary> public string Street { get; set; } = ""; /// <summary> /// 郵編 /// </summary> public string? ZipCode { get; set; } } public class Student { public int StudentID { get; set; } public IList<AddressInfo>? Addresses { get; set; } }
如果這里的地址表示收貨地址,于是每個學生都可以擁有多個地址。
然后,上下文類是這樣的。
public class MyContext : DbContext { public DbSet<Student> Students { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder ob) { SqlConnectionStringBuilder strbd = new(); strbd.DataSource = <你的服務器>; strbd.InitialCatalog = "TestDB"; ob.UseSqlServer(strbd.ConnectionString) .LogTo(x => Console.WriteLine(x)); } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Student>(ste => { ste.HasKey(x => x.StudentID).HasName("PK_Stu_id"); // 它占有多個 Addr ste.OwnsMany(k => k.Addresses, ob => { // 此處可以配置主鍵 ob.HasKey(x => x.AddrID); ob.WithOwner() .HasForeignKey("stu_id").HasConstraintName("FK_StuID"); }); }); } }
數據庫會創建兩張表:
CREATE TABLE [Students] ( [StudentID] int NOT NULL IDENTITY, CONSTRAINT [PK_Stu_id] PRIMARY KEY ([StudentID]) ); CREATE TABLE [AddressInfo] ( [AddrID] int NOT NULL IDENTITY, [Province] nvarchar(max) NOT NULL, [City] nvarchar(max) NOT NULL, [Town] nvarchar(max) NOT NULL, [Road] nvarchar(max) NOT NULL, [Street] nvarchar(max) NOT NULL, [ZipCode] nvarchar(max) NULL, [stu_id] int NOT NULL, CONSTRAINT [PK_AddressInfo] PRIMARY KEY ([AddrID]), CONSTRAINT [FK_StuID] FOREIGN KEY ([stu_id]) REFERENCES [Students] ([StudentID]) ON DELETE CASCADE );
AddressInfo 表會創建一個外鍵來引用 Students 表的主鍵列。
接著,咱們加一個 Teacher 實體,和學生一樣,老師也有多個收貨地址。
public class Teacher { public int Tid { get; set; } public IList<AddressInfo>? Addresses { get; set; } }
上下文類也要做相應修改。
public class MyContext : DbContext { public DbSet<Student> Students { get; set; } public DbSet<Teacher> Teachers { get; set; } …… protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Student>(ste => { ste.HasKey(x => x.StudentID).HasName("PK_Stu_id"); // 它占有多個 Addr ste.OwnsMany(k => k.Addresses, ob => { // 此處可以配置主鍵 ob.HasKey(x => x.AddrID); // 必須要表名 ob.ToTable("Stu_Addr"); ob.WithOwner() .HasForeignKey("stu_id").HasConstraintName("FK_StuID"); }); }); modelBuilder.Entity<Teacher>(tet => { tet.HasKey(t => t.Tid).HasName("PK_TeacherID"); // 占用多個地址 tet.OwnsMany(t => t.Addresses, ob => { ob.HasKey(o => o.AddrID); // 主鍵 ob.ToTable("Teacher_Addr"); // 表名 ob.WithOwner().HasForeignKey("teach_id").HasConstraintName("FK_TeachID"); }); }); } }
這種情況下必須配置 AddressInfo 的表名。
這樣數據庫會創建四張表:
CREATE TABLE [Students] ( [StudentID] int NOT NULL IDENTITY, CONSTRAINT [PK_Stu_id] PRIMARY KEY ([StudentID]) ); CREATE TABLE [Teachers] ( [Tid] int NOT NULL IDENTITY, CONSTRAINT [PK_TeacherID] PRIMARY KEY ([Tid]) ); CREATE TABLE [Stu_Addr] ( [AddrID] int NOT NULL IDENTITY, [Province] nvarchar(max) NOT NULL, [City] nvarchar(max) NOT NULL, [Town] nvarchar(max) NOT NULL, [Road] nvarchar(max) NOT NULL, [Street] nvarchar(max) NOT NULL, [ZipCode] nvarchar(max) NULL, [stu_id] int NOT NULL, CONSTRAINT [PK_Stu_Addr] PRIMARY KEY ([AddrID]), CONSTRAINT [FK_StuID] FOREIGN KEY ([stu_id]) REFERENCES [Students] ([StudentID]) ON DELETE CASCADE ); CREATE TABLE [Teacher_Addr] ( [AddrID] int NOT NULL IDENTITY, [Province] nvarchar(max) NOT NULL, [City] nvarchar(max) NOT NULL, [Town] nvarchar(max) NOT NULL, [Road] nvarchar(max) NOT NULL, [Street] nvarchar(max) NOT NULL, [ZipCode] nvarchar(max) NULL, [teach_id] int NOT NULL, CONSTRAINT [PK_Teacher_Addr] PRIMARY KEY ([AddrID]), CONSTRAINT [FK_TeachID] FOREIGN KEY ([teach_id]) REFERENCES [Teachers] ([Tid]) ON DELETE CASCADE );
最后,咱們驗證一下,Owned 關系是否真的不能共享實例。
using(MyContext c = new()) { // 四個地址 AddressInfo addr1 = new() { Province = "冬瓜省", City = "嘎子市", Town = "小連子鎮", Road = "牛逼路", Street = "春風街3999號", ZipCode = "62347" }; AddressInfo addr2 = new() { Province = "提頭省", City = "抬扛臺", Town = "煙斗鎮", Road = "王八路", Street = "送人頭街666號", ZipCode = "833433" }; // 教師實例 Teacher tt = new(); // 學生實例 Student ss = new(); // 讓他們使用相同的地址實例 tt.Addresses = new List<AddressInfo>( [addr1, addr2] ); ss.Addresses = new List<AddressInfo>( [addr1, addr2] ); // 添加實體 c.Students.Add(ss); c.Teachers.Add(tt); // 保存到數據庫 c.SaveChanges(); }
運行后,未拋出異常,但有警告。而且數據庫中也有數據。
下面咱們改一下某個地址的 City 屬性。
using(MyContext c2 = new()) { var r1 = c2.Students.ToArray(); var r2 = c2.Teachers.ToArray(); AddressInfo? addr = r1.First()?.Addresses?.FirstOrDefault(); if(addr != null) { addr.City = "烤鴨市"; } c2.SaveChanges(); }
運行一下。
然后咱們查詢一下兩個地址表的數據。
select * from Stu_Addr; select * from Teacher_Addr;

只有 ID = 1 的學生的第一個地址的 City 屬性被更新,而教師地址未更新。可見,兩個實體是不共響地址實例的。這很好理解嘛,畢竟是兩個表的。
那么,如果把 Student - AddressInfo,Teacher - AddressInfo 的關系改為普通的一對多關系,又會怎樣?
public class MyContext : DbContext { public DbSet<Student> Students { get; set; } public DbSet<Teacher> Teachers { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder ob) { …… } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Student>(ste => { ste.HasKey(x => x.StudentID).HasName("PK_Stu_id"); ste.HasMany(x => x.Addresses) .WithOne() .HasForeignKey("stu_id") .HasConstraintName("FK_StuID"); }); modelBuilder.Entity<Teacher>(tet => { tet.HasKey(t => t.Tid).HasName("PK_TeacherID"); tet.HasMany(f => f.Addresses) .WithOne() .HasForeignKey("teacher_id") .HasConstraintName("FK_TeacherID"); }); // 注意:這時候 AddressInfo 實體需要主鍵 modelBuilder.Entity<AddressInfo>().HasKey(x => x.AddrID); } }
改為普通一對多關系時要注意,Student、Teacher、AddressInfo 三個實體都需要主鍵的, Owned 實體、復合類型(老周以前介紹過)這些不需要主鍵。
刪除剛剛的數據庫,重新建立新的數據庫,然后寫入數據。
using(MyContext c = new()) { c.Database.EnsureDeleted(); c.Database.EnsureCreated(); // 兩個地址 AddressInfo addr1 = new() { Province = "冬瓜省", City = "嘎子市", Town = "小連子鎮", Road = "牛逼路", Street = "春風街3999號", ZipCode = "62347" }; AddressInfo addr2 = new() { Province = "提頭省", City = "抬扛臺", Town = "煙斗鎮", Road = "王八路", Street = "送人頭街666號", ZipCode = "833433" }; // 教師實例 Teacher tt = new(); // 學生實例 Student ss = new(); // 讓他們使用相同的地址實例 tt.Addresses = new List<AddressInfo>( [addr1, addr2] ); ss.Addresses = new List<AddressInfo>( [addr1, addr2] ); // 添加實體 c.Students.Add(ss); c.Teachers.Add(tt); // 保存到數據庫 c.SaveChanges(); }
這時候,地址表只有一個,插入的數據如下:

教師和學生共享一個地址表,分別通過 stu_id 和 teacher_id 外鍵引用主表記錄。
然后更改第一個地址的 City 屬性。
using(MyContext c2 = new()) { var r1 = c2.Students.Include(s => s.Addresses).ToArray(); var r2 = c2.Teachers.Include(t => t.Addresses).ToArray(); AddressInfo? addr = r1.First()?.Addresses?.FirstOrDefault(); if(addr != null) { addr.City = "烤鴨市"; } c2.SaveChanges(); }
地址表的數據變為:

由于教師和學生共用一個地址表,所以他們的地址信息會相同。
using(MyContext c3 = new()) { // 加載全部數據 var students = c3.Students.Include(x => x.Addresses); var teachers = c3.Teachers.Include(x => x.Addresses); Console.WriteLine("---------- 學生 ---------"); foreach(var s in students) { Console.WriteLine($"學生:{s.StudentID}"); if(s.Addresses != null) { foreach(var a in s.Addresses) { Console.WriteLine($"\t{a.AddrID}, {a.Province}, {a.City}, {a.Town}"); } } } Console.WriteLine("\n---------- 教師 ---------"); foreach (var t in teachers) { Console.WriteLine($"老師:{t.Tid}"); if (t.Addresses != null) { foreach (var a in t.Addresses) { Console.WriteLine($"\t{a.AddrID}, {a.Province}, {a.City}, {a.Town}"); } } } }

【總結】
1、Owned 關系中,主實體完全掌控從實體,并且不與其他實體共享數據;
2、被“獨占”的實體不用使用 ModelBuilder.Entity<T> 方法配置,因此在 DbContext 派生時,也不能聲明為 DbSet<T> 屬性。而普通關系中的實體是允許的;
3、Owned 關系有一 Own 一、一 Own 多,不存在 多 Own 多。多 Own 多 就違背“獨占”原則了。普通關系中可以有多對多;

浙公網安備 33010602011771號