上篇文章中介紹了如何使用ef進行動態類型的管理,比如我們定義了ShopDbContext並且注冊了動態模型信息,下面的代碼實現了動態信息的增加:
Type modelType = IRuntimeModelProvider.GetType(1);//獲取id=1的模型類型 object obj = Activator.CreateInstance(modelType);//創建實體 entity = obj as DynamicEntity;//類型轉換,目的是進行賦值 entity["Id"]=1; entity["Name"]="名稱"; ShopDbContext.Add(entity); ShopDbContext.SaveChanges();
上面的方式只能在程序運行前,先把模型配置好,然后再啟動程序,無法做到程序運行期間動態改變模型的信息,現在我們來改進下前面的功能:
1,實現在線的模型結構配置管理
2,模型配置變化后動態生成數據庫表
3,運行時注冊模型信息到DbContext
一、實現在線的模型結構配置管理
上一篇文章內容中,我們是把模型信息保存了配置文件中,程序啟動時加載配置文件並解析,完成模型的編譯。現在我們要把配置信息放到數據庫中,用一個數據表存儲配置信息。首先改造下前面提到的RuntimeModelMeta類,代碼如下:
public class RuntimeModelMeta
{
public int ModelId { get; set; }
public string ModelName { get; set; }//模型名稱
public string ClassName { get; set; }//類名稱
public string Properties{get;set;}//屬性集合json序列化結果
public class ModelPropertyMeta
{
public string Name { get; set; }//對應的中文名稱
public string PropertyName { get; set; } //類屬性名稱
public int Length { get; set; }//數據長度,主要用於string類型
public bool IsRequired { get; set; }//是否必須輸入,用於數據驗證
public string ValueType { get; set; }//數據類型,可以是字符串,日期,bool等
}
}
就是把 public ModelPropertyMeta[] ModelProperties { get; set; }屬性改成了String類型,然后我們直接定義個用於模型配置管理的DbContext,代碼如下:
public class ModelDbContext : DbContext
{
public ModelDbContext(DbContextOptions<ShopDbContext> options) :base(options)
{
}
public DbSet<RuntimeModelMeta> Metas { get; set; }
}
有了這個DbContext,操作RuntimeModelMeta就比較簡單了。另外為了方便模型屬性數據的操作,增加一些擴展方法,如下:
public static class RuntimeModelMetaExtensions
{
//反序列化獲得集合
public static RuntimeModelMeta.ModelPropertyMeta[] GetProperties(this RuntimeModelMeta meta)
{
if (string.IsNullOrEmpty(meta.Properties))
{
return null;
}
return JsonConvert.DeserializeObject<RuntimeModelMeta.ModelPropertyMeta[]>(meta.Properties);
}
//把集合序列化成字符串,用於保存
public static void SetProperties(this RuntimeModelMeta meta, RuntimeModelMeta.ModelPropertyMeta[] properties)
{
meta.Properties = JsonConvert.SerializeObject(properties);
}
}
操作很簡單,但是問題是模型信息變化時如何告訴DbContext,我們到第三部分的時候,再詳細說,這里只需要完成配置信息管理即可。
二、模型配置變化后動態生成數據庫表
我們這里直接采用SQL語句來操作數據庫,下面是簡單的封裝類:
public static class ModelDbContextExtensions
{
//添加字段
public static void AddField(this ModelDbContext context, RuntimeModelMeta model, RuntimeModelMeta.ModelPropertyMeta property)
{
using (DbConnection conn = context.Database.GetDbConnection())
{
if (conn.State != System.Data.ConnectionState.Open)
{
conn.Open();
}
DbCommand addFieldCmd = conn.CreateCommand();
addFieldCmd.CommandText = $"alert table {model.ClassName} add {property.PropertyName} ";
switch (property.ValueType)
{
case "int":
addFieldCmd.CommandText += "int";
break;
case "datetime":
addFieldCmd.CommandText += "datetime";
break;
case "bool":
addFieldCmd.CommandText += "bit";
break;
default:
addFieldCmd.CommandText += "nvarchar(max)";
break;
}
addFieldCmd.ExecuteNonQuery();
}
}
//刪除字段
public static void RemoveField(this ModelDbContext context, RuntimeModelMeta model,string property)
{
using (DbConnection conn = context.Database.GetDbConnection())
{
if (conn.State != System.Data.ConnectionState.Open)
{
conn.Open();
}
DbCommand removeFieldCmd = conn.CreateCommand();
removeFieldCmd.CommandText = $"alert table {model.ClassName} DROP COLUMN {property}";
removeFieldCmd.ExecuteNonQuery();
}
}
//創建模型表
public static void CreateModel(this ModelDbContext context,RuntimeModelMeta model)
{
using (DbConnection conn = context.Database.GetDbConnection())
{
if (conn.State != System.Data.ConnectionState.Open)
{
conn.Open();
}
DbCommand createTableCmd = conn.CreateCommand();
createTableCmd.CommandText = $"create table {model.ClassName}";
createTableCmd.CommandText += "{id int identity(1,1)";
foreach (var p in model.GetProperties())
{
createTableCmd.CommandText += $",{p.PropertyName} ";
switch (p.ValueType)
{
case "int":
createTableCmd.CommandText += "int";
break;
case "datetime":
createTableCmd.CommandText += "datetime";
break;
case "bool":
createTableCmd.CommandText += "bit";
break;
default:
createTableCmd.CommandText += "nvarchar(max)";
break;
}
}
createTableCmd.CommandText += "}";
createTableCmd.ExecuteNonQuery();
}
}
}
在模型配置信息發生變化的時候,通過上面的封裝類直接操作數據庫完成數據表結構的變化,當然這里提供的方法很少,大家可以再擴展,比如修改字段類型,刪除表等操作。
三、運行是注冊模型信息到DbContext
我們在前面通過重寫OnModelCreating方法,注冊模型到DbContext,但是這個方法只會被執行一次,在運行時期間如果模型信息發生了變化,DbContext是無法同步的,所以這個方法就行不通了。DbContext還提供了另外一個方法叫void OnConfiguring(DbContextOptionsBuilder optionsBuilder),這個方法在每次實例化DbContext的時候都會被調用,那我們如何利用這個方法完成模型信息的注冊。這個方法包含一個參數DbContextOptionsBuilder,這個類型提供了一個方法,可以讓我們注冊模型,方法如下:
DbContextOptionsBuilder UseModel(IModel model)
IModel就是模型信息維護的類,自然我們會想到,自己去創建一個IModel,然后通過上面的方法完成注冊。那現在的問題是IModel如何得到?我們重寫OnModelCreating方法的時候發現有一個ModelBuilder參數,從這個類型的名字我們可能立馬想到,能不能通過它得到我們所需要的信息?通過查看EntityFramework.Core源碼,發現它就是我們要找的東西。首先我們看下ModelBuilder的構造方法:
ModelBuilder(ConventionSet conventions)
它需要一個ConventionSet,直接翻譯過來就是約束的集合(如果有錯誤歡迎大家拍磚),那如何得到這樣的對象?通過查看ef源碼,框架里通過IConventionSetBuilder創建的ConventionSet,所以我們也用它,我們先在DbContext中通過依賴注入的方式引用ICoreConventionSetBuilder,代碼如下:
public class ShopDbContext:DbContext
{
private readonly ICoreConventionSetBuilder _builder;
public ShopDbContext(DbContextOptions<ShopDbContext> options, ICoreConventionSetBuilder builder) :base(options)
{
_builder = builder;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
//完成ModelBuilder實例化
var modelBuilder = new ModelBuilder(_builder.CreateConventionSet());
}
}
有了ModelBuilder后,我們可以通過ModelBuilder.Model獲取一個IMutableModel,通過這個對象可以完成模型信息注冊,代碼如下:
public class ShopDbContext:DbContext
{
private readonly ICoreConventionSetBuilder _builder;
private readonly IRuntimeModelProvider _modelProvider;
public ShopDbContext(DbContextOptions<ShopDbContext> options, ICoreConventionSetBuilder builder, IRuntimeModelProvider modelProvider) :base(options)
{
_builder = builder;
_modelProvider = modelProvider;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var modelBuilder = new ModelBuilder(_builder.CreateConventionSet());
//_modelProvider就是上一篇文章提到的,但是實現上需要修改下,因為現在的模型信息是存到數據庫中了
Type[] runtimeModels = _modelProvider.GetTypes();
foreach (var item in runtimeModels)
{
//添加模型信息
modelBuilder.Model.AddEntityType(item);
}
//完成注冊
optionsBuilder.UseModel(modelBuilder.Model);
base.OnConfiguring(optionsBuilder);
}
}
這樣我們就完成了注冊動態模型信息的功能。如果生成的表名稱需要個性化,我們可以通過下面的方式修改:
modelBuilder.Model.AddEntityType(item).SqlServer().TableName=""
由於我們在上面用到了ICoreConventionSetBuilder,所以我們需要在Startup中需要調用AddEntityFramework進行服務注冊,代碼如下:
public void ConfigureServices(IServiceCollection services)
{
。。。。。。
services.AddEntityFramework().AddDbContext<ShopDbContext>(option => {
option.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"), sql => {
sql.UseRowNumberForPaging();
sql.MaxBatchSize(50);
});
});
。。。。。。
}
我們上面提到,OnConfiguring方法在每次DbContext實例化的時候都會調用,那我們的模型信息每次都要build一下,也不是很好,ef是采用了緩存的辦法,那我們自然也可以采用。最終ShopDbContext的完整代碼如下:
public class ShopDbContext:DbContext
{
private readonly ICoreConventionSetBuilder _builder;
private readonly IRuntimeModelProvider _modelProvider;
private readonly IMemoryCache _cache;
private static string DynamicCacheKey = "DynamicModel";
public ShopDbContext(DbContextOptions<ShopDbContext> options, ICoreConventionSetBuilder builder, IRuntimeModelProvider modelProvider, IMemoryCache cache) :base(options)
{
_builder = builder;
_modelProvider = modelProvider;
_cache = cache;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
//直接從緩存讀取model,如果不存在再build
IMutableModel model = _cache.GetOrCreate(DynamicCacheKey, entry => {
var modelBuilder = new ModelBuilder(_builder.CreateConventionSet());
Type[] runtimeModels = _modelProvider.GetTypes();
foreach (var item in runtimeModels)
{
modelBuilder.Model.AddEntityType(item).SqlServer().TableName = "";
}
_cache.Set(DynamicCacheKey, modelBuilder.Model);
return modelBuilder.Model;
});
optionsBuilder.UseModel(model);
base.OnConfiguring(optionsBuilder);
}
當模型配置發生變化時,把緩存清理一下,這樣下次再訪問的時候,就能夠按照新的配置重新Build。
Ok了,所有的工作做完后,就完全可以實現運行時動態模型配置的功能了。
后面的文章會繼續介紹動態模型與動態表單的實現方法。
