前言
經過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。要配置的實體類型將成為關系中的實體。作為關系目標的實體類型將成為依賴對象,且包含主體的外鍵。
- WillCascadeOnDelete:配置是否對關系啟用級聯刪除。
- Map:將關系配置為使用未在對象模型中公開的外鍵屬性。可通過指定配置操作來自定義列和表。如果指定了空的配置操作,則約定將生成列名。如果在對象模型中公開了外鍵屬性,則使用 HasForeignKey 方法。並非所有關系都支持在對象模型中公開外鍵屬性。
- MapKey:配置外鍵的列名。
- ToTable:配置外鍵列所在表的名稱和架構。
經常用到的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 => 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 }
- 執行“Add-Migration FirstMigration”命令,添加一個名為FirstMigration的遷移
- 執行“Update-Database”命令,更新數據庫架構
如果更新數據庫存在沖突而不能執行更新,可以添加 -Force強制執行,例如:“Update-Database -Force” - 設置自動遷移
每次都通過控制台來進行遷移太過麻煩,可以設置為自動遷移。
有以下兩個參數可以對自動遷移進行設置:
1. AutomaticMigrationsEnabled:獲取或設置 指示遷移數據庫時是否可使用自動遷移的值。
2. AutomaticMigrationDataLossAllowed:獲取或設置 指示是否可接受自動遷移期間的數據丟失的值。如果設置為false,則將在數據丟失可能作為自動遷移一部分出現時引發異常。
修改遷移的Configuration類如下:
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 #endregion 24 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 #endregion 36 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 Context 13 { 14 get { return EFDbContext; } 15 } 16 17 [Import(typeof (DbContext))] 18 private EFDbContext EFDbContext { get; set; } 19 } 20 }
經過如此重構,DbContext上下文就與實體無關了,數據訪問功能與業務實體便完成解耦。
我們來對比重構前后的變化:
我們來對比一下重構前后的變化:
重構前:
- 【缺】上下文與實體類強耦合,不通用,每添加一個實體都要修改上下文類
- 【優】上下文中有實體信息,支持手動數據遷移,可以使用命令行進行遷移功能的啟用、添加遷移腳本
重構后:
- 【優】上下文與實體類解耦,能通用
- 【缺?優?】上下文中沒有實體信息,無法支持手動遷移(手動遷移需要上下文中的實體信息來生成遷移腳本),但支持自動遷移(使用運行時的上下文,這時已經加載了實體信息了)
那么,是否重構,就看具體需求而定了。不過項目最終還是要使用自動遷移的,因為在項目上線之后,如果需要遷移線上的數據庫,那時數據庫在服務器中了,就不可能使用VS的控制台命令行的方式來添加遷移了,只能啟用自動遷移來完成數據庫結構的變更遷移。
還有個問題,就是沒有了帶實體的上下文類,無法通過命令行的方式來啟用數據遷移功能了,怎樣啟用數據遷移呢?這時需要手動在GMF.Demo.Core.Data項目中手動來添加一個Migrations文件夾,再在文件夾中添加一個繼承自 DbMigrationsConfiguration<TDbContext> 的Configuration類來啟用數據遷移功能,並且設置AutomaticMigrationsEnabled與AutomaticMigrationDataLossAllowed兩個屬性為true來啟用自動遷移。最后,還需要將數據庫初始化策略更改為“遷移數據庫到最新版本(MigrateDatabaseToLatestVersion)”,即 Database.SetInitializer(new MigrateDatabaseToLatestVersion<TDbContext, Configuration>()); 來引用Configuration類。
源碼獲取
為了讓大家能第一時間獲取到本架構的最新代碼,也為了方便我對代碼的管理,本系列的源碼已加入微軟的開源項目網站 http://www.codeplex.com,地址為:
https://gmframework.codeplex.com/
可以通過下列途徑獲取到最新代碼:
- 如果你是本項目的參與者,可以通過VS自帶的團隊TFS直接連接到 https://tfs.codeplex.com:443/tfs/TFS17 獲取最新代碼
- 如果你安裝有SVN客戶端(親測TortoiseSVN 1.6.7可用),可以連接到 https://gmframework.svn.codeplex.com/svn 獲取最新代碼
- 如果以上條件都不滿足,你可以進入頁面 https://gmframework.codeplex.com/SourceControl/latest 查看最新代碼,也可以點擊頁面上的 Download 鏈接進行壓縮包的下載,你還可以點擊頁面上的 History 鏈接獲取到歷史版本的源代碼
- 如果你想和大家一起學習MVC,學習EF,歡迎加入Q群:5008599(群發言僅限技術討論,拒絕閑聊,拒絕醬油,拒絕廣告)
- 如果你想與我共同來完成這個開源項目,可以隨時聯系我。
系列導航
- MVC實用架構設計(〇)——總體設計
- MVC實用架構設計(一)——項目結構搭建
- MVC實用架構設計(二)——使用MEF應用IOC
- MVC實用架構設計(三)——EF-Code First(1):Repository,UnitOfWork,DbContext
- MVC實用架構設計(三)——EF-Code First(2):實體映射、數據遷移,重構
- MVC實用架構設計(三)——EF-Code First(3):使用T4模板生成相似代碼
- MVC實用架構設計(三)——EF-Code First(4):數據查詢
- MVC實用架構設計(三)——EF-Code First(5):二級緩存
- MVC實體架構設計(三)——EF-Code First(6):數據更新
- 未完待續。。。