前言
終於到EF了,實在不好意思,最近有點忙,本篇離上一篇發布已經一個多星期了,工作中的小迭代告一段落,終於有點時間來繼續我們的架構設計了,在這里先對大家表示歉意。
其實這段時間我並不是把這個系列給忘記了,而是一直在思考,想着接下來應該怎么寫。因為園子里已經有很多非常優秀的EF的文章了,比如:
- Entity Framework Code First 學習日記
- 【譯著】Code First :使用Entity. Framework編程
- Entity Framework技術系列
- EF框架step by step
這些系列都寫得非常好,基本涵蓋了EF的所有常見技術點。我再詳寫這些就顯得多余了,但為了整個系列的完整性,還是要提上一提,不然后面就沒法繼續了。
本篇會比較長,主要講解UnitOfWork、Repository模式以及DbContext實現類的構造,來給架構demo添加數據訪問功能,實現數據庫驗證的用戶登錄功能。
Repository
在數據庫系統中,對於數據層來說,所有的操作歸根結底無非“C(增加)、R(讀取)、U(修改)、D(刪除)”這四種操作。四種操作當中,與與業務相關度最大的是讀取操作,根據各種不同的業務需求提交不同的查詢,其最終執行應該放到業務層面中去進行,而增加,修改,刪除這三種操作較為通用,可以作為通用數據操作封裝到Repository中。在Repository中,唯一的變化點就是各種不同的實體類型,既然是變化點就應該進行封裝,這里使用泛型來封裝這個變化點。
對於實體的查詢操作,需要着重說明一下。EF是ORM,ORM最大的特點就是看到的使用的都是對象,讀取一個對象,就相當於執行了一個“select * from table”的查詢操作,稍微了解數據庫的同學都知道,這是非常消耗性能與內存的,數據查詢最好還是應該按需查詢,要什么取什么。EF的查詢操作如果不處理好,很容易造成巨大的性能問題,比如有些同學喜歡在Repository中定義諸如GetByName,GetByXX,GetByXXX的操作,這些操作通常並不是每個實體都需要,即使需要也很有局限性,最后這些操作大都倫為“雞肋”,限制了使用場景,也不能按需查詢。還有些同學定義考慮到了按條件查詢,定義了比如GetByPredicate(predicate)的操作,但使用了IEnumerable<T>的返回值,相當於把這部分數據都加載到內存中,再進行后續操作。甚至還有定義 IEnumerable<T> GetAll(); 操作的,明白的同學都知道這是件多么恐怖的事,相當於把整個表的數據都加載到內存中,再進行后續操作!!!諸如此類,問題多多……其實,數據查詢,只要在Repository中定義一個只讀的IQueryable<T>的數據集,其他的事就完全不用管了,業務層中想要什么就取什么就好了,當然,為了充分利用EF對主鍵查詢的緩存特性,定義一個 GetByKey 的查詢操作還是可以接受的。PS:關於EF數據查詢的問題,后面還會專門有一篇文章來探討。總之,EF的數據查詢做不好,影響很大,這也是很多同學說EF性能不好的重要原因。
不知不覺說了那么多,呵呵,主要是關於數據查詢有太多的話想說了,但這不是本文的重點,嚴重跑題了。直接上接口定義吧:
1 namespace GMF.Component.Data 2 { 3 /// <summary> 4 /// 定義倉儲模型中的數據標准操作 5 /// </summary> 6 /// <typeparam name="TEntity">動態實體類型</typeparam> 7 public interface IRepository<TEntity> where TEntity : Entity 8 { 9 #region 屬性 10 11 /// <summary> 12 /// 獲取 當前實體的查詢數據集 13 /// </summary> 14 IQueryable<TEntity> Entities { get; } 15 16 #endregion 17 18 #region 公共方法 19 20 /// <summary> 21 /// 插入實體記錄 22 /// </summary> 23 /// <param name="entity"> 實體對象 </param> 24 /// <param name="isSave"> 是否執行保存 </param> 25 /// <returns> 操作影響的行數 </returns> 26 int Insert(TEntity entity, bool isSave = true); 27 28 /// <summary> 29 /// 批量插入實體記錄集合 30 /// </summary> 31 /// <param name="entities"> 實體記錄集合 </param> 32 /// <param name="isSave"> 是否執行保存 </param> 33 /// <returns> 操作影響的行數 </returns> 34 int Insert(IEnumerable<TEntity> entities, bool isSave = true); 35 36 /// <summary> 37 /// 刪除指定編號的記錄 38 /// </summary> 39 /// <param name="id"> 實體記錄編號 </param> 40 /// <param name="isSave"> 是否執行保存 </param> 41 /// <returns> 操作影響的行數 </returns> 42 int Delete(object id, bool isSave = true); 43 44 /// <summary> 45 /// 刪除實體記錄 46 /// </summary> 47 /// <param name="entity"> 實體對象 </param> 48 /// <param name="isSave"> 是否執行保存 </param> 49 /// <returns> 操作影響的行數 </returns> 50 int Delete(TEntity entity, bool isSave = true); 51 52 /// <summary> 53 /// 刪除實體記錄集合 54 /// </summary> 55 /// <param name="entities"> 實體記錄集合 </param> 56 /// <param name="isSave"> 是否執行保存 </param> 57 /// <returns> 操作影響的行數 </returns> 58 int Delete(IEnumerable<TEntity> entities, bool isSave = true); 59 60 /// <summary> 61 /// 刪除所有符合特定表達式的數據 62 /// </summary> 63 /// <param name="predicate"> 查詢條件謂語表達式 </param> 64 /// <param name="isSave"> 是否執行保存 </param> 65 /// <returns> 操作影響的行數 </returns> 66 int Delete(Expression<Func<TEntity, bool>> predicate, bool isSave = true); 67 68 /// <summary> 69 /// 更新實體記錄 70 /// </summary> 71 /// <param name="entity"> 實體對象 </param> 72 /// <param name="isSave"> 是否執行保存 </param> 73 /// <returns> 操作影響的行數 </returns> 74 int Update(TEntity entity, bool isSave = true); 75 76 /// <summary> 77 /// 查找指定主鍵的實體記錄 78 /// </summary> 79 /// <param name="key"> 指定主鍵 </param> 80 /// <returns> 符合編號的記錄,不存在返回null </returns> 81 TEntity GetByKey(object key); 82 83 #endregion 84 } 85 }
還要說明一下,每個操作方法都帶有一個 isSave 可選參數,是為了單個實體操作的需要,免去了每次都要調用 context.SaveChanged()的麻煩。如果是進行多個實體的單元事務操作,就需要把這個參數設置為 false 。
Repository的通用實現如下:
1 namespace GMF.Component.Data 2 { 3 /// <summary> 4 /// EntityFramework倉儲操作基類 5 /// </summary> 6 /// <typeparam name="TEntity">動態實體類型</typeparam> 7 public abstract class EFRepositoryBase<TEntity> : IRepository<TEntity> where TEntity : Entity 8 { 9 #region 屬性 10 11 /// <summary> 12 /// 獲取 倉儲上下文的實例 13 /// </summary> 14 [Import] 15 public IUnitOfWork UnitOfWork { get; set; } 16 17 /// <summary> 18 /// 獲取或設置 EntityFramework的數據倉儲上下文 19 /// </summary> 20 protected IUnitOfWorkContext EFContext 21 { 22 get 23 { 24 if (UnitOfWork is IUnitOfWorkContext) 25 { 26 return UnitOfWork as IUnitOfWorkContext; 27 } 28 throw new DataAccessException(string.Format("數據倉儲上下文對象類型不正確,應為IUnitOfWorkContext,實際為 {0}", UnitOfWork.GetType().Name)); 29 } 30 } 31 32 /// <summary> 33 /// 獲取 當前實體的查詢數據集 34 /// </summary> 35 public virtual IQueryable<TEntity> Entities 36 { 37 get { return EFContext.Set<TEntity>(); } 38 } 39 40 #endregion 41 42 #region 公共方法 43 44 /// <summary> 45 /// 插入實體記錄 46 /// </summary> 47 /// <param name="entity"> 實體對象 </param> 48 /// <param name="isSave"> 是否執行保存 </param> 49 /// <returns> 操作影響的行數 </returns> 50 public virtual int Insert(TEntity entity, bool isSave = true) 51 { 52 PublicHelper.CheckArgument(entity, "entity"); 53 EFContext.RegisterNew(entity); 54 return isSave ? EFContext.Commit() : 0; 55 } 56 57 /// <summary> 58 /// 批量插入實體記錄集合 59 /// </summary> 60 /// <param name="entities"> 實體記錄集合 </param> 61 /// <param name="isSave"> 是否執行保存 </param> 62 /// <returns> 操作影響的行數 </returns> 63 public virtual int Insert(IEnumerable<TEntity> entities, bool isSave = true) 64 { 65 PublicHelper.CheckArgument(entities, "entities"); 66 EFContext.RegisterNew(entities); 67 return isSave ? EFContext.Commit() : 0; 68 } 69 70 /// <summary> 71 /// 刪除指定編號的記錄 72 /// </summary> 73 /// <param name="id"> 實體記錄編號 </param> 74 /// <param name="isSave"> 是否執行保存 </param> 75 /// <returns> 操作影響的行數 </returns> 76 public virtual int Delete(object id, bool isSave = true) 77 { 78 PublicHelper.CheckArgument(id, "id"); 79 TEntity entity = EFContext.Set<TEntity>().Find(id); 80 return entity != null ? Delete(entity, isSave) : 0; 81 } 82 83 /// <summary> 84 /// 刪除實體記錄 85 /// </summary> 86 /// <param name="entity"> 實體對象 </param> 87 /// <param name="isSave"> 是否執行保存 </param> 88 /// <returns> 操作影響的行數 </returns> 89 public virtual int Delete(TEntity entity, bool isSave = true) 90 { 91 PublicHelper.CheckArgument(entity, "entity"); 92 EFContext.RegisterDeleted(entity); 93 return isSave ? EFContext.Commit() : 0; 94 } 95 96 /// <summary> 97 /// 刪除實體記錄集合 98 /// </summary> 99 /// <param name="entities"> 實體記錄集合 </param> 100 /// <param name="isSave"> 是否執行保存 </param> 101 /// <returns> 操作影響的行數 </returns> 102 public virtual int Delete(IEnumerable<TEntity> entities, bool isSave = true) 103 { 104 PublicHelper.CheckArgument(entities, "entities"); 105 EFContext.RegisterDeleted(entities); 106 return isSave ? EFContext.Commit() : 0; 107 } 108 109 /// <summary> 110 /// 刪除所有符合特定表達式的數據 111 /// </summary> 112 /// <param name="predicate"> 查詢條件謂語表達式 </param> 113 /// <param name="isSave"> 是否執行保存 </param> 114 /// <returns> 操作影響的行數 </returns> 115 public virtual int Delete(Expression<Func<TEntity, bool>> predicate, bool isSave = true) 116 { 117 PublicHelper.CheckArgument(predicate, "predicate"); 118 List<TEntity> entities = EFContext.Set<TEntity>().Where(predicate).ToList; 119 return entities.Count > 0 ? Delete(entities, isSave) : 0; 120 } 121 122 /// <summary> 123 /// 更新實體記錄 124 /// </summary> 125 /// <param name="entity"> 實體對象 </param> 126 /// <param name="isSave"> 是否執行保存 </param> 127 /// <returns> 操作影響的行數 </returns> 128 public virtual int Update(TEntity entity, bool isSave = true) 129 { 130 PublicHelper.CheckArgument(entity, "entity"); 131 EFContext.RegisterModified(entity); 132 return isSave ? EFContext.Commit() : 0; 133 } 134 135 /// <summary> 136 /// 查找指定主鍵的實體記錄 137 /// </summary> 138 /// <param name="key"> 指定主鍵 </param> 139 /// <returns> 符合編號的記錄,不存在返回null </returns> 140 public virtual TEntity GetByKey(object key) 141 { 142 PublicHelper.CheckArgument(key, "key"); 143 return EFContext.Set<TEntity>().Find(key); 144 } 145 146 #endregion 147 } 148 }
實現類中所有操作最終都是通過單元操作來提交的,關於單元操作,馬上就會講到。
UnitOfWork
引入單元操作,主要是為了給各個實體維護一個共同的DbContext上下文對象,保證所有的操作都是在共同的上下文中進行的。EF的操作提交 context.SaveChanged() 默認就是事務性的,只要保證了當前的所有實體的操作都是在一個共同的上下文中進行的,就實現了事務操作了。
在業務層中,各個實體的增刪改操作都是通過各個實體的Repository進行的,只需要提供一個提交保存的功能作為最后調用,即可保證當前的提交是事務性的。因此定義給業務層引用的單元操作接口如下:
1 namespace GMF.Component.Data 2 { 3 /// <summary> 4 /// 業務單元操作接口 5 /// </summary> 6 public interface IUnitOfWork 7 { 8 #region 屬性 9 10 /// <summary> 11 /// 獲取 當前單元操作是否已被提交 12 /// </summary> 13 bool IsCommitted { get; } 14 15 #endregion 16 17 #region 方法 18 19 /// <summary> 20 /// 提交當前單元操作的結果 21 /// </summary> 22 /// <returns></returns> 23 int Commit(); 24 25 /// <summary> 26 /// 把當前單元操作回滾成未提交狀態 27 /// </summary> 28 void Rollback(); 29 30 #endregion 31 } 32 }
在數據組件內部,數據操作最終都提交到一個與IUnitOfWork接口的實現類中進行操作,以保證各個實體的Repository與IUnitOfWork使用的是同一個DbContext上下文。定義數據單元操作接口如下:
1 namespace GMF.Component.Data 2 { 3 /// <summary> 4 /// 數據單元操作接口 5 /// </summary> 6 public interface IUnitOfWorkContext : IUnitOfWork, IDisposable 7 { 8 /// <summary> 9 /// 為指定的類型返回 System.Data.Entity.DbSet,這將允許對上下文中的給定實體執行 CRUD 操作。 10 /// </summary> 11 /// <typeparam name="TEntity"> 應為其返回一個集的實體類型。 </typeparam> 12 /// <returns> 給定實體類型的 System.Data.Entity.DbSet 實例。 </returns> 13 DbSet<TEntity> Set<TEntity>() where TEntity : Entity; 14 15 /// <summary> 16 /// 注冊一個新的對象到倉儲上下文中 17 /// </summary> 18 /// <typeparam name="TEntity"> 要注冊的類型 </typeparam> 19 /// <param name="entity"> 要注冊的對象 </param> 20 void RegisterNew<TEntity>(TEntity entity) where TEntity : Entity; 21 22 /// <summary> 23 /// 批量注冊多個新的對象到倉儲上下文中 24 /// </summary> 25 /// <typeparam name="TEntity"> 要注冊的類型 </typeparam> 26 /// <param name="entities"> 要注冊的對象集合 </param> 27 void RegisterNew<TEntity>(IEnumerable<TEntity> entities) where TEntity : Entity; 28 29 /// <summary> 30 /// 注冊一個更改的對象到倉儲上下文中 31 /// </summary> 32 /// <typeparam name="TEntity"> 要注冊的類型 </typeparam> 33 /// <param name="entity"> 要注冊的對象 </param> 34 void RegisterModified<TEntity>(TEntity entity) where TEntity : Entity; 35 36 /// <summary> 37 /// 注冊一個刪除的對象到倉儲上下文中 38 /// </summary> 39 /// <typeparam name="TEntity"> 要注冊的類型 </typeparam> 40 /// <param name="entity"> 要注冊的對象 </param> 41 void RegisterDeleted<TEntity>(TEntity entity) where TEntity : Entity; 42 43 /// <summary> 44 /// 批量注冊多個刪除的對象到倉儲上下文中 45 /// </summary> 46 /// <typeparam name="TEntity"> 要注冊的類型 </typeparam> 47 /// <param name="entities"> 要注冊的對象集合 </param> 48 void RegisterDeleted<TEntity>(IEnumerable<TEntity> entities) where TEntity : Entity; 49 } 50 }
在單元操作的實現基類中,定義一個只讀的DbContext抽象屬性,實際的DbContext上下文需要在實現類中進行重寫賦值。
1 namespace GMF.Component.Data 2 { 3 /// <summary> 4 /// 單元操作實現 5 /// </summary> 6 public abstract class UnitOfWorkContextBase : IUnitOfWorkContext 7 { 8 /// <summary> 9 /// 獲取 當前使用的數據訪問上下文對象 10 /// </summary> 11 protected abstract DbContext Context { get; } 12 13 /// <summary> 14 /// 獲取 當前單元操作是否已被提交 15 /// </summary> 16 public bool IsCommitted { get; private set; } 17 18 /// <summary> 19 /// 提交當前單元操作的結果 20 /// </summary> 21 /// <returns></returns> 22 public int Commit() 23 { 24 if (IsCommitted) 25 { 26 return 0; 27 } 28 try 29 { 30 int result = Context.SaveChanges(); 31 IsCommitted = true; 32 return result; 33 } 34 catch (DbUpdateException e) 35 { 36 if (e.InnerException != null && e.InnerException.InnerException is SqlException) 37 { 38 SqlException sqlEx = e.InnerException.InnerException as SqlException; 39 string msg = DataHelper.GetSqlExceptionMessage(sqlEx.Number); 40 throw PublicHelper.ThrowDataAccessException("提交數據更新時發生異常:" + msg, sqlEx); 41 } 42 throw; 43 } 44 } 45 46 /// <summary> 47 /// 把當前單元操作回滾成未提交狀態 48 /// </summary> 49 public void Rollback() 50 { 51 IsCommitted = false; 52 } 53 54 public void Dispose() 55 { 56 if (!IsCommitted) 57 { 58 Commit(); 59 } 60 Context.Dispose(); 61 } 62 63 /// <summary> 64 /// 為指定的類型返回 System.Data.Entity.DbSet,這將允許對上下文中的給定實體執行 CRUD 操作。 65 /// </summary> 66 /// <typeparam name="TEntity"> 應為其返回一個集的實體類型。 </typeparam> 67 /// <returns> 給定實體類型的 System.Data.Entity.DbSet 實例。 </returns> 68 public DbSet<TEntity> Set<TEntity>() where TEntity : Entity 69 { 70 return Context.Set<TEntity>(); 71 } 72 73 /// <summary> 74 /// 注冊一個新的對象到倉儲上下文中 75 /// </summary> 76 /// <typeparam name="TEntity"> 要注冊的類型 </typeparam> 77 /// <param name="entity"> 要注冊的對象 </param> 78 public void RegisterNew<TEntity>(TEntity entity) where TEntity : Entity 79 { 80 EntityState state = Context.Entry(entity).State; 81 if (state == EntityState.Detached) 82 { 83 Context.Entry(entity).State = EntityState.Added; 84 } 85 IsCommitted = false; 86 } 87 88 /// <summary> 89 /// 批量注冊多個新的對象到倉儲上下文中 90 /// </summary> 91 /// <typeparam name="TEntity"> 要注冊的類型 </typeparam> 92 /// <param name="entities"> 要注冊的對象集合 </param> 93 public void RegisterNew<TEntity>(IEnumerable<TEntity> entities) where TEntity : Entity 94 { 95 try 96 { 97 Context.Configuration.AutoDetectChangesEnabled = false; 98 foreach (TEntity entity in entities) 99 { 100 RegisterNew(entity); 101 } 102 } 103 finally 104 { 105 Context.Configuration.AutoDetectChangesEnabled = true; 106 } 107 } 108 109 /// <summary> 110 /// 注冊一個更改的對象到倉儲上下文中 111 /// </summary> 112 /// <typeparam name="TEntity"> 要注冊的類型 </typeparam> 113 /// <param name="entity"> 要注冊的對象 </param> 114 public void RegisterModified<TEntity>(TEntity entity) where TEntity : Entity 115 { 116 if (Context.Entry(entity).State == EntityState.Detached) 117 { 118 Context.Set<TEntity>().Attach(entity); 119 } 120 Context.Entry(entity).State = EntityState.Modified; 121 IsCommitted = false; 122 } 123 124 /// <summary> 125 /// 注冊一個刪除的對象到倉儲上下文中 126 /// </summary> 127 /// <typeparam name="TEntity"> 要注冊的類型 </typeparam> 128 /// <param name="entity"> 要注冊的對象 </param> 129 public void RegisterDeleted<TEntity>(TEntity entity) where TEntity : Entity 130 { 131 Context.Entry(entity).State = EntityState.Deleted; 132 IsCommitted = false; 133 } 134 135 /// <summary> 136 /// 批量注冊多個刪除的對象到倉儲上下文中 137 /// </summary> 138 /// <typeparam name="TEntity"> 要注冊的類型 </typeparam> 139 /// <param name="entities"> 要注冊的對象集合 </param> 140 public void RegisterDeleted<TEntity>(IEnumerable<TEntity> entities) where TEntity : Entity 141 { 142 try 143 { 144 Context.Configuration.AutoDetectChangesEnabled = false; 145 foreach (TEntity entity in entities) 146 { 147 RegisterDeleted(entity); 148 } 149 } 150 finally 151 { 152 Context.Configuration.AutoDetectChangesEnabled = true; 153 } 154 } 155 } 156 }
實體數據操作中,需要特別說明一下的是批量操作。EF不支持批量操作(直接執行SQL語句的方式除外),但我們可以使用多次變更一次提交的方式來進行批量的插入,刪除等操作。在進行數據的變更時,EF默認會自動的跟蹤數據的變化(AutoDetectChangesEnabled = true),當變更的數據量較大的時候,EF的跟蹤工作量就會驟增,使指定操作變得非常緩慢(這也是部分同學懷疑EF的性能問題的一個懷疑點),其實,只要在批量操作的時候把自動跟蹤關閉(AutoDetectChangesEnabled = false),即可解決緩慢的問題。如以上代碼 144 行與 152 行所示。
特別說明:本文的UnitOfWork實現方案參考了 dax.net 的 《深度剖析Byteart Retail案例:倉儲(Repository)及其上下文(Repository Context)》,在此表示感謝。
至此,我們回顧一下解決方案的結構:
與業務實體無關的數據組件 GMF.Component.Data 已經搭建完成,下面的工作將與業務實體密切相關。
業務整合應用
與業務實體相關的數據功能代碼定義在 GMF.Demo.Core.Data 項目中。
業務實體的倉儲實現
首先,實現各個實體的 Repository 倉儲操作,這里只以 用戶信息(Member)實體為例:
1 namespace GMF.Demo.Core.Data.Repositories 2 { 3 /// <summary> 4 /// 倉儲操作接口——用戶信息 5 /// </summary> 6 public interface IMemberRepository : IRepository<Member> { } 7 }
1 namespace GMF.Demo.Core.Data.Repositories.Impl 2 { 3 /// <summary> 4 /// 倉儲操作實現——用戶信息 5 /// </summary> 6 [Export(typeof(IMemberRepository))] 7 public class MemberRepository : EFRepositoryBase<Member>, IMemberRepository { } 8 }
可以發現,通用倉儲操作在數據組件中封裝好后,在實際業務中只需要編寫非常少量的代碼即可實現各個實體的倉儲操作,這就是封裝的好處。
數據上下文實現
DbContext上下文的實現,這里先使用微軟官方示例中的傳統方案:
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 } 45 }
值得注意的是,在EF中提供了很多的規則來定義 EF生成數據庫 的行為,如下圖所示:
我們可以在 DbContext 的派生類中重寫 OnModelCreating 方法來移除一些規則來達到某些需求,比如在此,我們移除 OneToManyCascadeDeleteConvention 來達到禁用數據庫的 一對多的級聯刪除 ,需要時再在做實體映射時啟用,就能防止由於誤操作而導致實體相關的數據都被刪除的情況。
當然,這里只定義的DbContext上下文的實現類,並沒有進行使用,如何來使用這個上下文呢,只需實現一個單元操作實現類即可:
1 namespace GMF.Demo.Core.Data.Context 2 { 3 /// <summary> 4 /// Demo項目單元操作類 5 /// </summary> 6 [Export(typeof (IUnitOfWork))] 7 public class DemoUnitOfWorkContext : UnitOfWorkContextBase 8 { 9 /// <summary> 10 /// 獲取或設置 當前使用的數據訪問上下文對象 11 /// </summary> 12 protected override DbContext Context 13 { 14 get { return DemoDbContext; } 15 } 16 17 /// <summary> 18 /// 獲取或設置 默認的Demo項目數據訪問上下文對象 19 /// </summary> 20 [Import(typeof (DbContext))] 21 public DemoDbContext DemoDbContext { get; set; } 22 } 23 }
創建數據庫時的數據初始化
在本系列前面的示例中,由於沒有數據訪問功能,使用了一個數據來模擬數據源,現在,我們可以通過創建數據庫時初始化一些數據,以使得在下面的操作中有數據可用。
EF創建數據庫的策略同樣有多種可選:
由各個策略類的名稱即可猜到具體的作用(可見代碼的語義化很重要),這里使用CreateDatabaseIfNotExists,即數據庫不存在時創建。
1 namespace GMF.Demo.Core.Data.Initialize 2 { 3 /// <summary> 4 /// 數據庫初始化策略 5 /// </summary> 6 public class SampleData : CreateDatabaseIfNotExists<DemoDbContext> 7 { 8 protected override void Seed(DemoDbContext context) 9 { 10 List<Member> members = new List<Member> 11 { 12 new Member { UserName = "admin", Password = "123456", Email = "admin@gmfcn.net", NickName = "管理員" }, 13 new Member { UserName = "gmfcn", Password = "123456", Email = "mf.guo@qq.com", NickName = "郭明鋒" } 14 }; 15 DbSet<Member> memberSet = context.Set<Member>(); 16 members.ForEach(m => memberSet.Add(m)); 17 context.SaveChanges(); 18 } 19 } 20 }
為了不使業務代碼與EF的耦合過大,我們規定:項目中對 EntityFramework.dll 的引用到 GMF.Demo.Core.Data項目止,再上層的代碼不依賴於EF組件。因此還需要在這定義一個執行初始化的輔助類:
1 namespace GMF.Demo.Core.Data.Initialize 2 { 3 /// <summary> 4 /// 數據庫初始化操作類 5 /// </summary> 6 public static class DatabaseInitializer 7 { 8 /// <summary> 9 /// 數據庫初始化 10 /// </summary> 11 public static void Initialize( ) 12 { 13 Database.SetInitializer(new SampleData()); 14 using (var db = new DemoDbContext()) 15 { 16 db.Database.Initialize(false); 17 } 18 } 19 } 20 }
此輔助類將在程序初始化時調用,網站項目里,在Global的Application_Start方法中調用:
業務整合,實現登錄功能
為了在業務實現中方便調用 IUnitOfWork 進行多實體的單元操作,定義一個核心與業務實現基類
1 namespace GMF.Demo.Core.Impl 2 { 3 /// <summary> 4 /// 核心業務實現基類 5 /// </summary> 6 public abstract class CoreServiceBase 7 { 8 /// <summary> 9 /// 獲取或設置 工作單元對象,用於處理同步業務的事務操作 10 /// </summary> 11 [Import] 12 protected IUnitOfWork UnitOfWork { get; set; } 13 } 14 }
在核心業務實現中添加用到的各個實體Repository倉儲操作接口對象的屬性,並更新登錄方法,使其調用倉儲操作完成登錄業務
1 namespace GMF.Demo.Core.Impl 2 { 3 /// <summary> 4 /// 賬戶模塊核心業務實現 5 /// </summary> 6 public abstract class AccountService : CoreServiceBase, IAccountContract 7 { 8 #region 屬性 9 10 #region 受保護的屬性 11 12 /// <summary> 13 /// 獲取或設置 用戶信息數據訪問對象 14 /// </summary> 15 [Import] 16 protected IMemberRepository MemberRepository { get; set; } 17 18 /// <summary> 19 /// 獲取或設置 登錄記錄信息數據訪問對象 20 /// </summary> 21 [Import] 22 protected ILoginLogRepository LoginLogRepository { get; set; } 23 24 #endregion 25 26 #endregion 27 28 /// <summary> 29 /// 用戶登錄 30 /// </summary> 31 /// <param name="loginInfo">登錄信息</param> 32 /// <returns>業務操作結果</returns> 33 public virtual OperationResult Login(LoginInfo loginInfo) 34 { 35 PublicHelper.CheckArgument(loginInfo, "loginInfo"); 36 Member member = MemberRepository.Entities.SingleOrDefault(m => m.UserName == loginInfo.Access || m.Email == loginInfo.Access); 37 if (member == null) 38 { 39 return new OperationResult(OperationResultType.QueryNull, "指定賬號的用戶不存在。"); 40 } 41 if (member.Password != loginInfo.Password) 42 { 43 return new OperationResult(OperationResultType.Warning, "登錄密碼不正確。"); 44 } 45 LoginLog loginLog = new LoginLog { IpAddress = loginInfo.IpAddress, Member = member }; 46 LoginLogRepository.Insert(loginLog); 47 return new OperationResult(OperationResultType.Success, "登錄成功。", member); 48 } 49 } 50 }
在此需要特別說明的是,為了保持項目結構的整潔與規范,實體數據操作的Repository與UnitOfWork只能被業務層(核心層及其派生層次)調用,而不能被業務層的上層調用,比如MVC項目中,不能被控制器調用,防止開發人員不遵守規范,在控制器中進行業務實現導致項目結構的混亂。因此給實體的Repository接口屬性與業務基類中的UnitOfWork屬性的可訪問性都要設置為 protected,而不能為 public。
在MVC的Web.Config中添加一個數據連接配置:
<connectionStrings> <add name="default" connectionString="Data Source=.; Integrated Security=True; Initial Catalog=DemoContext; Pooling=True; MultipleActiveResultSets=True;" providerName="System.Data.SqlClient" /> </connectionStrings>
至此,我們的項目結構變成了如下圖所示:
運行項目,嘗試登錄並成功,查看登錄記錄表,我們可以發現新增了一條登錄記錄:
源碼下載
為了讓大家能第一時間獲取到本架構的最新代碼,也為了方便我對代碼的管理,本系列的源碼已加入微軟的開源項目網站 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):數據更新
- 未完待續。。。