開篇
經過3年的努力與付出,終於實現了我的職業規劃——系統架構師。先就職於某女性內衣電商ERP團隊,由於一些原因,轉至某捕魚遊戲公司,找份想要的工作真的不容易。呵呵,感慨一下。。
特此記錄一下作為架構師的第一個作品,與大家分享交流。。
感謝Google,感謝Codeplex,感謝廣州知微團隊中曾經協助我的同事們
需求背景
在現有很多系統中,常常有多個資料庫,多種資料庫的使用。因此在程式中,我們需要一個這樣的資料訪問組件:
- 支援多個資料庫;
- 支援多種資料庫:SQL SERVER / MYSQL / ORACLE;
- 提供統一持久介面;
- 配置及初始化靈活;
- 業務對象與資料庫物件之間的映射配置方便;
為啥是EF4.1
拿到這些需求,我的第一反應是利用現有的ORM:NH或EF進行封裝改造,自己造輪子雖然能學到不少東西,但可行性與風險相對較高。
經過一些思考與測試,我選擇了EF4.1作為封裝改造的目標,有以下幾點原因:
- 微軟系愛微軟 :)
- EF4.1支援POCO,映射配置類,還有超強工具EF Power Tool
- 對比了EF4.1與NH3,兩者的功能差不多,基於現有團隊的情況,EF4.1相對學習成本較低;
- LINQ TO Entities 相對 LINQ TO NH來說,實現得更加全面;
多個資料庫IDbContextStorage
確定使用EF4.1進行封裝後,碰到了第一個問題:怎樣支援多個資料庫呢?
我們使用EF4.1時一般使用以下代碼:
public class TestDbContext :DbContext{ public DbSet<UserInfo> UserInfoes {get; set;} public DbSet<UserAddress> UserAddresses {get;set;} ...}
using(var ctx = new TestDbContext()){ // doSomething}
這下我就鬱悶了,如果要支援多個資料庫,可能是TestDb1Context,TestDb2Context的類寫在項目中,這樣就有以下幾點壞處了:
- 無法提供統一的資料提供者;
- 有幾個資料庫就會有幾個DbContext,而且直接將DbContext暴露給了上層;
- 管理麻煩;
這時突然想起來在知微時的一個開源項目SharpArch,看了看它裡面NH的一些寫法,好眼熟呢,這不正是我想要的麼。多個資料庫的話,我也可以通過一個IDbContextStorage來進行管理。 代碼如下:
/// <summary>/// DbContext倉庫介面
/// </summary>public interface IDbContextStorage{ /// <summary> /// 根據key擷取DbContext /// </summary> /// <param name="key">key</param> /// <returns>DbContext</returns> DbContext GetByKey(string key); /// <summary> /// 根據Key將DbContext寫入Storage /// </summary> /// <param name="key">Key</param> /// <param name="dbContext">DbContext</param> void SetByKey(string key, DbContext dbContext); /// <summary> /// 擷取所有DDbContext /// </summary> /// <returns>所有DDbContext</returns> IEnumerable<DbContext> GetAllDbContexts();}
接下來,我們可以實現IDbContextStorage,用於WEB應用程式的WebDbContextStorage:
/// <summary>/// 用於WEB應用程式的DbContext/// </summary>public class WebDbContextStorage :IDbContextStorage{ // DbContext容器 private Dictionary<string, DbContext> _storage = new Dictionary<string, DbContext>(); /// <summary> /// 建構函式 /// 用於註冊HttpApplication.EndRequest事件,關閉資料庫連接 /// </summary> /// <param name="app"></param> public WebDbContextStorage(HttpApplication app) { app.EndRequest += (sender, args) => { DbContextManager.CloseAllDbContexts();
}; } #region Implementation of IDbContextStorage /// <summary> /// 根據key擷取DbContext /// </summary> /// <param name="key">key</param> /// <returns>DbContext</returns> public DbContext GetByKey(string key) { DbContext context; return !_storage.TryGetValue(key, out context) ? null : context; } /// <summary> /// 根據Key將DbContext寫入Storage /// </summary> /// <param name="key">Key</param> /// <param name="dbContext">DbContext</param> public void SetByKey(string key, DbContext dbContext) { _storage.Add(key, dbContext); } /// <summary> /// 擷取所有DDbContext /// </summary> /// <returns>所有DDbContext</returns> public IEnumerable<DbContext> GetAllDbContexts() { return _storage.Values; } #endregion}
此處存在的疑問點:由於EF自己進行資料庫連接的管理,由於HttpApplication.EndRequest事件中關閉資料庫連接此種方法效果如何還沒有進行過測試,不過不礙事。
動態組建DbContext:IDbContextBuilder
到了這一步,我得怎樣去除項目代碼中的多個DbContext,即動態地去組建DbContext呢。。
OK,Google,Codeplex,Google code輪番上陣,查呀查呀。目的只有一個,弄清楚DbContext是怎樣組建的,終於在某段代碼中發現了,原來有DbModelBuilder這個東東,爽呐,動手開搞
其原理是,DbContext分為2個步驟組建:
- 載入映射設定檔
- 根據連接字串及資料庫Provider準備資料庫連接
/// <summary>/// DbContext組裝介面
/// </summary>public interface IDbContextBuilder<T> where T:DbContext{ /// <summary> /// 組裝DbContext /// </summary> /// <returns>DbContext</returns> T BuildDbContext();}
public class DbContextBuilder<T> : DbModelBuilder, IDbContextBuilder<T> where T : DbContext{ // 資料庫用戶端ProviderFactory private readonly DbProviderFactory _factory; // 連接字串配置 private readonly ConnectionStringSettings _cnStringSettings; // 如果資料庫不存在是否建立 private readonly bool _recreateDatabaseIfExists; // 是否LazyLoad private readonly bool _lazyLoadingEnabled; public DbContextBuilder(string connectionStringName, string mappingAssemblyPath, string mappingNamespace, bool recreateDatabaseIfExists = false, bool lazyLoadingEnabled = true) { this.Conventions.Remove<IncludeMetadataConvention>(); _cnStringSettings = ConfigurationManager.ConnectionStrings[connectionStringName]; _factory = DbProviderFactories.GetFactory(_cnStringSettings.ProviderName); _recreateDatabaseIfExists = recreateDatabaseIfExists; _lazyLoadingEnabled = lazyLoadingEnabled; AddConfigurations(mappingAssemblyPath, mappingNamespace); } private void AddConfigurations(string mappingAssemblyPath, string mappingNamespace) { if (string.IsNullOrEmpty(mappingAssemblyPath)) throw new ArgumentNullException("mappingAssemblyPath"); if (string.IsNullOrEmpty(mappingNamespace)) throw new ArgumentNullException("mappingNamespace"); var hashMapping = false;
// 關鍵代碼 var asm = Assembly.LoadFrom(GetAssemblyPath(mappingAssemblyPath)); foreach (var type in asm.GetTypes().Where(c => !c.IsAbstract && c.BaseType != null && c.BaseType.IsGenericType && c.BaseType.GetGenericTypeDefinition() == typeof(EntityTypeConfiguration<>)) ) { hashMapping = true; dynamic configurationInstance = Activator.CreateInstance(type); Configurations.Add(configurationInstance); } if(!hashMapping) throw new ApplicationException("DbContext組裝時,沒有發現映射配置類"); } private static string GetAssemblyPath(string assemblyName) { return (assemblyName.IndexOf(".dll") == -1) ? assemblyName.Trim() + ".dll" : assemblyName.Trim(); } #region Implementation of IDbContextBuilder<T> /// <summary> /// 組é裝°DbContext /// </summary> /// <returns>DbContext</returns> public T BuildDbContext() {
// 關鍵代碼 var cn = _factory.CreateConnection(); cn.ConnectionString = _cnStringSettings.ConnectionString; var dbModel = this.Build(cn); var ctx = dbModel.Compile().CreateObjectContext<ObjectContext>(cn); ctx.ContextOptions.LazyLoadingEnabled = this._lazyLoadingEnabled; if (!ctx.DatabaseExists()) { ctx.CreateDatabase(); } else if (_recreateDatabaseIfExists) { ctx.DeleteDatabase(); ctx.CreateDatabase(); } return (T)new DbContext(ctx, false); } #endregion}
上述代碼中,最關鍵的地方是:
- 將DbContext相關的映射配置類載入到DbModelBuilder的配置列表中,這裡我提供給了外部2個參數mappingAssemblyPath-映射配置類DLL的路徑,mappingNamespace-該DbContext的映射配置類所在的命名空間
- 準備資料庫連接,根據DbProviderFactory,ConnectionString得到資料庫類型與資料庫連接字串,另外還有2個參數recreateDatabaseIfExists-資料庫不存在時是否自動建立,lazyLoadingEnabled-是否消極式載入。
後續
由於時間的原因,後續的介紹只能放到後續了。。
大致有以下幾塊:
- DbContexts在應用程式開始時的初始化裝配器;
- 統一持久介面IRepository<T>及其實現EFRepository<T>,這個網上有很多例子了;
- UnitOfWork的實現:事件,批量提交,資源回收;
- PagedList<T>的分頁機制;
- 多條件動態組合查詢與排序;
效能最佳化
- 代碼實戰測試及SQL效能跟蹤;
- 跨庫關聯查詢,大表關聯查詢所帶來效能問題的解決;
- 利用Sql Server Profiler 進行效能調優;
希望園子裡的朋友們能提出寶貴建議,一起提高!