尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

【EF Core】再谈普通实体关系与 Owned 关系的区别

【EF Core】再谈普通实体关系与 Owned 关系的区别
📅 发布时间:2026/6/18 19:30:57

在很多个世纪前,老周曾写过实体之间普通关系(一对一,一对多,多对多)与 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");// 它占有多个 Addrste.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");// 它占有多个 Addrste.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;

image

只有 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();
}

这时候,地址表只有一个,插入的数据如下:

image

教师和学生共享一个地址表,分别通过 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();}

地址表的数据变为:

image

由于教师和学生共用一个地址表,所以他们的地址信息会相同。

 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}");}}}}

image

 

【总结】

1、Owned 关系中,主实体完全掌控从实体,并且不与其他实体共享数据;

2、被“独占”的实体不用使用 ModelBuilder.Entity<T> 方法配置,因此在 DbContext 派生时,也不能声明为 DbSet<T> 属性。而普通关系中的实体是允许的;

3、Owned 关系有一 Own 一、一 Own 多,不存在 多 Own 多。多 Own 多 就违背“独占”原则了。普通关系中可以有多对多;

 

相关新闻

  • qoj6104 Building Bombing
  • Cursor小程序实战系列三: 前后端对接保姆级拆解
  • 课前问题思考2

最新新闻

  • PyCaret低代码实现房价预测:从数据准备到模型上线全链路
  • 【Springboot毕设全套源码+文档】基于springboot的智慧仓库(丰富项目+远程调试+讲解+定制)
  • 2026年6月PE排水管企业推荐指南 - 多才菠萝
  • 全维度测评报告:2026 杭州黄金回收报价套路拆解,称重、验金、扣费猫腻逐项核验 - 奢侈品回收评测
  • DSP56800到DSP56800E代码移植:AGU寄存器加载策略与兼容性问题详解
  • Python自动化测试实战:从Selenium到Pytest的完整技术栈解析

日新闻

  • 2026年不锈钢卷板厂家推荐排行榜:冷轧热轧/304/201不锈钢卷板,高颜值耐腐蚀源头厂家实力精选 - 企业推荐官【官方】
  • FLUX.1-dev FP8模型实战指南:24GB以下显卡高效部署方案
  • 2026佛山长途搬家价目表:跨省跨市搬家费用完整计算指南 - 从来都是英雄出少年

周新闻

  • 3步解锁iOS设备:applera1n激活锁绕过完全指南
  • 39 2026 人工智能证书终极盘点,普通人选 AI 证书可以从这些方向入手
  • Redis 暴露公网有多危险?从端口检查到补救步骤

月新闻

  • 【总结】入门篇:50句话让你记住架构核心概念
  • WeChatMsg技术方案解析:实现Mac微信数据自主管理的完整解决方案
  • WeChatMsg:革新性微信数据备份方案,打造你的专属数字记忆库

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号