EntityFramework Core 2.x/3.x (ef core) 在遷移中自動生成數據庫表和列說明

前言

在項目開發中有沒有用過拼音首字母做列名或者接手這樣的項目?

看見xmspsqb(項目審批申請表)這種表名時是否有一種無法抑制的想肛了取名的老兄的衝動?

更坑爹的是這種數據庫沒有文檔(或者文檔老舊不堪早已無用)也沒有數據庫內部說明,是不是很無奈?

但是,凡事就怕有但是,有些表和列名字確實太專業(奇葩),用英文不是太長就是根本不知道用什麼(英文差……),似乎也只能用拼音。好吧,那就用吧,寫個說明湊活用用。這個時候問題就來了,如何用sql生成表和列說明?在ef core中又怎樣生成表和列說明?

正文

基礎準備

以sqlserver爲例。

  • 使用ssms管理器編輯說明:不瞎的都知道吧。
  • 使用sql生成說明:
-- 添加
exec sys.sp_addextendedproperty
    @name=N'MS_Description'
  , @value=N'<說明>'
  , @level0type=N'SCHEMA'
  , @level0name=N'<dbo>'
  , @level1type=N'TABLE'
  , @level1name=N'<表名>'
  , @level2type=N'COLUMN'
  , @level2name=N'<列名>'

尖括號中的內容根據情況修改,需要注意,如果說明已經存在會報錯(MSDN的描述爲返回值0爲成功,1爲失敗,並不拋出異常,爲避免麻煩,我選擇先判斷再操作,以下相同)。如果需要添加的是表說明,那麼@level2type@level2nameNULL即可。

-- 刪除
exec sys.sp_dropextendedproperty
    @name=N'MS_Description'
  , @level0type=N'SCHEMA'
  , @level0name=N'<dbo>'
  , @level1type=N'TABLE'
  , @level1name=N'<表名>'
  , @level2type=N'COLUMN'
  , @level2name=N'<列名>'

很好,只需要這兩個內置存儲過程就可以用sql管理說明了,修改雖然也有,但是先刪再加也一樣,就不寫了。還有一個遺留問題,上面的存儲過程可能會報錯,後面的sql就得不到執行,爲避免麻煩,這需要解決一下,思路很直接,查詢下是否存在,存在的話先刪再加,不存在就直接加。

-- 查詢說明是否存在
select exists (
    select t.name as tname,c.name as cname, d.value as Description
    from sysobjects t
    left join syscolumns c
        on c.id=t.id and t.xtype='U' and t.name <> 'dtproperties'
    left join sys.extended_properties d
        on c.id=d.major_id and c.colid=d.minor_id and d.name = 'MS_Description'
    where t.name = '<表名>' and c.name = '<列名>' and d.value is not null)

尖括號中的內容根據情況修改,如果要查詢的是表說明,刪除and c.name = '<列名>'的部分即可。

EF Core 擴展

不錯,判斷問題也解決了,直接使用sql管理基本上也就夠用了,那麼如果使用ef core託管數據庫該怎麼辦呢?思路也很清晰,使用ef遷移。在遷移中管理sql,MigrationBuilder.Sql(string sql)方法可以在遷移中執行任何自定義sql。把上面的sql傳給方法就可以讓ef遷移自動生成說明,並且生成的獨立遷移腳本文件也包含說明相關sql。

問題看似解決了,但是(又是但是),這個解決方案實在是太難用了。

  • 這樣的字符串沒有智能提示和代碼着色,怎麼寫錯的都不知道。
  • 後續管理困難,很可能跟數據庫文檔一樣的下場,沒人維護更新,隨着版本推移逐漸淪爲垃圾。
  • 不好用!不優雅!

接下來就是個人思考嘗試後得到的解決方案:

  • 將說明分散到說明對象的臉上,讓查閱和修改都能隨手完成,降低維護成本,利用C#的特性可以優雅的解決這個問題。
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Enum | AttributeTargets.Property | AttributeTargets.Field, Inherited = true, AllowMultiple = false)]
public class DbDescriptionAttribute : Attribute
{
    /// <summary>
    /// 初始化新的實例
    /// </summary>
    /// <param name="description">說明內容</param>
    public DbDescriptionAttribute(string description) => Description = description;

    /// <summary>
    /// 說明
    /// </summary>
    public virtual string Description { get; }
}
  • 讀取特性並應用到遷移中。不過我並不打算讓遷移直接讀取特性,首先在遷移過程中實體類型並不會載入,從模型獲取實體類型結果是null,需要自己想辦法把模型類型傳入遷移。其次我希望遷移能時刻與模型匹配,ef遷移會生成多個遷移類代碼,追蹤整個實體模型的變更歷史,而特性一旦修改,就會丟失舊的內容,無法充分利用ef遷移的跟蹤能力。基於以上考慮,可以把模型的說明寫入模型註解,ef遷移會將模型註解寫入遷移快照。最後就是在適當的時機讀取特性並寫入註解,很顯然,這個時機就是OnModelCreating方法。
public static ModelBuilder ConfigDatabaseDescription(this ModelBuilder modelBuilder)
{
    foreach (var entityType in modelBuilder.Model.GetEntityTypes())
    {
        //添加表說明
        if (entityType.FindAnnotation(DbDescriptionAnnotationName) == null && entityType.ClrType?.CustomAttributes.Any(
                attr => attr.AttributeType == typeof(DbDescriptionAttribute)) == true)
        {
            entityType.AddAnnotation(DbDescriptionAnnotationName,
                (entityType.ClrType.GetCustomAttribute(typeof(DbDescriptionAttribute)) as DbDescriptionAttribute
                )?.Description);
        }

        //添加列說明
        foreach (var property in entityType.GetProperties())
        {
            if (property.FindAnnotation(DbDescriptionAnnotationName) == null && property.PropertyInfo?.CustomAttributes
                    .Any(attr => attr.AttributeType == typeof(DbDescriptionAttribute)) == true)
            {
                var propertyInfo = property.PropertyInfo;
                var propertyType = propertyInfo?.PropertyType;
                //如果該列的實體屬性是枚舉類型,把枚舉的說明追加到列說明
                var enumDbDescription = string.Empty;
                if (propertyType.IsEnum
                    || (propertyType.IsDerivedFrom(typeof(Nullable<>)) && propertyType.GenericTypeArguments[0].IsEnum))
                {
                    var @enum = propertyType.IsDerivedFrom(typeof(Nullable<>))
                        ? propertyType.GenericTypeArguments[0]
                        : propertyType;

                    var descList = new List<string>();
                    foreach (var field in @enum?.GetFields() ?? new FieldInfo[0])
                    {
                        if (!field.IsSpecialName)
                        {
                            var desc = (field.GetCustomAttributes(typeof(DbDescriptionAttribute), false)
                                .FirstOrDefault() as DbDescriptionAttribute)?.Description;
                            descList.Add(
                                $@"{field.GetRawConstantValue()} : {(desc.IsNullOrWhiteSpace() ? field.Name : desc)}");
                        }
                    }

                    var isFlags = @enum?.GetCustomAttribute(typeof(FlagsAttribute)) != null;
                    var enumTypeDbDescription =
                        (@enum?.GetCustomAttributes(typeof(DbDescriptionAttribute), false).FirstOrDefault() as
                            DbDescriptionAttribute)?.Description;
                    enumTypeDbDescription += enumDbDescription + (isFlags ? " [是標誌位枚舉]" : string.Empty);
                    enumDbDescription =
                        $@"( {(enumTypeDbDescription.IsNullOrWhiteSpace() ? "" : $@"{enumTypeDbDescription}; ")}{string.Join("; ", descList)} )";
                }

                property.AddAnnotation(DbDescriptionAnnotationName,
                    $@"{(propertyInfo.GetCustomAttribute(typeof(DbDescriptionAttribute)) as DbDescriptionAttribute)
                        ?.Description}{(enumDbDescription.IsNullOrWhiteSpace() ? "" : $@" {enumDbDescription}")}");
            }
        }
    }

    return modelBuilder;
}

OnModelCreating方法中調用ConfigDatabaseDescription方法即可將說明寫入模型註解。其中的關鍵是AddAnnotation這個ef core提供的API,不清楚1.x和ef 6.x有沒有這個功能。其中DbDescriptionAnnotationName就是個名稱,隨便取,只要不和已有註解重名即可。可以看到,這個方法同時支持掃描並生成枚舉類型的說明,包括可空枚舉。

  • 在遷移中讀取模型註解並生成說明。有了之前的準備工作,到這裏就好辦了。
public static MigrationBuilder ApplyDatabaseDescription(this MigrationBuilder migrationBuilder, Migration migration)
{
    var defaultSchema = "dbo";
    var descriptionAnnotationName = ModelBuilderExtensions.DbDescriptionAnnotationName;

    foreach (var entityType in migration.TargetModel.GetEntityTypes())
    {
        //添加表說明
        var tableName = entityType.Relational().TableName;
        var schema = entityType.Relational().Schema;
        var tableDescriptionAnnotation = entityType.FindAnnotation(descriptionAnnotationName);

        if (tableDescriptionAnnotation != null)
        {
            migrationBuilder.AddOrUpdateTableDescription(
                tableName,
                tableDescriptionAnnotation.Value.ToString(),
                schema.IsNullOrEmpty() ? defaultSchema : schema);
        }

        //添加列說明
        foreach (var property in entityType.GetProperties())
        {
            var columnDescriptionAnnotation = property.FindAnnotation(descriptionAnnotationName);

            if (columnDescriptionAnnotation != null)
            {
                migrationBuilder.AddOrUpdateColumnDescription(
                    tableName,
                    property.Relational().ColumnName,
                    columnDescriptionAnnotation.Value.ToString(),
                    schema.IsNullOrEmpty() ? defaultSchema : schema);
            }
        }
    }

    return migrationBuilder;
}

在遷移的UpDown方法末尾調用ApplyDatabaseDescription方法即可取出模型註解中的說明並生成和執行相應的sql。

至此,一個好用的數據庫說明管理就基本完成了。因爲這個方法使用了大量ef core提供的API,所以基本上是完整支持ef core的各種實體映射,實測包括與實體類名、屬性名不一致的表名、列名,(嵌套的)Owned類型屬性(類似ef 6.x的複雜類型屬性 Complex Type)、表拆分等。可以說基本上沒有什麼後顧之憂。這裏的sql是以sqlserver爲例,如果使用的是mysql或其他關係數據庫,需要自行修改sql以及AddOrUpdateColumnDescriptionAddOrUpdateTableDescription的邏輯。

其中Owned類型屬性在生成遷移時可能會生成錯誤代碼,導致編譯錯誤CS1061 "ReferenceOwnershipBuilder"未包含"HasAnnotation"的定義且……;,只需要把HasAnnotation替換成HasEntityTypeAnnotation即可。估計是微軟的老兄粗心沒注意這個問題。(貌似EF Core 3.0後這個問題消失了,HasEntityTypeAnnotation方法被刪除)

ps:爲什麼不直接使用Description或者DisplayName之類的內置特性而要使用自定義特性,因爲Description在語義上是指廣泛的說明,並不能明確表明這是數據庫說明,同時避免與現存代碼糾纏不清影響使用,爲加強語義性,使用新增的自定義特性。

ps2:爲什麼不使用xml註釋文檔,因爲這會讓這個功能產生對/doc編譯選項和弱類型文本的依賴,甚至需要對文檔配置嵌入式資源,也會增加編碼難度,同時會影響現存代碼的xml註釋,爲避免影響現存代碼,對非代碼和編譯器不可檢查行爲的依賴,保證代碼健壯性,不使用xml文檔註釋。

效果預覽

效果預覽
效果預覽
效果預覽
效果預覽
效果預覽
效果預覽
效果預覽

更新

2019-12-14

已更新 EF core 3.x 版本代碼,具體代碼請看 Github 項目庫中的 NetCore_3.0 分支(實際上已經更新到 .Net Core 3.1,懶得改名了。好麻煩 (¬_¬") )。

轉載請完整保留以下內容,未經授權刪除以下內容進行轉載盜用的,保留追究法律責任的權利!

本文地址:https://www.cnblogs.com/coredx/p/10026783.html

完整源代碼:Github

裏面有各種小東西,這只是其中之一,不嫌棄的話可以Star一下。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章