在一個大型系統中,應該允許訪問多個數據庫,甚至是多個異構的數據庫。例如表單模塊使用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開發框架目錄