一、 前言
二、 实体映射
(一) DataAnnotation
(二) Fluent API
(三) 映射代码示例
三、 数据迁移
四、 代码重构
五、 源码下载
六、 系列导航
经过EF的《 第一篇 》,我们已经把数据访问层基本搭建起来了,但并没有涉及实体关系,实体关系对于一个数据库系统来说至关重要。
实体与数据库的映射可以通过DataAnnotation与FluentAPI两种方式来进行映射:
DataAnnotation 特性由.NET 3.5中引进,给.NET中的类提供了一种添加验证的方式。DataAnnotation由命名空间System.ComponentModel.DataAnnotations提供。下面列举实体模型中常用的DataAnnotation特性:
System.ComponentModel.DataAnnotations命名空间中只定义了部分实体验证的特性,在EntityFramework程序集中定义了更多的数据映射特性:
对于实体关系对应的数据表关系,无非“0:1,1:1,0:N,1:N,N:N”这几种,可以使用导航属性中的数据类型来表示,0…1端使用单实体类型表示,N端使用ICollection<T>集合类型表示。对于单实体端,默认是可为空的,即为0关系,如果要设置为1关系,要使用[Required]标签来进行标记。但对于一对一中的关系主体与依赖对象确无法做更细节的控制。
使用DataAnnotation非常简单,但对于EntityFramework中的特性,就要在实体类中引入EntityFramework程序集,但实体类最好能是保持 与架构无关性 的POCO类,才能更具通用性。所以,最好是在数据层中使用FluentAPI 在数据层中 进行实体类与数据库之间的映射工作。
当然,System.ComponentModel.DataAnnotations命名空间的DataAnnotation在EntityFramework程序集中也有相应的API:
上面这些API均无需引用EntityFramework,推荐使用DataAnnotation方式来设置映射。
以下API的DataAnnotation特性是在EntityFramework中定义,如果也使用DataAnnotation方式来设置映射,就会给实体类增加额外的第三方程序集的依赖。所以以下API的映射推荐使用FluentAPI的方式来设置映射:
经常用到的DataAnnotation与FluentAPI列举完了,使用上还是遵守这个原则:
如果在System.ComponentModel.DataAnnotations命名空间存在相应的标签,就使用 DataAnnotation 的方式,如果不存在,则使用 FluentAPI 的方式。
上图是一个以用户信息为中心的实体关系图,关系说明如下:
1 namespace GMF.Demo.Core.Models 2 { 3 /// <summary> 4 /// 实体类——用户信息 5 /// </summary> 6 [Description("用户信息")] 7 public class Member : Entity 8 { 9 public int Id { get; set; }10 11 [Required]12 [StringLength(20)]13 public string UserName { get; set; }14 15 [Required]16 [StringLength(32)]17 public string Password { get; set; }18 19 [Required]20 [StringLength(20)]21 public string NickName { get; set; }22 23 [Required]24 [StringLength(50)]25 public string Email { get; set; }26 27 /// <summary>28 /// 获取或设置 用户扩展信息29 /// </summary>30 public virtual MemberExtend Extend { get; set; }31 32 /// <summary>33 /// 获取或设置 用户拥有的角色信息集合34 /// </summary>35 public virtual ICollection<Role> Roles { get; set; }36 37 /// <summary>38 /// 获取或设置 用户登录记录集合39 /// </summary>40 public virtual ICollection<LoginLog> LoginLogs { get; set; }41 }42 }
1 namespace GMF.Demo.Core.Models 2 { 3 /// <summary> 4 /// 实体类——用户扩展信息 5 /// </summary> 6 [Description("用户扩展信息")] 7 public class MemberExtend : Entity 8 { 9 /// <summary>10 /// 初始化一个 用户扩展实体类 的新实例11 /// </summary>12 public MemberExtend()13 {14 Id = CombHelper.NewComb();15 }16 17 public Guid Id { get; set; }18 19 public string Tel { get; set; }20 21 public MemberAddress Address { get; set; }22 23 public virtual Member Member { get; set; }24 }25 }
1 namespace GMF.Demo.Core.Models 2 { 3 /// <summary> 4 /// 用户地址信息 5 /// </summary> 6 public class MemberAddress 7 { 8 [StringLength(10)] 9 public string Province { get; set; }10 11 [StringLength(20)]12 public string City { get; set; }13 14 [StringLength(20)]15 public string County { get; set; }16 17 [StringLength(60, MinimumLength = 5)]18 public string Street { get; set; }19 }20 }
1 namespace GMF.Demo.Core.Models 2 { 3 /// <summary> 4 /// 实体类——登录记录信息 5 /// </summary> 6 [Description("登录记录信息")] 7 public class LoginLog : Entity 8 { 9 /// <summary>10 /// 初始化一个 登录记录实体类 的新实例11 /// </summary>12 public LoginLog()13 {14 Id = CombHelper.NewComb();15 }16 17 public Guid Id { get; set; }18 19 [Required]20 [StringLength(15)]21 public string IpAddress { get; set; }22 23 /// <summary>24 /// 获取或设置 所属用户信息25 /// </summary>26 public virtual Member Member { get; set; }27 }28 }
1 namespace GMF.Demo.Core.Models 2 { 3 /// <summary> 4 /// 实体类——角色信息 5 /// </summary> 6 [Description("角色信息")] 7 public class Role : Entity 8 { 9 public Role()10 {11 Id = CombHelper.NewComb();12 }13 14 public Guid Id { get; set; }15 16 [Required]17 [StringLength(20)]18 public string Name { get; set; }19 20 [StringLength(100)]21 public string Description { get; set; }22 23 /// <summary>24 /// 获取或设置 角色类型25 /// </summary>26 public RoleType RoleType { get; set; }27 28 /// <summary>29 /// 获取或设置 角色类型的数值表示,用于数据库存储30 /// </summary>31 public int RoleTypeNum { get; set; }32 33 /// <summary>34 /// 获取或设置 拥有此角色的用户信息集合35 /// </summary>36 public virtual ICollection<Member> Members { get; set; }37 }38 }
1 namespace GMF.Demo.Core.Models 2 { 3 /// <summary> 4 /// 表示角色类型的枚举 5 /// </summary> 6 [Description("角色类型")] 7 public enum RoleType 8 { 9 /// <summary>10 /// 用户类型11 /// </summary>12 [Description("用户角色")]13 User = 0,14 15 /// <summary>16 /// 管理员类型17 /// </summary>18 [Description("管理角色")]19 Admin = 120 }21 }
实体类映射中,关系的映射配置在关系的两端都可以配置。例如,用户信息与登录信息的 一对多 关系可以在用户信息端配置:
HasMany(m => m.LoginLogs).WithRequired(n => n.Member);
等效于 在登录日志信息端配置:
HasRequired(m => m.Member).WithMany(n => n.LoginLogs);
但是,如果所有的关系映射都在作为主体的用户信息端进行配置,势必造成用户信息端配置的臃肿与职责不明。所以,为了保持各个实体类型的职责单一,实体关系推荐在关系的非主体端进行映射。
用户信息映射类,用户信息是关系的主体,所有的关系都不在此映射类中进行配置
1 namespace GMF.Demo.Core.Data.Configurations2 {3 public class MemberConfiguration : EntityTypeConfiguration<Member>4 {5 }6 }
用户扩展信息映射类,配置用户扩展信息与用户信息的 0:1 关系
1 namespace GMF.Demo.Core.Data.Configurations 2 { 3 public class MemberExtendConfiguration : EntityTypeConfiguration<MemberExtend> 4 { 5 public MemberExtendConfiguration() 6 { 7 HasRequired(m => m.Member).WithOptional(n => n.Extend); 8 } 9 }10 }
用户地址信息映射类,配置用户地址信息的复杂类型映射,复杂类型继承于 ComplexTypeConfiguration<>
1 namespace GMF.Demo.Core.Data.Configurations 2 { 3 public class MemberAddressConfiguration : ComplexTypeConfiguration<MemberAddress> 4 { 5 public MemberAddressConfiguration() 6 { 7 Property(m => m.Province).HasColumnName("Province"); 8 Property(m => m.City).HasColumnName("City"); 9 Property(m => m.County).HasColumnName("County");10 Property(m => m.Street).HasColumnName("Street");11 }12 }13 }
登录记录信息映射,配置登录信息与用户信息的 N:1 的关系
1 namespace GMF.Demo.Core.Data.Configurations 2 { 3 public class LoginLogConfiguration : EntityTypeConfiguration<LoginLog> 4 { 5 public LoginLogConfiguration() 6 { 7 HasRequired(m => m.Member).WithMany(n => n.LoginLogs); 8 } 9 }10 }
角色信息映射,配置角色信息与用户信息的 N:N 的关系
1 namespace GMF.Demo.Core.Data.Configurations 2 { 3 public class RoleConfiguration : EntityTypeConfiguration<Role> 4 { 5 public RoleConfiguration() 6 { 7 HasMany(m => m.Members).WithMany(n => n.Roles); 8 } 9 }10 }
映射类需要在数据访问上下文中进行应用才能生效,只要在DbContext的OnModelCreating方法中进行映射配置添加即可。
1 protected override void OnModelCreating(DbModelBuilder modelBuilder) 2 { 3 //移除一对多的级联删除约定,想要级联删除可以在 EntityTypeConfiguration<TEntity>的实现类中进行控制 4 modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>(); 5 //多对多启用级联删除约定,不想级联删除可以在删除前判断关联的数据进行拦截 6 //modelBuilder.Conventions.Remove<ManyToManyCascadeDeleteConvention>(); 7 8 modelBuilder.Configurations.Add(new MemberConfiguration()); 9 modelBuilder.Configurations.Add(new MemberExtendConfiguration());10 modelBuilder.Configurations.Add(new MemberAddressConfiguration());11 modelBuilder.Configurations.Add(new RoleConfiguration());12 modelBuilder.Configurations.Add(new LoginLogConfiguration());13 }
经过上面的折腾,数据库结构已经大变,项目当然运行不起来了。
根据提示,必须进行迁移来更新数据库结构。EntityFramework的数据迁移通过 NuGet 来进行。打开程序包管理器控制台(Package Manager Console),键入“ get-help EntityFramework”命令,可以获得相关的帮助信息。
若想了解各个子命令的帮助细节,也可键入“get-help 子命令”命令,例如:get-help Enable-Migrations
下面我们来对项目进行数据迁移,在我们的项目中,EntityFramework的依赖止于项目GMF.Demo.Core.Data,项目的数据迁移也是在此项目中进行。迁移步骤如下:
1 namespace GMF.Demo.Core.Data.Migrations 2 { 3 internal sealed class Configuration : DbMigrationsConfiguration<DemoDbContext> 4 { 5 public Configuration() 6 { 7 AutomaticMigrationsEnabled = false; 8 } 9 10 protected override void Seed(DemoDbContext context)11 {12 // This method will be called after migrating to the latest version.13 14 // You can use the DbSet<T>.AddOrUpdate() helper extension method 15 // to avoid creating duplicate seed data. E.g.16 //17 // context.People.AddOrUpdate(18 // p => p.FullName,19 // new Person { FullName = "Andrew Peters" },20 // new Person { FullName = "Brice Lambson" },21 // new Person { FullName = "Rowan Miller" }22 // );23 //24 }25 }26 }
方法Seed中可以进行数据迁移后的数据初始化工作,将在每次迁移之后运行。如上代码所示,AddOrUpdate是IDbSet<TEntity>的扩展方法,如果指定条件的数据不存在,则会添加,如果存在,会更新。所以,如果数据是通过此方法来初始化的,在与业务更新之后,再次进行数据迁移后,还是会被还原。
还有一个名为InitialCreate的类,配置生成数据库的细节:
1 namespace GMF.Demo.Core.Data.Migrations 2 { 3 public partial class InitialCreate : DbMigration 4 { 5 public override void Up() 6 { 7 CreateTable( 8 "dbo.Roles", 9 c => new10 {11 Id = c.Guid(nullable: false),12 Name = c.String(nullable: false, maxLength: 20),13 Description = c.String(maxLength: 100),14 IsDeleted = c.Boolean(nullable: false),15 AddDate = c.DateTime(nullable: false),16 Timestamp = c.Binary(nullable: false, fixedLength: true, timestamp: true, storeType: "rowversion"),17 })18 .PrimaryKey(t => t.Id);19 20 CreateTable(21 "dbo.Members",22 c => new23 {24 Id = c.Int(nullable: false, identity: true),25 UserName = c.String(nullable: false, maxLength: 20),26 Password = c.String(nullable: false, maxLength: 32),27 NickName = c.String(nullable: false, maxLength: 20),28 Email = c.String(nullable: false, maxLength: 50),29 IsDeleted = c.Boolean(nullable: false),30 AddDate = c.DateTime(nullable: false),31 Timestamp = c.Binary(nullable: false, fixedLength: true, timestamp: true, storeType: "rowversion"),32 })33 .PrimaryKey(t => t.Id);34 35 CreateTable(36 "dbo.MemberExtends",37 c => new38 {39 Id = c.Guid(nullable: false),40 IsDeleted = c.Boolean(nullable: false),41 AddDate = c.DateTime(nullable: false),42 Timestamp = c.Binary(nullable: false, fixedLength: true, timestamp: true, storeType: "rowversion"),43 Member_Id = c.Int(nullable: false),44 })45 .PrimaryKey(t => t.Id)46 .ForeignKey("dbo.Members", t => t.Member_Id)47 .Index(t => t.Member_Id);48 49 CreateTable(50 "dbo.LoginLogs",51 c => new52 {53 Id = c.Guid(nullable: false),54 IpAddress = c.String(nullable: false, maxLength: 15),55 IsDeleted = c.Boolean(nullable: false),56 AddDate = c.DateTime(nullable: false),57 Timestamp = c.Binary(nullable: false, fixedLength: true, timestamp: true, storeType: "rowversion"),58 Member_Id = c.Int(),59 })60 .PrimaryKey(t => t.Id)61 .ForeignKey("dbo.Members", t => t.Member_Id)62 .Index(t => t.Member_Id);63 64 CreateTable(65 "dbo.MemberRoles",66 c => new67 {68 Member_Id = c.Int(nullable: false),69 Role_Id = c.Guid(nullable: false),70 })71 .PrimaryKey(t => new { t.Member_Id, t.Role_Id })72 .ForeignKey("dbo.Members", t => t.Member_Id, cascadeDelete: true)73 .ForeignKey("dbo.Roles", t => t.Role_Id, cascadeDelete: true)74 .Index(t => t.Member_Id)75 .Index(t => t.Role_Id);76 77 }78 79 public override void Down()80 {81 DropIndex("dbo.MemberRoles", new[] { "Role_Id" });82 DropIndex("dbo.MemberRoles", new[] { "Member_Id" });83 DropIndex("dbo.LoginLogs", new[] { "Member_Id" });84 DropIndex("dbo.MemberExtends", new[] { "Member_Id" });85 DropForeignKey("dbo.MemberRoles", "Role_Id", "dbo.Roles");86 DropForeignKey("dbo.MemberRoles", "Member_Id", "dbo.Members");87 DropForeignKey("dbo.LoginLogs", "Member_Id", "dbo.Members");88 DropForeignKey("dbo.MemberExtends", "Member_Id", "dbo.Members");89 DropTable("dbo.MemberRoles");90 DropTable("dbo.LoginLogs");91 DropTable("dbo.MemberExtends");92 DropTable("dbo.Members");93 DropTable("dbo.Roles");94 }95 }96 }
1 namespace GMF.Demo.Core.Data.Migrations 2 { 3 internal sealed class Configuration : DbMigrationsConfiguration<DemoDbContext> 4 { 5 public Configuration() 6 { 7 AutomaticMigrationsEnabled = true; 8 AutomaticMigrationDataLossAllowed = true; 9 }10 11 protected override void Seed(DemoDbContext context)12 {13 List<Member> members = new List<Member>14 {15 new Member { UserName = "admin", Password = "123456", Email = "admin@gmfcn.net", NickName = "管理员" },16 new Member { UserName = "gmfcn", Password = "123456", Email = "mf.guo@qq.com", NickName = "郭明锋" }17 };18 DbSet<Member> memberSet = context.Set<Member>();19 memberSet.AddOrUpdate(m => new { m.Id }, members.ToArray());20 }21 }22 }
修改数据库初始化策略如下:
Database.SetInitializer(new MigrateDatabaseToLatestVersion<DemoDbContext, Configuration>());
经过上面的演练,我们的项目变成如下图所示:
现在的项目中,数据访问上下文DemoDbContext代码如下所示:
1 namespace GMF.Demo.Core.Data.Context 2 { 3 /// <summary> 4 /// Demo项目数据访问上下文 5 /// </summary> 6 [Export(typeof(DbContext))] 7 public class DemoDbContext : DbContext 8 { 9 #region 构造函数10 11 /// <summary>12 /// 初始化一个 使用连接名称为“default”的数据访问上下文类 的新实例13 /// </summary>14 public DemoDbContext()15 : base("default") { }16 17 /// <summary>18 /// 初始化一个 使用指定数据连接名称或连接串 的数据访问上下文类 的新实例19 /// </summary>20 public DemoDbContext(string nameOrConnectionString)21 : base(nameOrConnectionString) { }22 23 #endregion24 25 #region 属性26 27 public DbSet<Role> Roles { get; set; }28 29 public DbSet<Member> Members { get; set; }30 31 public DbSet<MemberExtend> MemberExtends { get; set; }32 33 public DbSet<LoginLog> LoginLogs { get; set; }34 35 #endregion36 37 protected override void OnModelCreating(DbModelBuilder modelBuilder)38 {39 //移除一对多的级联删除约定,想要级联删除可以在 EntityTypeConfiguration<TEntity>的实现类中进行控制40 modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();41 //多对多启用级联删除约定,不想级联删除可以在删除前判断关联的数据进行拦截42 //modelBuilder.Conventions.Remove<ManyToManyCascadeDeleteConvention>();43 44 modelBuilder.Configurations.Add(new MemberConfiguration());45 modelBuilder.Configurations.Add(new MemberExtendConfiguration());46 modelBuilder.Configurations.Add(new MemberAddressConfiguration());47 modelBuilder.Configurations.Add(new RoleConfiguration());48 modelBuilder.Configurations.Add(new LoginLogConfiguration());49 }50 }51 }
由代码可以看出,当前的上下文类与业务实体是强耦合的,分别耦合在DbSet<TEntity>的属性与OnModelCreating方法上。如果要解耦,对于属性,可以使用DbContext.Set<TEntity>()方法来实现指定实体的属性,对于OnModelCreating中的方法实现中的映射配置对象,则可提取一个通用接口,通过接口进行分别映射。
定义接口如下:
1 namespace GMF.Component.Data 2 { 3 /// <summary> 4 /// 实体映射接口 5 /// </summary> 6 [InheritedExport] 7 public interface IEntityMapper 8 { 9 /// <summary>10 /// 将当前实体映射对象注册到当前数据访问上下文实体映射配置注册器中11 /// </summary>12 /// <param name="configurations">实体映射配置注册器</param>13 void RegistTo(ConfigurationRegistrar configurations);14 }15 }
IEntityMapper接口添加了MEF的InheritedExport特性,该特性可以沿着继承链传递所施加的特性。在需要的时候,就可以通过ImportManyAttribute一次性导出所有实现了IEntityMapper接口的实现类对象。
在实体映射类中添加IEntityMapper的实现,如角色映射类中:
1 namespace GMF.Demo.Core.Data.Configurations 2 { 3 public class RoleConfiguration : EntityTypeConfiguration<Role>, IEntityMapper 4 { 5 public RoleConfiguration() 6 { 7 HasMany(m => m.Members).WithMany(n => n.Roles); 8 } 9 10 public void RegistTo(ConfigurationRegistrar configurations)11 {12 configurations.Add(this);13 }14 }15 }
下面来对数据访问上下文进行改造,并转移到数据组件 GMF.Component.Data 中。
添加一个IEnumerable<IEntityMapper>类型的属性EntityMappers,并添加ImportManyAttribute,用于引入所有实现了IEntityMapper的类的对象。
在重写的OnModelCreating方法中,遍历EntityMappers集合,调用其中的RegistTo进行实体映射类对象的添加。
1 namespace GMF.Component.Data 2 { 3 /// <summary> 4 /// EF数据访问上下文 5 /// </summary> 6 [Export(typeof (DbContext))] 7 public class EFDbContext : DbContext 8 { 9 public EFDbContext()10 : base("default") { }11 12 public EFDbContext(string nameOrConnectionString)13 : base(nameOrConnectionString) { }14 15 [ImportMany]16 public IEnumerable<IEntityMapper> EntityMappers { get; set; }17 18 protected override void OnModelCreating(DbModelBuilder modelBuilder)19 {20 modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();21 22 if (Configuration == null)23 {24 return;25 }26 foreach (var mapper in EntityMappers)27 {28 mapper.RegistTo(modelBuilder.Configurations);29 }30 }31 }32 }
上下文EFDbContext在单元操作类 EFRepositoryContext 类中进行使用:
1 namespace GMF.Component.Data 2 { 3 /// <summary> 4 /// 数据单元操作类 5 /// </summary> 6 [Export(typeof (IUnitOfWork))] 7 internal class EFRepositoryContext : UnitOfWorkContextBase 8 { 9 /// <summary>10 /// 获取 当前使用的数据访问上下文对象11 /// </summary>12 protected override DbContext Context13 {14 get { return EFDbContext; }15 }16 17 [Import(typeof (DbContext))]18 private EFDbContext EFDbContext { get; set; }19 }20 }
经过如此重构,DbContext上下文就与实体无关了,数据访问功能与业务实体便完成解耦。
联系客服