关系映射是建立表与表之间关系最重要的环节,如果说属性映射是内部环境,那么关系映射就是外部依赖。我们知道映射都是在上下文中的OnModelCreating方法中配置的,如果在学习阶段这样使用没有问题,但是在实际的项目开发中,且不说上下文看起来非常臃肿,最重要的是难以维护,所以我们都是要将其分层解耦。
1、One-to-Many Relationship (一对多)
这是一个控制台项目案例。我将POCO类都放在了Entity文件夹中;将每一个POCO的映射放在Map文件夹中。
由于大部分POCO类都有其共性,所以定义BaseEntity类并使其他类都继承自该类。
- BaseEntity
using System;
namespace EntityFramework_CreateDbContext.Entity
{
///
/// POCP基类(一般pocp的类都有相同属性,我们一般将其抽象为一个基类)
///
public abstract class BaseEntity
{
public int Id { get; set; }
public DateTime CreateTime { get; set; }
public DateTime ModifiedTime { get; set; }
}
}
- Customer
using System.Collections.Generic;
namespace EntityFramework_CreateDbContext.Entity
{
///
/// 客户
///
public class Customer:BaseEntity
{
public string Name { get; set; }
public string Email { get; set; }
public virtual ICollection Orders { get; set; }
}
}
- order
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace EntityFramework_CreateDbContext.Entity
{
///
/// 订单
///
public class Order:BaseEntity
{
public byte Quanatity { get; set; }
public int Price { get; set; }
public int CustomerId { get; set; }
public virtual Customer Customer{ get; set; }
}
}
我们将POCO类放在Entity文件夹中,将每一个POCO的映射放在Map文件夹中,那么如何建立每一个POCO类的映射呢?在EF6.x中有EntityTypeConfigation
- CustomerMap
using System.Data.Entity.ModelConfiguration;
using EntityFramework_CreateDbContext.Entity;
namespace EntityFramework_CreateDbContext.Map
{
public class CustomerMap:EntityTypeConfiguration
{
public CustomerMap()
{
//设置 生成表
ToTable("Customers");
//设置主键
HasKey(t => t.Id);
//设置生成 字段的类型、长度、不为空
Property(t => t.Name).HasColumnType("VARCHAR").HasMaxLength(50).IsRequired();
//设置生成 字段的类型、长度、不为空
Property(t => t.Email).HasColumnType("VARCHAR").HasMaxLength(50).IsRequired();
//设置生成 字段
Property(t => t.CreateTime);
//设置生成 字段
Property(t => t.ModifiedTime);
HasMany(t => t.Orders).WithRequired(w => w.Customer)
.HasForeignKey(k => k.CustomerId);
}
}
}
- OrderMap
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity.ModelConfiguration;
using EntityFramework_CreateDbContext.Entity;
namespace EntityFramework_CreateDbContext.Map
{
public class OrderMap:EntityTypeConfiguration
{
public OrderMap()
{
ToTable("Orders");
HasKey(t => t.Id);
Property(t => t.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
Property(t => t.Quanatity);
Property(t => t.Price);
Property(t => t.CustomerId);
Property(t => t.CreateTime);
Property(t => t.ModifiedTime);
//外键关系
HasRequired(t=>t.Customer).WithMany(c=>c.Orders)
.HasForeignKey(t=>t.CustomerId).WillCascadeOnDelete(false);
}
}
}
然后将映射Map类添加(注册)到Configuration配置中。
- EfDbContext
using System;
using System.Data.Entity;
using System.Data.Entity.ModelConfiguration;
using System.Linq;
using System.Reflection;
using EntityFramework_CreateDbContext.Entity;
namespace EntityFramework_CreateDbContext
{
public class EfDbContext:DbContext
{
public EfDbContext():base("server=.;database=createDbContext;uid=sa;pwd=123123")
{
Database.SetInitializer(new CreateDatabaseIfNotExists());
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
//在程序集中查找,命名空间不为空、并且父类不是null、并且父类是泛型、并且父类类型是EntityTypeConfiguration<>
var typesToRegister = Assembly.GetExecutingAssembly().GetTypes()
.Where(type => !string.IsNullOrEmpty(type.Namespace))
.Where(type => type.BaseType != null && type.BaseType.IsGenericType
&& type.BaseType.GetGenericTypeDefinition() ==
typeof(EntityTypeConfiguration<>));
//遍历该程序集,通过反射的方式获取到类,并将类注册到上下文对象中
foreach (var type in typesToRegister)
{
dynamic configurationInstance = Activator.CreateInstance(type);
modelBuilder.Configurations.Add(configurationInstance);//将Map映射类注册到配置中
}
base.OnModelCreating(modelBuilder);
}
public DbSet Customers { get; set; }
public DbSet Orders { get; set; }
}
}
此时,我们再来看上下文类,非常干净,分门别类,不臃肿且方便维护。
接下来分析二者关系的配置,前面在订单表(Order)中配置二者的关系,一个订单必须对应一个客户,所以其关系为HasRequired,同时一个客户会下多个订单,此时则用WithMany,同时订单中的CustomerId为Customer的外键,以此来进行约束。这样的配置为逆向角度。我们也可以正向角度在Order中配置。HasMany(p => p.Orders).WithRequired(w => w.Customer).HasForeignKey(k => k.CustomerId);
最终生成的数据库结构如下:
2、Many-to-Many Relationship (多对多)
如果两个表之间的任何记录都与另一个表的若干行记录有关,因为关系系统无法表达这两者的关系,所以就需要第三个表来维护这两个表的关系,此表则称为关联表或者链接表。多对多关系就是这样的场景,需要第三个表来关联或维护两个表的关系。
案例场景:
一个学生可以学习多门课程,一门课程也可以被多名学生学习。
- Student
using System.Collections.Generic;
namespace EntityFramework_CreateDbContext.Entity
{
///
/// 学生
///
public class Student:BaseEntity
{
public string Name { get; set; }
public byte Age { get; set; }
public virtual ICollection Courses { get; set; }
}
}
- Course
using System.Collections.Generic;
namespace EntityFramework_CreateDbContext.Entity
{
///
/// 课程
///
public class Course : BaseEntity
{
///
/// 课程名称
///
public string Name { get; set; }
///
/// 课程时长
///
public int MaximumStrength { get; set; }
public virtual ICollection Students { get; set; }
}
}
- StudentMap
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity.ModelConfiguration;
using EntityFramework_CreateDbContext.Entity;
namespace EntityFramework_CreateDbContext.Map
{
public class StudentMap : EntityTypeConfiguration
{
public StudentMap()
{
//table
ToTable("Students");
//key
HasKey(t => t.Id);
//property
Property(t => t.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
Property(t => t.Name).HasColumnType("VARCHAR").HasMaxLength(50);
Property(t => t.Age);
Property(t => t.CreateTime);
Property(t => t.ModifiedTime);
//relationship
HasMany(t => t.Courses).WithMany(c => c.Students)
.Map(t => t.ToTable("StudentCourses")
.MapLeftKey("StudentId")
.MapRightKey("CourseId"));
}
}
}
上述学生映射类片段表明:一个学生可以参加多门课程,一门课程可以有很多学生。为了实现学生和课程的多对多关系,我们自定义第三个表(StudentCourses表),MapLeftKey和MapRightKey定义了键的名称,其中左键作为定义关系的键。若不自定义键名称,则默认自动使用类型和加上“_”在加上id作为键的名称。
- CourseMap
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity.ModelConfiguration;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using EntityFramework_CreateDbContext.Entity;
namespace EntityFramework_CreateDbContext.Map
{
public class CourseMap:EntityTypeConfiguration
{
public CourseMap()
{
//table
ToTable("Courses");
//property
Property(t => t.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
Property(t => t.Name).HasColumnType("VARCHAR").HasMaxLength(50);
Property(t => t.MaximumStrength);
Property(t => t.CreateTime);
Property(t => t.ModifiedTime);
}
}
}
最终生成的数据库结构如下:
刚才上面的是映射多对多关系且第三给奥自动生成。有时候我们需要显示定义第三个表来维护Student和Courses的关系,所以对多对多关系,还有另一种映射方式,即显示定义第三个表。
- StudentCourse
namespace EntityFramework_CreateDbContext.Entity
{
public class StudentCourse : BaseEntity
{
public int StudentId { get; set; }
public virtual Student Student { get; set; }
public int CoursesId { get; set; }
public virtual Course Course { get; set; }
}
}
- Student
using System.Collections.Generic;
namespace EntityFramework_CreateDbContext.Entity
{
///
/// 学生
///
public class Student:BaseEntity
{
public string Name { get; set; }
public byte Age { get; set; }
public virtual ICollection StudentCourses { get; set; }
}
}
- Course
using System.Collections.Generic;
namespace EntityFramework_CreateDbContext.Entity
{
///
/// 课程
///
public class Course : BaseEntity
{
///
/// 课程名称
///
public string Name { get; set; }
///
/// 课程时长
///
public int MaximumStrength { get; set; }
public virtual ICollection StudentCourses { get; set; }
}
}
- CourseMap
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity.ModelConfiguration;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using EntityFramework_CreateDbContext.Entity;
namespace EntityFramework_CreateDbContext.Map
{
public class CourseMap:EntityTypeConfiguration
{
public CourseMap()
{
//table
ToTable("Courses");
//property
Property(t => t.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
Property(t => t.Name).HasColumnType("VARCHAR").HasMaxLength(50);
Property(t => t.MaximumStrength);
Property(t => t.CreateTime);
Property(t => t.ModifiedTime);
HasMany(t => t.StudentCourses)
.WithRequired(t => t.Course)
.HasForeignKey(k => k.CoursesId);
}
}
}
- StudentMap
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity.ModelConfiguration;
using EntityFramework_CreateDbContext.Entity;
namespace EntityFramework_CreateDbContext.Map
{
public class StudentMap : EntityTypeConfiguration
{
public StudentMap()
{
//table
ToTable("Students");
//key
HasKey(t => t.Id);
//property
Property(t => t.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
Property(t => t.Name).HasColumnType("VARCHAR").HasMaxLength(50);
Property(t => t.Age);
Property(t => t.CreateTime);
Property(t => t.ModifiedTime);
//relationship
HasMany(t => t.StudentCourses)
.WithRequired(t => t.Student)
.HasForeignKey(k => k.StudentId);
}
}
}
最终生成的数据库结构如下:
3、One-to-One Relationship (一对一)
案例场景:
一个学生可能有联系方式,也可能没有,一个联系方式必须对应一个学生。
- StudentInfo
namespace EntityFramework_CreateDbContext.Entity
{
public class StudentInfo
{
public long Id { get; set; }
public string Name { get; set; }
public virtual StudentContact StudentContact{ get; set; }
}
}
- StudentContact
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace EntityFramework_CreateDbContext.Entity
{
public class StudentContact
{
public long Id { get; set; }
public string ContactNumber { get; set; }
public virtual Student Student { get; set; }
}
}
(1)HasOptional then WithRequired
学生映射和联系方式映射:
- StudentInfoMap
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity.ModelConfiguration;
using EntityFramework_CreateDbContext.Entity;
namespace EntityFramework_CreateDbContext.Map
{
public class StudentInfoMap:EntityTypeConfiguration
{
public StudentInfoMap()
{
ToTable("StudentInfos");
HasKey(t => t.Id);
Property(t => t.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
HasOptional(t => t.StudentContact)//一个学生可能有联系方式,也有可能没有
.WithRequired(k => k.StudentInfo);//一个联系方式必定对应一个学生
}
}
}
- StudentContactMap
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity.ModelConfiguration;
using EntityFramework_CreateDbContext.Entity;
namespace EntityFramework_CreateDbContext.Map
{
public class StudentContactMap : EntityTypeConfiguration
{
public StudentContactMap()
{
ToTable("StudentContacts");
HasKey(t => t.Id);
Property(x => x.Id)
.HasColumnName("StudentInfoId")
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
}
}
}
最终生成的数据库结构如下:
从上图分析:在学生信息表中,主键Id为自增长;而在学生联系表中StudentId即自定义的属性Id,并为其起别名,此列既是主键又是外键。
(2)HasOptional then WithOptionalPrincipal
此时,我们将学生和联系方式映射类修改如下:
- StudentInfoMap
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity.ModelConfiguration;
using EntityFramework_CreateDbContext.Entity;
namespace EntityFramework_CreateDbContext.Map
{
public class StudentInfoMap:EntityTypeConfiguration
{
public StudentInfoMap()
{
ToTable("StudentInfos");
HasKey(t => t.Id);
Property(t => t.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
HasOptional(t => t.StudentContact)//一个学生可能有联系方式,也有可能没有
.WithOptionalPrincipal(k => k.StudentInfo);//一个联系方式必定对应一个学生
}
}
}
- StudentContactMap
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity.ModelConfiguration;
using EntityFramework_CreateDbContext.Entity;
namespace EntityFramework_CreateDbContext.Map
{
public class StudentContactMap : EntityTypeConfiguration
{
public StudentContactMap()
{
ToTable("StudentContacts");
HasKey(t => t.Id);
Property(x => x.Id)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);//设置联系人表中的id为自动增长
}
}
}
最终生成的数据库结构如下:
从上图分析:在学生信息表中,主键Id为自增长;在联系表中,Id为主键且自增长,Student_Id自动创建并且在学生模型和联系方式模型中没有导航属性,Student_Id 可空且是 Student 表中 Id 的外键。此方式创建的关联外键更符合我们经常创建的方式。
(3)HasOptional then WithOptionalDependent
我们再将学生和学生联系方式映射类配置如下:
- StudentInfoMap
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity.ModelConfiguration;
using EntityFramework_CreateDbContext.Entity;
namespace EntityFramework_CreateDbContext.Map
{
public class StudentInfoMap:EntityTypeConfiguration
{
public StudentInfoMap()
{
ToTable("StudentInfos");
HasKey(t => t.Id);
Property(t => t.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
HasOptional(t => t.StudentContact)//一个学生可能有联系方式,也有可能没有
.WithOptionalDependent(k => k.StudentInfo);//一个联系方式必定对应一个学生
}
}
}
- StudentContactMap
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity.ModelConfiguration;
using EntityFramework_CreateDbContext.Entity;
namespace EntityFramework_CreateDbContext.Map
{
public class StudentContactMap : EntityTypeConfiguration
{
public StudentContactMap()
{
ToTable("StudentContacts");
HasKey(t => t.Id);
Property(x => x.Id)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);//设置联系人表中的id为自动增长
}
}
}
最终生成的数据库结构如下:
从上图分析:在联系表中,Id为主键且自增长;在学生信息表中,主键Id为自增长,StudentContact_Id自动创建并且在学生模型和联系方式模型中没有导航属性,StudentContact_Id 可空且是 StudentContact 表中 Id 的外键。
由此我们知道,选择 WithOptionalPrincipal 使得该实体作为主体,意味着它包含关系的主键。选择 WithOptionalDependent 使得该实体作为依赖体,意味着它将具有关系的外键。
(4)HasRequired then WithOptional
上述我们一直是从学生表映射类中入手来配置和学生联系方式表的关系,如果想要从学生联系方式映射中入手呢?当然也是可以的,此时我们将学生映射和学生联系方式映射类配置如下:
- StudentInfoMap
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity.ModelConfiguration;
using EntityFramework_CreateDbContext.Entity;
namespace EntityFramework_CreateDbContext.Map
{
public class StudentInfoMap:EntityTypeConfiguration
{
public StudentInfoMap()
{
ToTable("StudentInfos");
HasKey(t => t.Id);
Property(t => t.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
}
}
}
- StudentContactMap
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity.ModelConfiguration;
using EntityFramework_CreateDbContext.Entity;
namespace EntityFramework_CreateDbContext.Map
{
public class StudentContactMap : EntityTypeConfiguration
{
public StudentContactMap()
{
ToTable("StudentContacts");
HasKey(t => t.Id);
Property(x => x.Id)
.HasColumnName("StudentId")
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);//设置联系人表中的id为自动增长
HasRequired(t => t.StudentInfo)//一个联系方式必定对应一个学生
.WithOptional(k => k.StudentContact);//一个学生可能有联系方式,也有可能没有
}
}
}
这种情况的结果就和第一种利用 HasOptional 和 WithRequired 来配置一对一关系一样,只是配置角度不一样而已。
(5)HasRequired then WithOptional
依然是利用 HasRequired 和 WithOptional 从学生联系方式映射类中入手,学生映射类不变,如此同样可以得到和第二种一样的结果,代码如下:
- StudentInfoMap
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity.ModelConfiguration;
using EntityFramework_CreateDbContext.Entity;
namespace EntityFramework_CreateDbContext.Map
{
public class StudentInfoMap:EntityTypeConfiguration
{
public StudentInfoMap()
{
ToTable("StudentInfos");
HasKey(t => t.Id);
Property(t => t.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
}
}
}
- StudentContactMap
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity.ModelConfiguration;
using EntityFramework_CreateDbContext.Entity;
namespace EntityFramework_CreateDbContext.Map
{
public class StudentContactMap : EntityTypeConfiguration
{
public StudentContactMap()
{
ToTable("StudentContacts");
HasKey(t => t.Id);
Property(x => x.Id)
.HasColumnName("StudentId")
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);//设置联系人表中的id为自动增长
HasRequired(t => t.StudentInfo) //一个联系方式必定对应一个学生
.WithOptional(k => k.StudentContact) //一个学生可能有联系方式,也有可能没有
.Map(x => x.MapKey("Student_Id"));
}
}
}
参考书籍
- 你必须掌握的Entity Framework 6.x与Core 2.0