EF Core預編譯模型Compiled Model

波多爾斯基發表於2023-11-20

前言

最近還在和 npgsqlEF Core 鬥爭,由於 EF Core 暫時還不支援 AOT,因此在 AOT 應用程式中使用 EF Core 時,會提示問題:

image

聽這個意思,似乎使用 Compiled Model 可以解決問題,於是就又研究了一下 EF Core 的這個功能。

在 EF Core 中,模型根據實體類和配置構建,預設情況下,每次建立一個新的 DbContext 例項時,EF Core 都會構建模型。對於需要頻繁建立 DbContext 例項的應用程式,這可能會導致效能問題。

Entity Framework Core(EF Core)的預編譯模型(Compiled Model)對應提供了一種最佳化,在 EF Core 6 preview 5 中首次增加了這個功能,可以讓設計人員預編譯模型,避免在後續執行查詢時動態生成模型。

預編譯模型的優勢

  1. 效能提升:透過預編譯模型,可以減少應用程式啟動時的開銷,特別是對於大型模型。

此處的啟動時間,指 DbContext 的首次啟動時間,由於延遲查詢的機制,一般 DbContext 並不會在新建物件時完成啟動,而是在首次執行插入或者查詢時完成這個過程。

參考下圖(來自參考 1):
image

顯然,隨著模型的規模增大,啟動時間線性增長;但是使用預編譯模型後,啟動時間和模型大小基本無關,保持在一個極低的水平。

  1. 一致性:確保每個 DbContext 例項使用相同的模型配置。

使用預編譯模型

  1. 生成編譯模型
    使用 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);
}

可以看到,最佳化工具幫我們生成了非常多的程式碼,尤其是與型別描述相關的程式碼,因此,如果我們修改模型,那麼必須重新執行一遍對應的生成指令。

  1. 修改 DbContext
    修改你的 DbContext 類,讓它使用這個預編譯模型。
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    if (!optionsBuilder.IsConfigured)
    {
        // 指定編譯模型的使用
        optionsBuilder.UseModel(CompiledModels.MyCompiledModel.Instance);
    }
}

權衡利弊

核心優點:

  1. 提升啟動速度,對實體型別較多的 DbContext 尤其顯著。

缺點:

  1. 不支援全域性查詢過濾、Lazy loading proxiesChange tracking proxies 和自定義 IModelCacheKeyFactory
  2. 每次修改模型都必須重新生成最佳化程式碼。

不支援的東西很多,每次修改模型還需要重新生成就非常麻煩,因此,如果不是真的啟動速度已經非常慢了不建議使用

後記

我在使用 EF Core 的 Compiled Model 之後依然提示相同的錯誤,後來發現錯誤是從 Reflection 相關類爆出的,而不是 EF Core 的相關類。所以錯誤裡說的 Compiled Model 和 EF Core 的 Compiled Model 概念不同,應該指 AOT 不支援反射中動態載入,需要提前編譯。現在 EF Core 還沒完全準備好,因此,重申一下,EF Core 8 暫時不支援 AOT

參考

相關文章