EF Core 关系映射结构的搭建——一对多关系


写在前面

在EF Core 中关系映射就是将关系数据库中使用的主键/外键表示形式映射到对象模型中使用的对象之间的引用。这也是这类ORM(Object-Relational Mapping 对象—关系映射)工具最核心的内容——通过关系搭建起对象模型之间的联系。
这篇文章将分别展示在EF Core 中如何构建一对多关系结构,一个良好的数据库表结构设计能提高后续各种开发的效率。


关系型数据库中,实体间无非就三种关系

  • 一对多关系:单个实体与任意数量的其他实体关联。
  • 一对一关系:单个实体与另一个实体关联。
  • 多对多关系:任意数量的实体与任意数量的其他实体关联。

一、必需的一对多

EF Core中,一对多关系根据对象关系,可细分为很多种。不同场景下的配置会影响外键约束、导航属性行为、级联删除等。常见的配置项是HasOne().WithMany()

依赖实体的存在必须依赖于主体实体,即 “多的一端” 不能独立存在。例如:公司(Compay,主体)和部门(Department,依赖)—— 每个部门必须从于一个公司。

关系背景:公司与部门
主表:公司表(Company)

public class Company
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Address { get; set; }

    // 必需的一对多关系 - 公司必须有至少一个部门
    public ICollection<Department> Departments { get; set; } = new List<Department>();
}

从表:部门表(Department)

public class Department
{
    public int Id { get; set; }
    public string Name { get; set; }

    // 外键 - 必需关系
    public int CompanyId { get; set; }
    public Company Company { get; set; }
}

关联逻辑:

每个部门必须从于一个公司。关系的配置放在DepartmentConfig里。

  • HasOne(d => d.Company).WithMany(c => c.Departments):指定Company和Departments一对多的关系;
  • HasForeignKey(d => d.CompanyId):显示指定外键字段;
  • IsRequired():用于配置 必需的一对多关系,确保子实体(Department)必须始终关联到父实体(Company)
public class DepartmentConfig : IEntityTypeConfiguration<Department>
{
    public void Configure(EntityTypeBuilder<Department> builder)
    {
        builder.ToTable("T_Department");
        builder.HasKey(x => x.Id);
        builder.HasOne(d => d.Company).WithMany(c => c.Departments).HasForeignKey(d => d.CompanyId).IsRequired();
    }
}

二、非必需的一对多

依赖实体可以关联到主体,也可以不关联(外键为 null)。例如:部门表(Department,主体)和员工(Employee,依赖)—— 员工可能分配到部门中。
主表:部门表(Department)

public class Department
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Location { get; set; }

    // 可选关系:部门可以没有员工
    public ICollection<Employee> Employees { get; set; }
}

从表:员工表(Employee)

public class Employee
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Position { get; set; }
    public decimal Salary { get; set; }

    // 外键 - 可选关系(员工可以不属于任何部门)
    public int? DepartmentId { get; set; }
    public Department? Department { get; set; }
}

关联逻辑:

每个订单必须从属于一个消费者。关系的配置放在OrderConfig里。

  • HasOne(e => e.Department).WithMany(d => d.Employees):指定Department和Employees一对多的关系;
  • HasForeignKey(o => o.DepartmentId):显示指定外键字段;
  • IsRequired(false):用于配置 非必需的一对多关系,确保子实体(Department)非必需始终关联到父实体(Company)
public void Configure(EntityTypeBuilder<Employee> builder)
{
    builder.ToTable("T_Employee");
    builder.HasKey(e => e.Id);
    builder.HasOne(e => e.Department).WithMany(d => d.Employees).HasForeignKey(o => o.DepartmentId).IsRequired(false);
}

三、具有阴影外键的必需/非必需的一对多

阴影外键(Shadow Foreign Key) 是一种特殊的外键,它在实体类中没有对应的属性,仅存在于 EF Core 的元数据和数据库中。

在某些情况下,你可能不需要模型中的外键属性,因为外键是关系在数据库中表示方式的详细信息,而在完全以面向对象的方式使用关系时,不需要外键属性。 但是,如果要序列化实体(例如通过网络发送),则当实体不采用对象形式时,外键值可能是保持关系信息不变的有用方法。 因此,为实现此目的,务实的做法是在 .NET 类型中保留外键属性。 外键属性可以是私有的,这是一个折中的办法,既可以避免公开外键,又允许其值随实体一起传输。

(一)与(二)已经详细的讨论了必须与非必须的一对多,两者的区别在于子实体是否必须始终关联到父实体。后续的一对多变体示例默认才有必需的一对多关系。指定IsRequired(false)便是非必需的一对多关系

还是1.1.1里的公司与部门关系,这次我们去掉实体部门属性里的外键CompanyId
关系背景:公司与部门
主表:公司表(Company)

public class Company
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Address { get; set; }

    // 必需的一对多关系 - 公司必须有至少一个部门
    public ICollection<Department> Departments { get; set; } = new List<Department>();
}

从表:部门表(Department)

public class Department
{
    public int Id { get; set; }
    public string Name { get; set; }

    public Company Company { get; set; }
}

关联逻辑:

每个部门必须从于一个公司。关系的配置放在DepartmentConfig里。

  • HasOne(d => d.Company).WithMany(c => c.Departments):指定Company和Departments一对多的关系;
  • HasForeignKey(d => d.CompanyId):显示指定外键字段,虽然实体里并不存在CompanyId,但实际的数据库里是存在外键CompanyId,但不映射到实体里;
  • IsRequired():用于配置 必需的一对多关系,确保子实体(Department)必须始终关联到父实体(Company)
public class DepartmentConfig : IEntityTypeConfiguration<Department>
{
    public void Configure(EntityTypeBuilder<Department> builder)
    {
        builder.ToTable("T_Department");
        builder.HasKey(x => x.Id);
        builder.HasOne(d => d.Company).WithMany(c => c.Departments).HasForeignKey(d => d.CompanyId).IsRequired();
    }
}

四、单向导航的一对多

在很多基础表里,主体实体没有反向导航属性(即主体类中没有 “依赖实体集合”)。只有依赖实体(多的一端)有导航属性指向主体(一的一端)

  • 这样设计的目的是为了保证当非常多的表依赖基础表的时候,基础表不会因为导航属性变的十分臃肿。
  • 查询 “一的端” 数据时无法通过导航属性反向获取 “多的端”,需显式通过子表查询。
  • EF Core 仍会在数据库生成外键,仅实体模型中不暴露反向导航。

还是(一)里的公司与部门关系,这次我们去掉实体公司属性里的依赖实体集合

关系背景:公司与部门
主表:公司表(Company)

public class Company
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Address { get; set; }
}

从表:部门表(Department)

public class Department
{
    public int Id { get; set; }
    public string Name { get; set; }

    // 外键 - 必需关系
    public int CompanyId { get; set; }
    public Company Company { get; set; }
}

关联逻辑:

每个部门必须从于一个公司。关系的配置放在DepartmentConfig里。

  • HasOne(d => d.Company).WithMany():指定Company和Departments一对多的关系,WithMany() 调用时没有参数,指示此方向没有导航。;
  • HasForeignKey(d => d.CompanyId):显示指定外键字段;
  • IsRequired():用于配置 必需的一对多关系,确保子实体(Department)必须始终关联到父实体(Company)
public class DepartmentConfig : IEntityTypeConfiguration<Department>
{
    public void Configure(EntityTypeBuilder<Department> builder)
    {
        builder.ToTable("T_Department");
        builder.HasKey(x => x.Id);
        builder.HasOne(d => d.Company).WithMany().HasForeignKey(d => d.CompanyId).IsRequired();
    }
}

五、自引用的一对多

树形结构是最适合自引用关系,想象一个关系组织架构。每个员工有一个经理,每个经理有多个下属,但最顶部根节点上的顶级员工没有经理
关系背景:公司与部门
主表:员工组织结构表(Employee)

public class Employee
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Position { get; set; }
    public decimal Salary { get; set; }

    // 自引用外键 - 员工的直属经理
    public int? ManagerId { get; set; }

    // 导航属性 - 指向直属经理
    public Employee Manager { get; set; }

    // 反向导航属性 - 该员工管理的下属
    public ICollection<Employee> Subordinates { get; set; }
}

关联逻辑:

每个部门必须从于一个公司。关系的配置放在DepartmentConfig里。

  • HasOne(e => e.Manager).WithMany(e => e.Subordinates):每个员工有一个经理,每个经理有多个下属;
  • HasForeignKey(d => d.ManagerId):显示指定外键字段;
  • ==.IsRequired(false) ==:顶层员工没有经理
  • .OnDelete(DeleteBehavior.Restrict):防止级联删除
public class EmployeeConfig : IEntityTypeConfiguration<Employee>
{
    public void Configure(EntityTypeBuilder<Employee> builder)
    {
        builder.HasOne(e => e.Manager)          // 每个员工有一个经理
            .WithMany(e => e.Subordinates)      // 每个经理有多个下属
            .HasForeignKey(e => e.ManagerId)
            .IsRequired(false)                  // 顶层员工没有经理
            .OnDelete(DeleteBehavior.Restrict); // 防止级联删除
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值