在一個大型系統中,應該允許訪問多個數據庫,甚至是多個異構的數據庫。例如表單模塊使用mysql,數據倉庫模塊使用oracle等等。按照這個目標,數據的配置信息:
1 "Database": { 2 "ConnectionStrings": [ 3 { 4 "Name": "MicroStrutLibrary", 5 "ConnectionString": "Data Source=XXXX;Initial Catalog=XXXX;User Id=OperUser;Password=OperUser;MultipleActiveResultSets=true;Persist Security Info=true", 6 "ProviderName": "System.Data.SqlClient" 7 }, 8 { 9 "Name": "CMS", 10 "ConnectionString": "Data Source=XXXX;Initial Catalog=XXXX;User Id=OperUser;Password=OperUser;MultipleActiveResultSets=true;Persist Security Info=true", 11 "ProviderName": "System.Data.SqlClient" 12 } 13 ], 14 "Providers": [ 15 { 16 "Name": "System.Data.SqlClient", 17 "Type": "MicroStrutLibrary.Infrastructure.Core.Data.Entity.SqlServerDbContextOptionsBuilderProvider, MicroStrutLibrary.Infrastructure.Core.Data.Entity" 18 } 19 ] 20 }
每個數據庫都有一個Name(名稱,以後都用這個名稱訪問)、ConnectionString(數據庫鏈接串)、ProviderName(提供程序名)。對於ProviderName(提供程序名)在Providers中描述了對應的實現類描述TypeDescription。從上面我們可以看出,我們可以設置多個數據庫,不同的數據庫可以設置不同的Provider,也就是不同的數據庫類型,從而實現了多個異構數據庫的存取操作。
對於數據庫配置信息類DataConfigInfo,實現ConfigInfo,應該很簡單,就不贅述了,可以參見可換源的配置。
框架中,每個模塊(例如公共模塊、數據倉庫模塊、表單模塊、CMS模塊、授權認證模塊等)都對應於一個數據庫上下文DbContext。這個數據庫上下文指向一個數據庫,也就是要對應上數據配置中的Name屬性。我們的做法是新建一個DbNameAttribute,放在DbContext上,以確定具體數據庫的Name。
1 /// <summary> 2 /// 指定數據庫名稱特性 3 /// </summary> 4 [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] 5 public class DbNameAttribute : Attribute 6 { 7 /// <summary> 8 /// 數據庫名稱 9 /// </summary> 10 public string Name { get; set; } 11 12 /// <summary> 13 /// 構造函數 14 /// </summary> 15 /// <param name="name"></param> 16 public DbNameAttribute(string name) 17 { 18 this.Name = name; 19 } 20 }
DBNameAttribute中的Name應該設置的就是配置中的Name,從而確保DbContext對應的是哪個數據庫。
例如公共模塊部分的DbContext寫法如下:
1 [DbName("MicroStrutLibrary")] 2 public class CommonDbContext : EntityDbContext 3 { 4 public CommonDbContext(DbContextOptions<CommonDbContext> options) : base(options) 5 { 6 } 7 8 public DbSet<AccessoryInfo> Accessories { get; set; } 9 10 public DbSet<SystemParameterInfo> SystemParameters { get; set; } 11 public DbSet<SystemParameterDetailInfo> SystemParameterDetails { get; set; } 12 …… 13 }
大家會注意到CommonDbContext繼承EntityDbContext,他是框架的數據庫上下文抽象基類,繼承DbContext。這個抽象類的重寫OnModelCreating方法,找出當前具體實現EntityDbContext類(例如CommonDbContext)的所有DbSet屬性,形成該EntityDbContext用到的所有DbSet泛型的參數類,就是AccessoryInfo、SystemParameterInfo等類。然後找出所有繼承ORMapping關系映射基類EntityTypeConfiguration的子類,創建映射綁定關系。
抽象的關系映射基類EntityTypeConfiguration代碼如下:
1 public abstract class EntityTypeConfiguration<T> where T: class 2 { 3 public void Bind(ModelBuilder modelBuilder) 4 { 5 InnerBind(modelBuilder.Entity<T>()); 6 } 7 8 protected abstract void InnerBind(EntityTypeBuilder<T> builder); 9 }
例如系統參數的ORMapping類就可以寫成:
1 /// <summary> 2 /// 系統參數 映射信息 3 /// </summary> 4 public class SystemParameterMapper : EntityTypeConfiguration<SystemParameterInfo> 5 { 6 protected override void InnerBind(EntityTypeBuilder<SystemParameterInfo> builder) 7 { 8 builder.ToTable("SYSTEM_PARAMETER_INFO"); 9 10 builder.Property(p => p.AppCode).HasColumnName("APP_CODE").IsRequired(); 11 builder.Property(p => p.SystemParaCode).HasColumnName("SYSTEM_PARA_CODE").IsRequired(); 12 builder.Property(p => p.SystemParaName).HasColumnName("SYSTEM_PARA_NAME").IsRequired(); 13 builder.Property(p => p.SortOrder).HasColumnName("SORT_ORDER").IsRequired(); 14 builder.Property(p => p.Remark).HasColumnName("REMARK"); 15 16 builder.HasKey(p => new { p.AppCode, p.SystemParaCode }); 17 18 builder.HasMany(p => p.DetailList).WithOne().HasForeignKey(f => new { f.AppCode, f.SystemParaCode }).OnDelete(DeleteBehavior.Cascade); 19 } 20 }
前面講了數據庫的配置說明,DbContext的具體實現和ORMapping的實現。但是在程序中如何嵌入這些內容,如何將DbContext與配置關聯呢?尤其是數據庫的Provider是寫在配置中的,不能再在Startup中寫死services.AddDbContext<BloggingContext>(options => options.UseSqlServer(connection))吧?這種寫法,背離了我們使用配置方式的初衷了。
為了解決這個問題,我們寫一個startup的擴展方法,將配置和DbContext等關聯起來:
1 public static DbContextOptionsBuilder UseDb<TContext>(this DbContextOptionsBuilder optionsBuilder, IServiceProvider serviceProvider) where TContext : DbContext 2 { 3 DataConfigInfo config = serviceProvider.GetService<IOptions<DataConfigInfo>>().Value; 4 5 DbNameAttribute attribute = typeof(TContext).GetTypeInfo().GetCustomAttribute<DbNameAttribute>(false); 6 7 ConnectionStringSettingInfo connectionStringSetting = config.ConnectionStrings.SingleOrDefault(o => o.Name == attribute.Name); 8 ProviderSettingInfo providerSetting = config.Providers.SingleOrDefault(o => o.Name == connectionStringSetting.ProviderName); 9 10 Type providerType = Type.GetType(providerSetting.Type); 11 12 DbContextOptionsBuilderProvider providerInstance = Activator.CreateInstance(providerType) as DbContextOptionsBuilderProvider; 13 14 providerInstance.ConnectionStringSetting = connectionStringSetting; 15 providerInstance.ProviderSetting = providerSetting; 16 17 providerInstance.Build(optionsBuilder); 18 19 EntityDbContext.DbContexts.TryAdd(connectionStringSetting.Name, typeof(TContext)); 20 21 return optionsBuilder; 22 }
這個方法的主要作用是:TContext泛型參數是EntityDbContext的實現類,例如CommonDbContext。以上面介紹的數據庫配置信息和CommonDbContext為例說明:
1、獲取當前CommonDbContext類的DbNameAttribute,也就是MicroStrutLibrary。
2、然後獲取MicroStrutLibrary數據庫連接串信息
"Name": "MicroStrutLibrary",
"ConnectionString": "Data Source=XXXX;Initial Catalog=XXXX;User Id=OperUser;Password=OperUser;MultipleActiveResultSets=true;Persist Security Info=true",
"ProviderName": "System.Data.SqlClient"
3、再找出對應的DbProvider信息
"Name": "System.Data.SqlClient",
"Type": "MicroStrutLibrary.Infrastructure.Core.Data.Entity.SqlServerDbContextOptionsBuilderProvider, MicroStrutLibrary.Infrastructure.Core.Data.Entity"
4、根據DbProvider信息創建DbContextOptionsBuilderProvider類的實例,並設置屬性,執行實例的Build方法。具體這個類在下面講解。
5、將當前TContext追加到EntityDbContext基類的靜態屬性DbContexts字典中。這個字典主要是存放數據庫的Name和TContext的對應關系。目前暫且用不到(在通用數據查詢功能中用的,以後會介紹)。
重點來了,DbContextOptionsBuilderProvider類就是針對每種類型數據庫SQL Server、MySql、Oracle等的提供程序。Build方法的參數是各種數據庫類型的OptionsBuilder,這個OptionsBuilder在.net core類庫中,只需要在他們的基礎上封裝一下即可:
1 public abstract class DbContextOptionsBuilderProvider 2 { 3 public ProviderSettingInfo ProviderSetting { get; set; } 4 5 public ConnectionStringSettingInfo ConnectionStringSetting { get; set; } 6 7 public abstract void Build(DbContextOptionsBuilder optionsBuilder); 8 } 9 10 public class SqlServerDbContextOptionsBuilderProvider : DbContextOptionsBuilderProvider 11 { 12 public override void Build(DbContextOptionsBuilder optionsBuilder) 13 { 14 //SQLServer 2008 R2以以下版本 15 optionsBuilder.UseSqlServer(this.ConnectionStringSetting.ConnectionString, ob => ob.UseRowNumberForPaging()); 16 //SQLSever 2012以上版本 17 //optionsBuilder.UseSqlServer(this.ConnectionStringSetting.ConnectionString); 18 } 19 }
上面就是一個SQLServer的具體實現,不過有個說明的是SQLServer 2008R2及以下版本沒有offset fetch next語句,只能使用row_number() over方式,因此當數據庫是SQLServer 2008R2及以下版本使用optionsBuilder.UseSqlServer(this.ConnectionStringSetting.ConnectionString, ob => ob.UseRowNumberForPaging()),而SQLServer 2012以上版本使用optionsBuilder.UseSqlServer(this.ConnectionStringSetting.ConnectionString)。
還有一點說明的是SQLServer數據庫使用datetime2,而不是datetime,否則轉換會報錯。
面向雲的.net core開發框架目錄