前言
最近還在和 npgsql
與 EF Core
鬥爭,由於 EF Core
暫時還不支援 AOT,因此在 AOT 應用程式中使用 EF Core 時,會提示問題:
聽這個意思,似乎使用 Compiled Model
可以解決問題,於是就又研究了一下 EF Core 的這個功能。
在 EF Core 中,模型根據實體類和配置構建,預設情況下,每次建立一個新的 DbContext
例項時,EF Core 都會構建模型。對於需要頻繁建立 DbContext
例項的應用程式,這可能會導致效能問題。
Entity Framework Core(EF Core)的預編譯模型(Compiled Model)對應提供了一種最佳化,在 EF Core 6 preview 5 中首次增加了這個功能,可以讓設計人員預編譯模型,避免在後續執行查詢時動態生成模型。
預編譯模型的優勢
- 效能提升:透過預編譯模型,可以減少應用程式啟動時的開銷,特別是對於大型模型。
此處的啟動時間,指
DbContext
的首次啟動時間,由於延遲查詢的機制,一般 DbContext 並不會在新建物件時完成啟動,而是在首次執行插入或者查詢時完成這個過程。
參考下圖(來自參考 1):
顯然,隨著模型的規模增大,啟動時間線性增長;但是使用預編譯模型後,啟動時間和模型大小基本無關,保持在一個極低的水平。
- 一致性:確保每個
DbContext
例項使用相同的模型配置。
使用預編譯模型
- 生成編譯模型:
使用 EF Core 命令列工具,命令:
dotnet ef dbcontext optimize
這將生成 DbContext
的預編譯模型。我只有一個 POCO 類,生成了 3 個檔案,類名稱就是檔名稱。
[DbContext(typeof(DataContext))]
public partial class DataContextModel : RuntimeModel
{
static DataContextModel()
{
var model = new DataContextModel();
model.Initialize();
model.Customize();
_instance = model;
}
private static DataContextModel _instance;
public static IModel Instance => _instance;
partial void Initialize();
partial void Customize();
}
public partial class DataContextModel
{
partial void Initialize()
{
var deviceDatum = DeviceDatumEntityType.Create(this);
DeviceDatumEntityType.CreateAnnotations(deviceDatum);
AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
AddAnnotation("ProductVersion", "8.0.0-rc.2.23480.1");
AddAnnotation("Relational:MaxIdentifierLength", 63);
AddRuntimeAnnotation("Relational:RelationalModel", CreateRelationalModel());
}
private IRelationalModel CreateRelationalModel()
{
// 這裡面非常多描述型別的程式碼,節約篇幅我就不寫全了。
var relationalModel = new RelationalModel(this);
var deviceDatum = FindEntityType("AspireSample.DeviceDatum")!;
var defaultTableMappings = new List<TableMappingBase<ColumnMappingBase>>();
deviceDatum.SetRuntimeAnnotation("Relational:DefaultMappings", defaultTableMappings);
....
return relationalModel.MakeReadOnly();
}
}
internal partial class DeviceDatumEntityType
{
public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType baseEntityType = null)
{
var runtimeEntityType = model.AddEntityType(
"AspireSample.DeviceDatum",
typeof(DeviceDatum),
baseEntityType);
var id = runtimeEntityType.AddProperty(
"Id",
typeof(string),
propertyInfo: typeof(DeviceDatum).GetProperty("Id", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(DeviceDatum).GetField("<Id>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
afterSaveBehavior: PropertySaveBehavior.Throw);
id.TypeMapping = StringTypeMapping.Default.Clone(
comparer: new ValueComparer<string>(
(string v1, string v2) => v1 == v2,
(string v) => v.GetHashCode(),
(string v) => v),
keyComparer: new ValueComparer<string>(
(string v1, string v2) => v1 == v2,
(string v) => v.GetHashCode(),
(string v) => v),
providerValueComparer: new ValueComparer<string>(
(string v1, string v2) => v1 == v2,
(string v) => v.GetHashCode(),
(string v) => v),
mappingInfo: new RelationalTypeMappingInfo(
dbType: System.Data.DbType.String));
......
var key = runtimeEntityType.AddKey(
new[] { id });
runtimeEntityType.SetPrimaryKey(key);
return runtimeEntityType;
}
public static void CreateAnnotations(RuntimeEntityType runtimeEntityType)
{
runtimeEntityType.AddAnnotation("Relational:FunctionName", null);
runtimeEntityType.AddAnnotation("Relational:Schema", null);
runtimeEntityType.AddAnnotation("Relational:SqlQuery", null);
runtimeEntityType.AddAnnotation("Relational:TableName", "devicedata");
runtimeEntityType.AddAnnotation("Relational:ViewName", null);
runtimeEntityType.AddAnnotation("Relational:ViewSchema", null);
Customize(runtimeEntityType);
}
static partial void Customize(RuntimeEntityType runtimeEntityType);
}
可以看到,最佳化工具幫我們生成了非常多的程式碼,尤其是與型別描述相關的程式碼,因此,如果我們修改模型,那麼必須重新執行一遍對應的生成指令。
- 修改 DbContext:
修改你的DbContext
類,讓它使用這個預編譯模型。
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
// 指定編譯模型的使用
optionsBuilder.UseModel(CompiledModels.MyCompiledModel.Instance);
}
}
權衡利弊
核心優點:
- 提升啟動速度,對實體型別較多的
DbContext
尤其顯著。
缺點:
- 不支援全域性查詢過濾、
Lazy loading proxies
、Change tracking proxies
和自定義IModelCacheKeyFactory
。 - 每次修改模型都必須重新生成最佳化程式碼。
不支援的東西很多,每次修改模型還需要重新生成就非常麻煩,因此,如果不是真的啟動速度已經非常慢了不建議使用。
後記
我在使用 EF Core 的 Compiled Model 之後依然提示相同的錯誤,後來發現錯誤是從 Reflection 相關類爆出的,而不是 EF Core 的相關類。所以錯誤裡說的 Compiled Model 和 EF Core 的 Compiled Model 概念不同,應該指 AOT 不支援反射中動態載入,需要提前編譯。現在 EF Core 還沒完全準備好,因此,重申一下,EF Core 8 暫時不支援 AOT。