OSharp是什么?
OSharp是個快速開發框架,但不是一個大而全的包羅萬象的框架,嚴格的說,OSharp中什么都沒有實現。與其他大而全的框架最大的不同點,就是OSharp只做抽象封裝,不做實現。依賴注入、ORM、對象映射、日志、緩存等等功能,都只定義了一套最基礎最通用的抽象封裝,提供了一套統一的API、約定與規則,並定義了部分執行流程,主要是讓項目在一定的規范下進行開發。所有的功能實現端,都是通過現有的成熟的第三方組件來實現的,除了EntityFramework之外,所有的第三方實現都可以輕松的替換成另一種第三方實現,OSharp框架正是要起隔離作用,保證這種變更不會對業務代碼造成影響,使用統一的API來進行業務實現,解除與第三方實現的耦合,保持業務代碼的規范與穩定。
本文已同步到系列目錄:OSharp快速開發框架解說系列
前言
上篇 的數據層設計中,我們主要設計了數據對對外開放的 實體基類EntityBase<TKey>,單元操作接口IUnitOfWork 和 數據倉儲接口IRepository<TEntity, TKey>,下面我們來解說怎樣來使用 EntityFramework 對這些數據訪問需求進行實現。EntityFramework 的實現中,我們不僅要實現以上設計的兩個接口,還要做以下幾件事:
- 設計一個與 業務實體解耦的 EntityFramework數據上下文類
- 設計 實體加載方案,將 業務實體 加載到上下文中
- 設計數據遷移方案,對 EntityFramework 的運行環境進行初始化
- 使用 已初始化的上下文 實現 數據倉儲操作
下面,我們將逐條進行解說。
代碼分布圖:
設計與業務解耦的上下文類
EntityFramework 的封裝,首要目標就是與業務層,業務實體解耦。怎樣才算是解耦呢?如果不解耦,通常我們使用 EntityFramework 需要定義如下這樣一個上下文類的:
1 public class DemoDbContext : DbContext 2 { 3 public DbSet<Organization> Organizations { get; set; } 4 5 public DbSet<Role> Roles { get; set; } 6 7 public DbSet<User> Users { get; set; } 8 9 protected override void OnModelCreating(DbModelBuilder modelBuilder) 10 { 11 modelBuilder.Entity<Organization>().HasOptional(m => m.Parent).WithMany(n => n.Children); 12 modelBuilder.Entity<Role>().HasRequired(m => m.Organization).WithMany(); 13 modelBuilder.Entity<User>().HasMany(m => m.Roles).WithMany(n => n.Users); 14 } 15 }
在上面的上下文類中,業務實體與上下文類是緊耦合的,每次實體類或者實體之間的關系有變動,都要來修改這個上下文類DemoDbContext,又是無法擴展的,是典型的違反了“開閉原則”(軟件系統對於組件功能的擴展是開放的,是允許對其進行功能擴展的;對於原有代碼的修改是封閉的,即不應該修改原有的代碼。)。所以,我們應該讓 EntityFramework 的上下文與業務實體之間進行解耦,特別是對於 OSharp 這樣作為開發框架存在的基礎設施,解耦更是至關重要。
從上面的示例上下文類中得知,實體類與上下文的耦合主要發生在兩個部分:
- EntityFramework上下文類需擁有業務實體的 DbSet<TEntity> 類型的屬性,這個屬性即是業務實體加載到上下文的途徑,也是進行數據操作時獲取相應實體數據集DbSet<TEntity>的途徑。
- 在上下文類的 OnModelCreating 方法中配置實體間的關系細節
要進行解耦,也需要從這兩方面着手。OSharp 的 EntityFramework 上下文將定義為如下:
1 /// <summary> 2 /// EntityFramework-CodeFirst數據上下文 3 /// </summary> 4 public class CodeFirstDbContext : DbContext, IUnitOfWork, IDependency 5 { 6 /// <summary> 7 /// 初始化一個<see cref="CodeFirstDbContext"/>類型的新實例 8 /// </summary> 9 public CodeFirstDbContext() 10 : this(GetConnectionStringName()) 11 { } 12 13 /// <summary> 14 /// 使用連接名稱或連接字符串 初始化一個<see cref="CodeFirstDbContext"/>類型的新實例 15 /// </summary> 16 public CodeFirstDbContext(string nameOrConnectionString) 17 : base(nameOrConnectionString) 18 { } 19 20 /// <summary> 21 /// 獲取或設置 是否開啟事務提交 22 /// </summary> 23 public bool TransactionEnabled { get; set; } 24 25 /// <summary> 26 /// 獲取 數據庫連接串名稱 27 /// </summary> 28 /// <returns></returns> 29 private static string GetConnectionStringName() 30 { 31 string name = ConfigurationManager.AppSettings.Get("OSharp-ConnectionStringName") 32 ?? ConfigurationManager.AppSettings.Get("ConnectionStringName") ?? "default"; 33 return name; 34 } 35 36 /// <summary> 37 /// 提交當前單元操作的更改。 38 /// </summary> 39 /// <returns>操作影響的行數</returns> 40 public override int SaveChanges() 41 { 42 return SaveChanges(true); 43 } 44 45 /// <summary> 46 /// 提交當前單元操作的更改。 47 /// </summary> 48 /// <param name="validateOnSaveEnabled">提交保存時是否驗證實體約束有效性。</param> 49 /// <returns>操作影響的行數</returns> 50 internal int SaveChanges(bool validateOnSaveEnabled) 51 { 52 bool isReturn = Configuration.ValidateOnSaveEnabled != validateOnSaveEnabled; 53 try 54 { 55 Configuration.ValidateOnSaveEnabled = validateOnSaveEnabled; 56 int count = base.SaveChanges(); 57 TransactionEnabled = false; 58 return count; 59 } 60 catch (DbUpdateException e) 61 { 62 if (e.InnerException != null && e.InnerException.InnerException is SqlException) 63 { 64 SqlException sqlEx = e.InnerException.InnerException as SqlException; 65 string msg = DataHelper.GetSqlExceptionMessage(sqlEx.Number); 66 throw new OSharpException("提交數據更新時發生異常:" + msg, sqlEx); 67 } 68 throw; 69 } 70 finally 71 { 72 if (isReturn) 73 { 74 Configuration.ValidateOnSaveEnabled = !validateOnSaveEnabled; 75 } 76 } 77 } 78 #if NET45 79 80 #region Overrides of DbContext 81 82 /// <summary> 83 /// 異步提交當前單元操作的更改。 84 /// </summary> 85 /// <returns>操作影響的行數</returns> 86 public override Task<int> SaveChangesAsync() 87 { 88 return SaveChangesAsync(true); 89 } 90 91 #endregion 92 93 /// <summary> 94 /// 提交當前單元操作的更改。 95 /// </summary> 96 /// <param name="validateOnSaveEnabled">提交保存時是否驗證實體約束有效性。</param> 97 /// <returns>操作影響的行數</returns> 98 internal async Task<int> SaveChangesAsync(bool validateOnSaveEnabled) 99 { 100 bool isReturn = Configuration.ValidateOnSaveEnabled != validateOnSaveEnabled; 101 try 102 { 103 Configuration.ValidateOnSaveEnabled = validateOnSaveEnabled; 104 int count = await base.SaveChangesAsync(); 105 TransactionEnabled = false; 106 return count; 107 } 108 catch (DbUpdateException e) 109 { 110 if (e.InnerException != null && e.InnerException.InnerException is SqlException) 111 { 112 SqlException sqlEx = e.InnerException.InnerException as SqlException; 113 string msg = DataHelper.GetSqlExceptionMessage(sqlEx.Number); 114 throw new OSharpException("提交數據更新時發生異常:" + msg, sqlEx); 115 } 116 throw; 117 } 118 finally 119 { 120 if (isReturn) 121 { 122 Configuration.ValidateOnSaveEnabled = !validateOnSaveEnabled; 123 } 124 } 125 } 126 #endif 127 protected override void OnModelCreating(DbModelBuilder modelBuilder) 128 { 129 //移除一對多的級聯刪除 130 modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>(); 131 } 132 }
OSharp 中定義的 EntityFramework 上下文類 CodeFirstDbContext 主要做了以下幾件事:
- 上下文類 CodeFirstDbContext 實現了 IUnitOfWork 接口(與 架構系列 的那個上下文進行對比,OSharp 的 EntityFramework 的實現在架構上進行了簡化)。
- 默認讀取 Web.Config 中AppSettings中配置的名為“OSharp-ConnectionStringName”的鍵獲取默認數據庫連接串名稱,進行默認上下文的實例化。
- 上下文在執行提交保存之后,重置 public bool TransactionEnabled { get; set; } 屬性的值為 false。
- 在重寫 OnModelCreating 方法時移除 一對多的級聯刪除(OneToManyCascadeDeleteConvention),防止“刪除一個主干實體,子實體全部自動刪除”的誤刪除悲劇發生。如果需要級聯刪除,再在實體的 Fluent API 配置中單獨開啟。
- 當需要 業務實體TEntity 的 DbSet<TEntity>數據集時,可以通過 DbContext 的 public virtual DbSet<TEntity> Set<TEntity>() where TEntity : class{} 方法來獲取。
解耦的業務實體的加載與數據遷移
解耦的業務實體的加載
上述 EntityFramework 上下文進行了與業務實體的解耦,那么 業務實體 是怎樣加載到 上下文中的呢?下面我們就來解決這個問題。
在 EntityFramework 6 的版本中,EntityFramework 提供了兩個動態加載 業務實體類的途徑:
- 加載單個實體類配置: public virtual ConfigurationRegistrar Add<TEntityType>(EntityTypeConfiguration<TEntityType> entityTypeConfiguration) where TEntityType : class{}
- 加載單個實體類復合配置: public virtual ConfigurationRegistrar Add<TComplexType>(ComplexTypeConfiguration<TComplexType> complexTypeConfiguration) where TComplexType : class
- 使用反射加載程序集中的單個實體類配置或實體類復合配置: public virtual ConfigurationRegistrar AddFromAssembly(Assembly assembly){} ,此API為 EntityFramework 6 新增的
在 OSharp 的業務實體加載設計中,將使用前兩個加載方式。
首先,設計了一個專用於 業務實體加載到上下文 的接口:
1 /// <summary> 2 /// 實體映射接口 3 /// </summary> 4 public interface IEntityMapper 5 { 6 /// <summary> 7 /// 將當前實體映射對象注冊到當前數據訪問上下文實體映射配置注冊器中 8 /// </summary> 9 /// <param name="configurations">實體映射配置注冊器</param> 10 void RegistTo(ConfigurationRegistrar configurations); 11 }
分別對簡單實體與復合實體實現這個接口:
1 /// <summary> 2 /// 數據實體映射配置基類 3 /// </summary> 4 /// <typeparam name="TEntity">動態實體類型</typeparam> 5 /// <typeparam name="TKey">動態主鍵類型</typeparam> 6 public abstract class EntityConfigurationBase<TEntity, TKey> : EntityTypeConfiguration<TEntity>, IEntityMapper 7 where TEntity : EntityBase<TKey> 8 { 9 /// <summary> 10 /// 將當前實體映射對象注冊到當前數據訪問上下文實體映射配置注冊器中 11 /// </summary> 12 /// <param name="configurations">實體映射配置注冊器</param> 13 public void RegistTo(ConfigurationRegistrar configurations) 14 { 15 configurations.Add(this); 16 } 17 } 18 19 20 /// <summary> 21 /// 復合數據實體映射配置基類 22 /// </summary> 23 /// <typeparam name="TComplexType">動態復合實體類型</typeparam> 24 /// <typeparam name="TKey">動態主鍵類型</typeparam> 25 public abstract class ComplexTypeConfigurationBase<TComplexType, TKey> : ComplexTypeConfiguration<TComplexType>, IEntityMapper 26 where TComplexType : EntityBase<TKey> 27 { 28 #region Implementation of IEntityMapper 29 30 /// <summary> 31 /// 將當前實體映射對象注冊到當前數據訪問上下文實體映射配置注冊器中 32 /// </summary> 33 /// <param name="configurations">實體映射配置注冊器</param> 34 public void RegistTo(ConfigurationRegistrar configurations) 35 { 36 configurations.Add(this); 37 } 38 39 #endregion 40 }
每個業務實體類都要實現一個映射配置,在配置中可以通過 FluentAPI 進行實體與數據庫的映射細節配置(如表名,外鍵關系,索引等),例如:
1 public class OrganizationConfiguration : EntityConfigurationBase<Organization, int> 2 { 3 public OrganizationConfiguration() 4 { 5 HasOptional(m => m.Parent).WithMany(n => n.Children); 6 } 7 }
關於實體與數據庫的映射,通常有兩種方式:DataAnnotation 與 FluentAPI,那么,什么時候使用什么方式呢?為了實保持實體類的通用性,減少第三方依賴,通常遵從如下原則:
- 當映射特性與 EntityFramework 無關時,使用 DataAnnotation,例如 Required、StringLength 等。
- 當映射特性與 EntityFramework 耦合時,使用 FluentAPI,例如 ToTable、HasForeignKey、HasMany 等。
在進行 EntityFramework 的初始化的時候,需要將所有實現了 IEntityMapper 接口的所有實體映射配置類的實例都初始化出來,存儲在 DatabaseInitializer.EntityMappers 屬性中,然后在初始化 EntityFramework 上下文的時候,將這些 映射配置的實例 加載到 modelBuilder.Configurations 中,即可完成 EntityFramework 上下文對 業務實體 的加載及映射。
1 protected override void OnModelCreating(DbModelBuilder modelBuilder) 2 { 3 //移除一對多的級聯刪除 4 modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>(); 5 6 //注冊實體配置信息 7 ICollection<IEntityMapper> entityMappers = DatabaseInitializer.EntityMappers; 8 foreach (IEntityMapper mapper in entityMappers) 9 { 10 mapper.RegistTo(modelBuilder.Configurations); 11 } 12 }
數據初始化及遷移配置
種子數據初始化
在項目開發過程中,為了項目運行和測試的需要,往往需要在數據庫創建的時候,向數據庫中添加初始數據(種子數據),EntityFramework 創建數據庫時是支持種子數據的初始化的。
在OSharp中,定義了一個專用於種子數據初始化的接口:
1 /// <summary> 2 /// 初始化種子數據接口 3 /// </summary> 4 public interface ISeedAction 5 { 6 /// <summary> 7 /// 獲取 操作排序,數值越小越先執行 8 /// </summary> 9 int Order { get; } 10 11 /// <summary> 12 /// 定義種子數據初始化過程 13 /// </summary> 14 /// <param name="context">數據上下文</param> 15 void Action(DbContext context); 16 }
在各個模塊中,如果需要進行種子數據的初始化,需要實現這個接口,並在 Action 方法中使用上下文參數 context 實現數據的初始化:
1 public class IdentitySeedAction : ISeedAction 2 { 3 /// <summary> 4 /// 獲取 操作排序,數值越小越先執行 5 /// </summary> 6 public int Order { get { return 0; } } 7 8 /// <summary> 9 /// 定義種子數據初始化過程 10 /// </summary> 11 /// <param name="context">數據上下文</param> 12 public void Action(DbContext context) 13 { 14 List<Organization> organizations = new List<Organization>() 15 { 16 new Organization(){Name = "系統", Remark = "系統根節點", }, 17 }; 18 context.Set<Organization>().AddOrUpdate(m => new { m.Name }, organizations.ToArray()); 19 } 20 }
當數據庫不存在時,可以使用 EntityFramework 中定義的 CreateDatabaseIfNotExists<TDbContext> 來創建數據庫。OSharp 中從這個類派生了一個支持種子數據初始化的類型,來處理在創建數據庫的時候初始化種子數據:
1 /// <summary> 2 /// 在數據庫不存在時使用種子數據創建數據庫 3 /// </summary> 4 public class CreateDatabaseIfNotExistsWithSeed : CreateDatabaseIfNotExists<CodeFirstDbContext> 5 { 6 static CreateDatabaseIfNotExistsWithSeed() 7 { 8 SeedActions = new List<ISeedAction>(); 9 } 10 11 /// <summary> 12 /// 獲取 數據庫創建時的種子數據操作信息集合,各個模塊可以添加自己的初始化數據 13 /// </summary> 14 public static ICollection<ISeedAction> SeedActions { get; private set; } 15 16 protected override void Seed(CodeFirstDbContext context) 17 { 18 IEnumerable<ISeedAction> seedActions = SeedActions.OrderBy(m => m.Order); 19 foreach (ISeedAction seedAction in seedActions) 20 { 21 seedAction.Action(context); 22 } 23 } 24 }
在 EntityFramework 初始化的時候,只需要將 ISeedAction 接口的派生類的實例添加到 SeedActions 屬性中即可: CreateDatabaseIfNotExistsWithSeed.SeedActions.Add(new IdentitySeedAction());
數據遷移配置
OSharp 在數據遷移方面的考慮很簡單,默認開啟了 自動遷移 的遷移方式。自動遷移的啟用主要涉及兩個屬性:
- AutomaticMigrationsEnabled = true;獲取或設置指示遷移數據庫時是否可使用自動遷移的值。
- AutomaticMigrationDataLossAllowed = true;獲取或設置指示是否可接受自動遷移期間的數據丟失的值。 如果設置為 false,則將在數據丟失可能作為自動遷移一部分出現時引發異常。
遷移配置類中同樣定義了種子數據的初始化,需要注意的是:這里的種子數據與 CreateDatabaseIfNotExistsWithSeed 類中的有個不太一樣的地方, CreateDatabaseIfNotExistsWithSeed 類中的種子數據,只在創建數據庫的時候運行一次,而這里的,將會在每次進行遷移的時候,都會對種子數據進行還原。
1 /// <summary> 2 /// 在數據庫不存在時使用種子數據創建數據庫 3 /// </summary> 4 public class CreateDatabaseIfNotExistsWithSeed : CreateDatabaseIfNotExists<CodeFirstDbContext> 5 { 6 static CreateDatabaseIfNotExistsWithSeed() 7 { 8 SeedActions = new List<ISeedAction>(); 9 } 10 11 /// <summary> 12 /// 獲取 數據庫創建時的種子數據操作信息集合,各個模塊可以添加自己的初始化數據 13 /// </summary> 14 public static ICollection<ISeedAction> SeedActions { get; private set; } 15 16 protected override void Seed(CodeFirstDbContext context) 17 { 18 IEnumerable<ISeedAction> seedActions = SeedActions.OrderBy(m => m.Order); 19 foreach (ISeedAction seedAction in seedActions) 20 { 21 seedAction.Action(context); 22 } 23 } 24 }
EntityFramework 初始化
OSharp 中的 EntityFramework 數據存儲運行環境的初始化,主要包含如下幾個方面:
- 將 業務實體 的 EntityTypeConfiguration<TEntity> 實體映射類所在程序集加載到數據層中,並獲取所有 IEntityMapper 接口的派生類的實例以備創建數據上下文 CodeFirstDbContext 實例時使用
- 將所有 ISeedAction 接口派生的種子數據初始化類的實例加載到相應的EntityFramework初始化策略中
- 設置 EntityFramework 初始化策略,當數據庫不存在時,使用 CreateDatabaseIfNotExistsWithSeed 進行初始化;當數據庫存在時,使用 MigrateDatabaseToLatestVersion 數據遷移策略
- EntityFramework 數據庫架構的預熱
初始化的一個示例如下,在主程序入口中(例如Global的Application_Start):
1 private static void DatabaseInitialize() 2 { 3 Assembly assembly = Assembly.GetExecutingAssembly(); 4 DatabaseInitializer.AddMapperAssembly(assembly); 5 CreateDatabaseIfNotExistsWithSeed.SeedActions.Add(new IdentitySeedAction()); 6 7 DatabaseInitializer.Initialize(); 8 }
初始化類 DatabaseInitializer 實現如下:
1 /// <summary> 2 /// 數據庫初始化操作類 3 /// </summary> 4 public class DatabaseInitializer 5 { 6 private static readonly ICollection<Assembly> MapperAssemblies = new List<Assembly>(); 7 8 /// <summary> 9 /// 獲取 數據實體映射配置信息集合 10 /// </summary> 11 public static ICollection<IEntityMapper> EntityMappers { get { return GetAllEntityMapper(); } } 12 13 /// <summary> 14 /// 設置數據庫初始化,策略為自動遷移到最新版本 15 /// </summary> 16 public static void Initialize() 17 { 18 CodeFirstDbContext context = new CodeFirstDbContext(); 19 IDatabaseInitializer<CodeFirstDbContext> initializer; 20 if (!context.Database.Exists()) 21 { 22 initializer = new CreateDatabaseIfNotExistsWithSeed(); 23 } 24 else 25 { 26 initializer = new MigrateDatabaseToLatestVersion<CodeFirstDbContext, MigrationsConfiguration>(); 27 } 28 Database.SetInitializer(initializer); 29 30 //EF預熱 31 ObjectContext objectContext = ((IObjectContextAdapter)context).ObjectContext; 32 StorageMappingItemCollection mappingItemCollection = (StorageMappingItemCollection)objectContext.ObjectStateManager 33 .MetadataWorkspace.GetItemCollection(DataSpace.CSSpace); 34 mappingItemCollection.GenerateViews(new List<EdmSchemaError>()); 35 context.Dispose(); 36 } 37 38 /// <summary> 39 /// 添加需要搜索實體映射的程序集到檢索集合中 40 /// </summary> 41 public static void AddMapperAssembly(Assembly assembly) 42 { 43 assembly.CheckNotNull("assembly"); 44 if (MapperAssemblies.Any(m => m == assembly)) 45 { 46 return; 47 } 48 MapperAssemblies.Add(assembly); 49 } 50 51 private static ICollection<IEntityMapper> GetAllEntityMapper() 52 { 53 Type baseType = typeof(IEntityMapper); 54 Type[] mapperTypes = MapperAssemblies.SelectMany(assembly => assembly.GetTypes()) 55 .Where(type => baseType.IsAssignableFrom(type) && type != baseType && !type.IsAbstract).ToArray(); 56 ICollection<IEntityMapper> result = mapperTypes.Select(type => Activator.CreateInstance(type) as IEntityMapper).ToList(); 57 return result; 58 } 59 }
同時,數據層的核心API IUnitOfWork , IRepository<TEntity, TKey> 的實例化,是將由 Autofac 的 IoC 組件來完成的,需要在 IoC 初始化時進行注冊:
builder.RegisterGeneric(typeof(Repository<,>)).As(typeof(IRepository<,>));
Repository 倉儲操作實現
Repository 的實現類需要一個 IUnitOfWOrk 參數,這個參數是由 IoC 組件進行注入的,並給 UnitOfWork 與 Entities 屬性進行賦值。
1 /// <summary> 2 /// EntityFramework的倉儲實現 3 /// </summary> 4 /// <typeparam name="TEntity">實體類型</typeparam> 5 /// <typeparam name="TKey">主鍵類型</typeparam> 6 public class Repository<TEntity, TKey> : IRepository<TEntity, TKey> where TEntity : EntityBase<TKey> 7 { 8 private readonly DbSet<TEntity> _dbSet; 9 private readonly IUnitOfWork _unitOfWork; 10 11 public Repository(IUnitOfWork unitOfWork) 12 { 13 _unitOfWork = unitOfWork; 14 _dbSet = ((DbContext)unitOfWork).Set<TEntity>(); 15 } 16 17 /// <summary> 18 /// 獲取 當前單元操作對象 19 /// </summary> 20 public IUnitOfWork UnitOfWork { get { return _unitOfWork; } } 21 22 /// <summary> 23 /// 獲取 當前實體類型的查詢數據集 24 /// </summary> 25 public IQueryable<TEntity> Entities { get { return _dbSet; } } 26 27 ... 28 29 }
在操作后台進行提交保存的時候,將根據 UnitOfWork.TransactionEnabled 的值決定是否執行保存。
1 private int SaveChanges() 2 { 3 return _unitOfWork.TransactionEnabled ? 0 : _unitOfWork.SaveChanges(); 4 } 5 6 #if NET45 7 8 private async Task<int> SaveChangesAsync() 9 { 10 return _unitOfWork.TransactionEnabled ? 0 : await _unitOfWork.SaveChangesAsync(); 11 } 12 13 #endif
操作 API 的實現,下面將分類進行解說:
普通 增、改、刪 業務操作API
普通業務操作API主要是對單個或多個實體進行的單個或批量操作API:
1 /// <summary> 2 /// 插入實體 3 /// </summary> 4 /// <param name="entity">實體對象</param> 5 /// <returns>操作影響的行數</returns> 6 public int Insert(TEntity entity) 7 { 8 entity.CheckNotNull("entity"); 9 _dbSet.Add(entity); 10 return SaveChanges(); 11 } 12 13 /// <summary> 14 /// 批量插入實體 15 /// </summary> 16 /// <param name="entities">實體對象集合</param> 17 /// <returns>操作影響的行數</returns> 18 public int Insert(IEnumerable<TEntity> entities) 19 { 20 entities = entities as TEntity[] ?? entities.ToArray(); 21 _dbSet.AddRange(entities); 22 return SaveChanges(); 23 } 24 25 /// <summary> 26 /// 更新實體對象 27 /// </summary> 28 /// <param name="entity">更新后的實體對象</param> 29 /// <returns>操作影響的行數</returns> 30 public int Update(TEntity entity) 31 { 32 entity.CheckNotNull("entity"); 33 ((DbContext)_unitOfWork).Update<TEntity, TKey>(entity); 34 return SaveChanges(); 35 } 36 37 /// <summary> 38 /// 刪除實體 39 /// </summary> 40 /// <param name="entity">實體對象</param> 41 /// <returns>操作影響的行數</returns> 42 public int Delete(TEntity entity) 43 { 44 entity.CheckNotNull("entity"); 45 _dbSet.Remove(entity); 46 return SaveChanges(); 47 } 48 49 public virtual int Delete(TKey key) 50 { 51 CheckEntityKey(key, "key"); 52 TEntity entity = _dbSet.Find(key); 53 return entity == null ? 0 : Delete(entity); 54 } 55 56 /// <summary> 57 /// 刪除所有符合特定條件的實體 58 /// </summary> 59 /// <param name="predicate">查詢條件謂語表達式</param> 60 /// <returns>操作影響的行數</returns> 61 public int Delete(Expression<Func<TEntity, bool>> predicate) 62 { 63 predicate.CheckNotNull("predicate"); 64 TEntity[] entities = _dbSet.Where(predicate).ToArray(); 65 return entities.Length == 0 ? 0 : Delete(entities); 66 } 67 68 /// <summary> 69 /// 批量刪除刪除實體 70 /// </summary> 71 /// <param name="entities">實體對象集合</param> 72 /// <returns>操作影響的行數</returns> 73 public int Delete(IEnumerable<TEntity> entities) 74 { 75 entities = entities as TEntity[] ?? entities.ToArray(); 76 _dbSet.RemoveRange(entities); 77 return SaveChanges(); 78 }
上面的 Update 更新代碼,是通過一個擴展方法來完成的,這個擴展方法基本能解決在 EntityFramework 數據更新中會遇到的問題(詳見架構系列的《數據更新》篇)
1 /// <summary> 2 /// 更新上下文中指定的實體的狀態 3 /// </summary> 4 /// <typeparam name="TEntity">實體類型</typeparam> 5 /// <typeparam name="TKey">主鍵類型</typeparam> 6 /// <param name="dbContext">上下文對象</param> 7 /// <param name="entities">要更新的實體類型</param> 8 public static void Update<TEntity, TKey>(this DbContext dbContext, params TEntity[] entities) where TEntity : EntityBase<TKey> 9 { 10 dbContext.CheckNotNull("dbContext"); 11 entities.CheckNotNull("entities"); 12 13 foreach (TEntity entity in entities) 14 { 15 DbSet<TEntity> dbSet = dbContext.Set<TEntity>(); 16 try 17 { 18 DbEntityEntry<TEntity> entry = dbContext.Entry(entity); 19 if (entry.State == EntityState.Detached) 20 { 21 dbSet.Attach(entity); 22 entry.State = EntityState.Modified; 23 } 24 } 25 catch (InvalidOperationException) 26 { 27 TEntity oldEntity = dbSet.Find(entity.Id); 28 dbContext.Entry(oldEntity).CurrentValues.SetValues(entity); 29 } 30 } 31 }
針對 DTO 的 增、改、刪 業務操作API
在業務層實現對實體的增加,更新操作的時候,如果業務層接收的是 Dto 數據,需要對 Dto 的數據進行合法性檢查,再將 Dto 通過 數據映射組件 AutoMapper 創建或更新相應類型的實體數據模型 Model,然后再按需求對 Model 的導航屬性進行更新,再提交保存。在進行刪除操作的時候,需要使用傳入的主鍵 Id 檢索相應的實體信息,並檢查刪除操作的可行性,再提交到上下文中進行刪除操作,並刪除其他相關數據。在這些針對實體的業務操作中,存在着很多相似的重復代碼,這種重復代碼的存在,會極大降低系統的可維護性。因此,在 數據倉儲操作 中設計了一組專門針對 Dto 的業務操作API,利用 無返回委托 Action<T> 與 有返回委托 Func<T, RT> 來向底層傳遞 各實體業務操作的變化點的業務邏輯,以達到對 Dto 業務重復代碼的徹底重構。
1 /// <summary> 2 /// 以DTO為載體批量插入實體 3 /// </summary> 4 /// <typeparam name="TAddDto">添加DTO類型</typeparam> 5 /// <param name="dtos">添加DTO信息集合</param> 6 /// <param name="checkAction">添加信息合法性檢查委托</param> 7 /// <param name="updateFunc">由DTO到實體的轉換委托</param> 8 /// <returns>業務操作結果</returns> 9 public OperationResult Insert<TAddDto>(ICollection<TAddDto> dtos, Action<TAddDto> checkAction = null, Func<TAddDto, TEntity, TEntity> updateFunc = null) 10 where TAddDto : IAddDto 11 { 12 dtos.CheckNotNull("dtos"); 13 List<string> names = new List<string>(); 14 foreach (var dto in dtos) 15 { 16 TEntity entity = Mapper.Map<TEntity>(dto); 17 try 18 { 19 if (checkAction != null) 20 { 21 checkAction(dto); 22 } 23 if (updateFunc != null) 24 { 25 entity = updateFunc(dto, entity); 26 } 27 } 28 catch (Exception e) 29 { 30 return new OperationResult(OperationResultType.Error, e.Message); 31 } 32 _dbSet.Add(entity); 33 string name = GetNameValue(dto); 34 if (name != null) 35 { 36 names.Add(name); 37 } 38 } 39 int count = SaveChanges(); 40 return count > 0 41 ? new OperationResult(OperationResultType.Success, 42 names.Count > 0 43 ? "信息“{0}”添加成功".FormatWith(names.ExpandAndToString()) 44 : "{0}個信息添加成功".FormatWith(dtos.Count)) 45 : new OperationResult(OperationResultType.NoChanged); 46 } 47 48 /// <summary> 49 /// 以DTO為載體批量更新實體 50 /// </summary> 51 /// <typeparam name="TEditDto">更新DTO類型</typeparam> 52 /// <param name="dtos">更新DTO信息集合</param> 53 /// <param name="checkAction">更新信息合法性檢查委托</param> 54 /// <param name="updateFunc">由DTO到實體的轉換委托</param> 55 /// <returns>業務操作結果</returns> 56 public OperationResult Update<TEditDto>(ICollection<TEditDto> dtos, Action<TEditDto> checkAction = null, Func<TEditDto, TEntity, TEntity> updateFunc = null) 57 where TEditDto : IEditDto<TKey> 58 { 59 dtos.CheckNotNull("dtos" ); 60 List<string> names = new List<string>(); 61 foreach (var dto in dtos) 62 { 63 TEntity entity = _dbSet.Find(dto.Id); 64 if (entity == null) 65 { 66 return new OperationResult(OperationResultType.QueryNull); 67 } 68 entity = Mapper.Map(dto, entity); 69 try 70 { 71 if (checkAction != null) 72 { 73 checkAction(dto); 74 } 75 if (updateFunc != null) 76 { 77 entity = updateFunc(dto, entity); 78 } 79 } 80 catch (Exception e) 81 { 82 return new OperationResult(OperationResultType.Error, e.Message); 83 } 84 ((DbContext)_unitOfWork).Update<TEntity, TKey>(entity); 85 string name = GetNameValue(dto); 86 if (name != null) 87 { 88 names.Add(name); 89 } 90 } 91 int count = SaveChanges(); 92 return count > 0 93 ? new OperationResult(OperationResultType.Success, 94 names.Count > 0 95 ? "信息“{0}”更新成功".FormatWith(names.ExpandAndToString()) 96 : "{0}個信息更新成功".FormatWith(dtos.Count)) 97 : new OperationResult(OperationResultType.NoChanged); 98 } 99 100 /// <summary> 101 /// 以標識集合批量刪除實體 102 /// </summary> 103 /// <param name="ids">標識集合</param> 104 /// <param name="checkAction">刪除前置檢查委托</param> 105 /// <param name="deleteFunc">刪除委托,用於刪除關聯信息</param> 106 /// <returns>業務操作結果</returns> 107 public OperationResult Delete(ICollection<TKey> ids, Action<TEntity> checkAction = null, Func<TEntity, TEntity> deleteFunc = null) 108 { 109 ids.CheckNotNull("ids" ); 110 List<string> names = new List<string>(); 111 foreach (var id in ids) 112 { 113 TEntity entity = _dbSet.Find(id); 114 try 115 { 116 if (checkAction != null) 117 { 118 checkAction(entity); 119 } 120 if (deleteFunc != null) 121 { 122 entity = deleteFunc(entity); 123 } 124 } 125 catch (Exception e) 126 { 127 return new OperationResult(OperationResultType.Error, e.Message); 128 } 129 _dbSet.Remove(entity); 130 string name = GetNameValue(entity); 131 if (name != null) 132 { 133 names.Add(name); 134 } 135 } 136 int count = SaveChanges(); 137 return count > 0 138 ? new OperationResult(OperationResultType.Success, 139 names.Count > 0 140 ? "信息“{0}”刪除成功".FormatWith(names.ExpandAndToString()) 141 : "{0}個信息刪除成功".FormatWith(ids.Count)) 142 : new OperationResult(OperationResultType.NoChanged); 143 }
上面的代碼,使用了委托 Action , Func<T> 來封裝業務邏輯中的變化點(比如實體合法性檢查,給實體的導航屬性賦值等),而將公共代碼提取出來下沉到底層中,這是一個很好的封裝思路。
使用上面的封裝,我們在業務實現時只需要編號非常核心的幾行代碼,即可完成一個業務的操作,例如:
1 /// <summary> 2 /// 添加組織機構信息信息 3 /// </summary> 4 /// <param name="dtos">要添加的組織機構信息DTO信息</param> 5 /// <returns>業務操作結果</returns> 6 public OperationResult AddOrganizations(params OrganizationDto[] dtos) 7 { 8 dtos.CheckNotNull("dtos"); 9 List<Organization> organizations = new List<Organization>(); 10 OperationResult result = _organizationRepository.Insert(dtos, 11 dto => 12 { 13 if (_organizationRepository.ExistsCheck(m => m.Name == dto.Name)) 14 { 15 throw new Exception("組織機構名稱“{0}”已存在,不能重復添加。".FormatWith(dto.Name)); 16 } 17 }, 18 (dto, entity) => 19 { 20 if (dto.ParentId.HasValue && dto.ParentId.Value > 0) 21 { 22 Organization parent = _organizationRepository.GetByKey(dto.ParentId.Value); 23 if (parent == null) 24 { 25 throw new Exception("指定父組織機構不存在。"); 26 } 27 entity.Parent = parent; 28 } 29 organizations.Add(entity); 30 return entity; 31 }); 32 if (result.ResultType == OperationResultType.Success) 33 { 34 int[] ids = organizations.Select(m => m.Id).ToArray(); 35 RefreshOrganizationsTreePath(ids); 36 } 37 return result; 38 } 39 40 /// <summary> 41 /// 更新組織機構信息信息 42 /// </summary> 43 /// <param name="dtos">包含更新信息的組織機構信息DTO信息</param> 44 /// <returns>業務操作結果</returns> 45 public OperationResult EditOrganizations(params OrganizationDto[] dtos) 46 { 47 dtos.CheckNotNull("dtos"); 48 List<Organization> organizations = new List<Organization>(); 49 OperationResult result = _organizationRepository.Update(dtos, 50 dto => 51 { 52 if (_organizationRepository.ExistsCheck(m => m.Name == dto.Name, dto.Id)) 53 { 54 throw new Exception("組織機構名稱“{0}”已存在,不能重復添加。".FormatWith(dto.Name)); 55 } 56 }, 57 (dto, entity) => 58 { 59 if (!dto.ParentId.HasValue || dto.ParentId == 0) 60 { 61 entity.Parent = null; 62 } 63 else if (entity.Parent != null && entity.Parent.Id != dto.ParentId) 64 { 65 Organization parent = _organizationRepository.GetByKey(dto.Id); 66 if (parent == null) 67 { 68 throw new Exception("指定父組織機構不存在。"); 69 } 70 entity.Parent = parent; 71 } 72 organizations.Add(entity); 73 return entity; 74 }); 75 if (result.ResultType == OperationResultType.Success) 76 { 77 int[] ids = organizations.Select(m => m.Id).ToArray(); 78 RefreshOrganizationsTreePath(ids); 79 } 80 return result; 81 } 82 83 /// <summary> 84 /// 刪除組織機構信息信息 85 /// </summary> 86 /// <param name="ids">要刪除的組織機構信息編號</param> 87 /// <returns>業務操作結果</returns> 88 public OperationResult DeleteOrganizations(params int[] ids) 89 { 90 ids.CheckNotNull("ids"); 91 OperationResult result = _organizationRepository.Delete(ids, 92 entity => 93 { 94 if (entity.Children.Any()) 95 { 96 throw new Exception("組織機構“{0}”的子級不為空,不能刪除。".FormatWith(entity.Name)); 97 } 98 }); 99 return result; 100 }
如上面的代碼所示,只需要分別去實現 Action 與 Func,進行最核心的業務代碼實現,而其他的事,底層已經完全做了,是不是很簡潔
數據查詢 API
通過IQueryable<T>查詢數據源,能滿足大部分數據查詢的需求,但某些 EntityFramework 的特定查詢需求,還是應該單獨定義 數據查詢API,以更好的保障不丟失 EntityFramework 的數據查詢自由度。在這里主要定義了 通過主鍵查找實體、使用 Include 包含指定導航屬性 的數據查詢API:
1 /// <summary> 2 /// 實體存在性檢查 3 /// </summary> 4 /// <param name="predicate">查詢條件謂語表達式</param> 5 /// <param name="id">編輯的實體標識</param> 6 /// <returns>是否存在</returns> 7 public bool ExistsCheck(Expression<Func<TEntity, bool>> predicate, TKey id = default(TKey)) 8 { 9 TKey defaultId = default(TKey); 10 var entity = _dbSet.Where(predicate).Select(m => new { m.Id }).SingleOrDefault(); 11 bool exists = id.Equals(defaultId) ? entity != null : entity != null && entity.Id.Equals(defaultId); 12 return exists; 13 } 14 15 /// <summary> 16 /// 查找指定主鍵的實體 17 /// </summary> 18 /// <param name="key">實體主鍵</param> 19 /// <returns>符合主鍵的實體,不存在時返回null</returns> 20 public TEntity GetByKey(TKey key) 21 { 22 CheckEntityKey(key, "key"); 23 return _dbSet.Find(key); 24 } 25 26 /// <summary> 27 /// 獲取貪婪加載導航屬性的查詢數據集 28 /// </summary> 29 /// <param name="path">屬性表達式,表示要貪婪加載的導航屬性</param> 30 /// <returns>查詢數據集</returns> 31 public IQueryable<TEntity> GetInclude<TProperty>(Expression<Func<TEntity, TProperty>> path) 32 { 33 path.CheckNotNull("path"); 34 return _dbSet.Include(path); 35 } 36 37 /// <summary> 38 /// 獲取貪婪加載多個導航屬性的查詢數據集 39 /// </summary> 40 /// <param name="paths">要貪婪加載的導航屬性名稱數組</param> 41 /// <returns>查詢數據集</returns> 42 public IQueryable<TEntity> GetIncludes(params string[] paths) 43 { 44 paths.CheckNotNull("paths"); 45 IQueryable<TEntity> source = _dbSet; 46 foreach (var path in paths) 47 { 48 source = source.Include(path); 49 } 50 return source; 51 }
至此,EntityFramework 數據層的搭建已基本完成,有了數據層,就可以着手進行 實際業務 上的工作了。
開源說明
github.com
OSharp項目已在github.com上開源,地址為:https://github.com/i66soft/osharp,歡迎閱讀代碼,歡迎 Fork,如果您認同 OSharp 項目的思想,歡迎參與 OSharp 項目的開發。
在Visual Studio 2013中,可直接獲取 OSharp 的最新源代碼,獲取方式如下,地址為:https://github.com/i66soft/osharp.git
nuget
OSharp的相關類庫已經發布到nuget上,歡迎試用,直接在nuget上搜索 “osharp” 關鍵字即可找到
系列導航
本文已同步到系列目錄:OSharp快速開發框架解說系列