宛蛲 发表于 2025-9-13 15:40:20

【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")
      <strong>.LogTo(m </strong><strong>=></strong><strong> Debug.WriteLine(m))</strong>;
    }

    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 (
    int NOT NULL IDENTITY,
    bigint NOT NULL,
    int NOT NULL,
    bigint NOT NULL,
    bit NOT NULL,
    CONSTRAINT PRIMARY KEY ();

CREATE TABLE (
    int NOT NULL IDENTITY,
    int NULL,
    CONSTRAINT PRIMARY KEY (),
    CONSTRAINT FOREIGN KEY () REFERENCES ()
);

CREATE TABLE (
    int NOT NULL IDENTITY,
    int NULL,
    CONSTRAINT PRIMARY KEY (),
    CONSTRAINT FOREIGN KEY () REFERENCES ()
);EF Core 这货还挺聪明的,把外键分别放在 Desktop 和 Laptop 中,这样可避免在 HardwareInfo 中出现两个外键,不好约束。毕竟这是一对一关系,外键放在哪一端都可以。
然后看看“独占”关系中的一对一,它创建了两个表:
CREATE TABLE (
    int NOT NULL IDENTITY,
    int NULL,
    bigint NULL,
    int NULL,
    bigint NULL,
    bit NULL,
    CONSTRAINT PRIMARY KEY ()
);

CREATE TABLE (
    int NOT NULL IDENTITY,
    int NULL,
    bigint NULL,
    int NULL,
    bigint NULL,
    bit NULL,
    CONSTRAINT PRIMARY KEY ()
);你没看错,只有两个表,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 (
    int NOT NULL,
    int NOT NULL,
    bigint NOT NULL,
    int NOT NULL,
    bigint NOT NULL,
    bit NOT NULL,
    CONSTRAINT PRIMARY KEY (),
    CONSTRAINT FOREIGN KEY () REFERENCES () ON DELETE CASCADE
);

CREATE TABLE (
    int NOT NULL,
    int NOT NULL,
    bigint NOT NULL,
    int NOT NULL,
    bigint NOT NULL,
    bit NOT NULL,
    CONSTRAINT PRIMARY KEY (),
    CONSTRAINT FOREIGN KEY () REFERENCES () ON DELETE CASCADE
);

CREATE TABLE (
    int NOT NULL IDENTITY,
    CONSTRAINT PRIMARY KEY ()
);

CREATE TABLE (
    int NOT NULL IDENTITY,
    CONSTRAINT PRIMARY KEY ()
);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? 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 (
          int NOT NULL IDENTITY,
          CONSTRAINT PRIMARY KEY ()
      );

CREATE TABLE (
          int NOT NULL IDENTITY,
          nvarchar(max) NOT NULL,
          nvarchar(max) NOT NULL,
          nvarchar(max) NOT NULL,
          nvarchar(max) NOT NULL,
          nvarchar(max) NOT NULL,
          nvarchar(max) NULL,
          int NOT NULL,
          CONSTRAINT PRIMARY KEY (),
          CONSTRAINT FOREIGN KEY <strong>() REFERENCES ()</strong> ON DELETE CASCADE
      );AddressInfo 表会创建一个外键来引用 Students 表的主键列。
接着,咱们加一个 Teacher 实体,和学生一样,老师也有多个收货地址。
public class Teacher
{
    public int Tid { get; set; }
    public IList? 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 (
          int NOT NULL IDENTITY,
          CONSTRAINT PRIMARY KEY ()
      );

CREATE TABLE (
          int NOT NULL IDENTITY,
          CONSTRAINT PRIMARY KEY ()
      );

CREATE TABLE (
          int NOT NULL IDENTITY,
          nvarchar(max) NOT NULL,
          nvarchar(max) NOT NULL,
          nvarchar(max) NOT NULL,
          nvarchar(max) NOT NULL,
          nvarchar(max) NOT NULL,
          nvarchar(max) NULL,
          int NOT NULL,
          CONSTRAINT PRIMARY KEY (),
          CONSTRAINT FOREIGN KEY () REFERENCES () ON DELETE CASCADE
      );

CREATE TABLE (
          int NOT NULL IDENTITY,
          nvarchar(max) NOT NULL,
          nvarchar(max) NOT NULL,
          nvarchar(max) NOT NULL,
          nvarchar(max) NOT NULL,
          nvarchar(max) NOT NULL,
          nvarchar(max) NULL,
          int NOT NULL,
          CONSTRAINT PRIMARY KEY (),
          CONSTRAINT FOREIGN KEY () REFERENCES () 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( );
    ss.Addresses = new List( );

    // 添加实体
    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)
    {
      <strong>addr.City </strong><strong>= "烤鸭市"</strong><strong>;</strong>
    }
    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().<strong>HasKey(x =></strong><strong> x.AddrID)</strong>;
    }
}改为普通一对多关系时要注意,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( );
    ss.Addresses = new List( );

    // 添加实体
    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 方法配置,因此在 DbContext 派生时,也不能声明为 DbSet 属性。而普通关系中的实体是允许的;
3、Owned 关系有一 Own 一、一 Own 多,不存在 多 Own 多。多 Own 多 就违背“独占”原则了。普通关系中可以有多对多;
 

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
页: [1]
查看完整版本: 【EF Core】再谈普通实体关系与 Owned 关系的区别