〇、目录
一、前言
二、实体映射
(一) DataAnnotation
(二) Fluent API
(三) 映射代码示例
三、数据迁移
四、代码重构
五、源码获取
六、系列导航
一、前言
经过EF的《第一篇》,我们已经把数据访问层基本搭建起来了,但并没有涉及实体关系。实体关系对于一个数据库系统来说至关重要,而且EF的各个实体之间的联系,实体之间的协作,联合查询等也都依赖于这些实体关系。
二、实体映射
实体与数据库的映射可以通过DataAnnotation与FluentAPI两种方式来进行映射:
(一) DataAnnotation
DataAnnotation 特性由.NET 3.5中引进,给.NET中的类提供了一种添加验证的方式。DataAnnotation由命名空间System.ComponentModel.DataAnnotations提供。下面列举实体模型中常用的DataAnnotation特性:
- KeyAttribute:对应数据库中的主键
- RequiredAttribute:对应数据库中字段的数据是否可以为null
- MaxLengthAttribute:对应数据库中字符串类型字段的最大长度
- MinLengthAttribute:在数据库中无对应,但在代码中字符串最小长度
- ConcurrencyCheckAttribute:指定用于开放式并发检查的列的数据类型
- TimestampAttribute:将列的数据类型指定为行版本
System.ComponentModel.DataAnnotations命名空间中只定义了部分实体验证的特性,在EntityFramework程序集中定义了更多的数据映射特性:
- DatabaseGeneratedAttribute:标记指定实体属性是由数据库生成的,并指定生成策略(None数据库不生成值,Identity当插入行时,数据库生成值,Computed当插入或更新行时,数据库生成值)
- ColumnAttribute:指定实体属性在数据库中的列名及数据类型
- TableAttribute:指定实体类对应的数据表名
- ForeignKeyAttribute:指定导航属性的外键字段
- NotMappedAttribute:标记指定实体属性在创建数据库中不创建对应字段
- ComplexTypeAttribute:标记指定实体属性是将一个对象作为另一个对象的属性,映射到数据库中则子对象表现为多个属性字段
对于实体关系对应的数据表关系,无非“0:1,1:1,0:N,1:N,N:N”这几种,可以使用导航属性中的数据类型来表示,0…1端使用单实体类型表示,N端使用ICollection<T>集合类型表示。对于单实体端,默认是可为空的,即为0关系,如果要设置为1关系,要使用[Required]标签来进行标记。但对于一对一中的关系主体与依赖对象确无法做更细节的控制。
(二) Fluent API
使用DataAnnotation非常简单,但对于EntityFramework中的特性,就要在实体类中引入EntityFramework程序集,但实体类最好能是保持与架构无关性的POCO类,才能更具通用性。所以,最好是在数据层中使用FluentAPI在数据层中进行实体类与数据库之间的映射工作。
当然,System.ComponentModel.DataAnnotations命名空间的DataAnnotation在EntityFramework程序集中也有相应的API:
- HasKey - KeyAttribute:配置此实体类型的主键属性
- IsRequired - RequiredAttribute:将此属性配置为必需属性。用于存储此属性的数据库列将不可以为null
- HasMaxLength - MaxLengthAttribute:将属性配置为具有指定的最大长度
- IsConcurrencyToken - ConcurrencyCheckAttribute:将属性配置为用作开放式并发标记
- IsRowVersion - TimestampAttribute:将属性配置为数据库中的行版本。实际数据类型将因使用的数据库提供程序而异。将属性设置为行版本会自动将属性配置为开放式并发标记。
上面这些API均无需引用EntityFramework,推荐使用DataAnnotation方式来设置映射。
以下API的DataAnnotation特性是在EntityFramework中定义,如果也使用DataAnnotation方式来设置映射,就会给实体类增加额外的第三方程序集的依赖。所以以下API的映射推荐使用FluentAPI的方式来设置映射:
- ToTable - TableAttribute:配置此实体类型映射到的表名
- HasColumnName - ColumnAttribute:配置用于存储属性的数据库列的名称
- HasForeignKey - ForeignKeyAttribute:将关系配置为使用在对象模型中的外键属性。如果未在对象模型中公开外键属性,则使用Map方法
- Ignore - NotMappedAttribute:从模型中排队某个属性,使该属性不会映射到数据库
- HasRequired:通过此实体类型配置必需关系。除非指定此关系,否则实体类型的实例将无法保存到数据库。数据库中的外键不可为null。
- HasOptional:从此实体类型配置可选关系。实体类型的实例将能保存到数据库,而无需指定此关系。数据库中的外键可为null。
- HasMany:从此实体类型配置一对多关系。
- WithOptional:将关系配置为required:optional。(required:0…1端的1,表示必需,不可为null;optional:0…1端的0,表示可选,可为null。下同)
- WithOptionalDependent:将关系配置为optional:optional。要配置的实体类型将成为依赖对象,且包含主体的外键。作为关系目标的实体类型将成为关系中的主体。
- WithOptionalPrincipal:将关系配置为optional:optional。要配置的实体类型将成为关系中的主体。作为关系目标的实体类型将成为依赖对象,且包含主体的外键。
- WithRequired:将关系的指定端配置为必需的,且在关系的另一端有导航属性。
- WithRequiredDependent:将关系配置为required:required。要配置的实体类型将成为依赖对象,且包含主体的外键。作为关系目标的实体类型将成为关系中的主体。
- WithRequiredPrincipal:将关系配置为required:required。要配置的实体类型将成为关系中的实体。作为关系目标的实体类型将成为依赖对象,且包含主体的外键。
经常用到的DataAnnotation与FluentAPI列举完了,使用上还是遵守这个原则:
如果在System.ComponentModel.DataAnnotations命名空间存在相应的标签,就使用 DataAnnotation 的方式,如果不存在,则使用 FluentAPI 的方式。
(三) 映射代码示例
实体类关系图:
上图是一个以用户信息为中心的实体关系图,关系说明如下:
- 一个用户可拥有一个可选的用户扩展信息(1 - 0)
- 一个用户扩展信息拥有一个必需的所属用户信息(0 - 1)
- 一个用户扩展信息拥有一个用户地址信息(复合类型)
- 一个用户可对应多个登录日志信息(1 - N)
- 一个登录日志拥有一个必需的所属用户信息(N- 1)
- 一个用户可以拥有多个角色(N - N)
- 一个角色可以分配给多个用户(N - N)
实体类定义:
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 = 1 20 } 21 }
角色类型(枚举)
实体类映射:
实体类映射中,关系的映射配置在关系的两端都可以配置。例如,用户信息与登录信息的 一对多 关系可以在用户信息端配置:
HasMany(m => m.LoginLogs).WithRequired(n => n.Member);
等效于在登录日志信息端配置:
HasRequired(m => m.Member).WithMany(n => n.LoginLogs);
但是,如果所有的关系映射都在作为主体的用户信息端进行配置,势必造成用户信息端配置的臃肿与职责不明。所以,为了保持各个实体类型的职责单一,实体关系推荐在关系的非主体端进行映射。
用户信息映射类,用户信息是关系的主体,所有的关系都不在此映射类中进行配置
1 namespace GMF.Demo.Core.Data.Configurations 2 { 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,项目的数据迁移也是在此项目中进行。迁移步骤如下:
- 在“程序包管理器控制台”键入命令:Enable-Migrations -ProjectName GMF.Demo.Core.Data
- 添加后,项目中添加了一个名为Migrations的文件夹
添加生成以下代码: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 => new
10 {
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 => new
23 {
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 => new
38 {
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 => new
52 {
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 => new
67 {
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 =&g