zoukankan      html  css  js  c++  java
  • EntityFramework Core 2.x/3.x (ef core) 在迁移中自动生成数据库表和列说明

    在项目开发中有没有用过拼音首字母做列名或者接手这样的项目?

    看见xmspsqb(项目审批申请表)这种表名时是否有一种无法抑制的想肛了取名的老兄的冲动?

    更坑爹的是这种数据库没有文档(或者文档老旧不堪早已无用)也没有数据库内部说明,是不是很无奈?

    但是,凡事就怕有但是,有些表和列名字确实太专业(奇葩),用英文不是太长就是根本不知道用什么(英文差……),似乎也只能用拼音。好吧,那就用吧,写个说明凑活用用。这个时候问题就来了,如何用sql生成表和列说明?在ef core中又怎样生成表和列说明?

    以sqlserver为例。

    1、使用ssms管理器编辑说明:不瞎的都知道吧。

    2、使用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'列名'

      红字根据情况修改,需要注意,如果说明已经存在会报错。如果需要添加的是表说明,那么@level2type 和 @level2name填NULL即可。

      删除

      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)

      红字根据情况修改,如果要查询的是表说明,删除下划线部分即可。

      不错,判断问题也解决了,直接使用sql管理基本上也就够用了,那么如果使用ef core托管数据库该怎么办呢?思路也很清晰,使用ef迁移。在迁移中管理sql,MigrationBuilder.Sql(string sql)方法可以在迁移中执行任何自定义sql。把上面的sql传给方法就可以让ef迁移自动生成说明,并且生成的独立迁移脚本文件也包含说明相关sql。

      问题看似解决了,但是(又是但是),这个解决方案实在是太难用了。1、这样的字符串没有智能提示和代码着色,怎么写错的都不知道。2、后续管理困难,很可能跟数据库文档一样的下场,没人维护更新,随着版本推移逐渐沦为垃圾。3、不好用!不优雅!

      接下来就是个人思考尝试后得到的解决方案:

      1、将说明分散到说明对象的脸上,让查阅和修改都能随手完成,降低维护成本,利用C#的特性可以优雅的解决这个问题。

     1     [AttributeUsage(AttributeTargets.Class | AttributeTargets.Enum | AttributeTargets.Property | AttributeTargets.Field, Inherited = true, AllowMultiple = false)]
     2     public class DbDescriptionAttribute : Attribute
     3     {
     4         /// <summary>
     5         /// 初始化新的实例
     6         /// </summary>
     7         /// <param name="description">说明内容</param>
     8         public DbDescriptionAttribute(string description) => Description = description;
     9 
    10         /// <summary>
    11         /// 说明
    12         /// </summary>
    13         public virtual string Description { get; }
    14     }

      2、读取特性并应用到迁移中。不过我并不打算让迁移直接读取特性,首先在迁移过程中实体类型并不会载入,从模型获取实体类型结果是null,需要自己想办法把模型类型传入迁移。其次我希望迁移能时刻与模型匹配,ef迁移会生成多个迁移类代码,追踪整个实体模型的变更历史,而特性一旦修改,就会丢失旧的内容,无法充分利用ef迁移的跟踪能力。基于以上考虑,可以把模型的说明写入模型注解,ef迁移会将模型注解写入迁移快照。最后就是在适当的时机读取特性并写入注解,很显然,这个时机就是OnModelCreating方法。

     1         public static ModelBuilder ConfigDatabaseDescription(this ModelBuilder modelBuilder)
     2         {
     3             foreach (var entityType in modelBuilder.Model.GetEntityTypes())
     4             {
     5                 //添加表说明
     6                 if (entityType.FindAnnotation(DbDescriptionAnnotationName) == null && entityType.ClrType?.CustomAttributes.Any(
     7                         attr => attr.AttributeType == typeof(DbDescriptionAttribute)) == true)
     8                 {
     9                     entityType.AddAnnotation(DbDescriptionAnnotationName,
    10                         (entityType.ClrType.GetCustomAttribute(typeof(DbDescriptionAttribute)) as DbDescriptionAttribute
    11                         )?.Description);
    12                 }
    13 
    14                 //添加列说明
    15                 foreach (var property in entityType.GetProperties())
    16                 {
    17                     if (property.FindAnnotation(DbDescriptionAnnotationName) == null && property.PropertyInfo?.CustomAttributes
    18                             .Any(attr => attr.AttributeType == typeof(DbDescriptionAttribute)) == true)
    19                     {
    20                         var propertyInfo = property.PropertyInfo;
    21                         var propertyType = propertyInfo?.PropertyType;
    22                         //如果该列的实体属性是枚举类型,把枚举的说明追加到列说明
    23                         var enumDbDescription = string.Empty;
    24                         if (propertyType.IsEnum
    25                             || (propertyType.IsDerivedFrom(typeof(Nullable<>)) && propertyType.GenericTypeArguments[0].IsEnum))
    26                         {
    27                             var @enum = propertyType.IsDerivedFrom(typeof(Nullable<>))
    28                                 ? propertyType.GenericTypeArguments[0]
    29                                 : propertyType;
    30 
    31                             var descList = new List<string>();
    32                             foreach (var field in @enum?.GetFields() ?? new FieldInfo[0])
    33                             {
    34                                 if (!field.IsSpecialName)
    35                                 {
    36                                     var desc = (field.GetCustomAttributes(typeof(DbDescriptionAttribute), false)
    37                                         .FirstOrDefault() as DbDescriptionAttribute)?.Description;
    38                                     descList.Add(
    39                                         $@"{field.GetRawConstantValue()} : {(desc.IsNullOrWhiteSpace() ? field.Name : desc)}");
    40                                 }
    41                             }
    42 
    43                             var isFlags = @enum?.GetCustomAttribute(typeof(FlagsAttribute)) != null;
    44                             var enumTypeDbDescription =
    45                                 (@enum?.GetCustomAttributes(typeof(DbDescriptionAttribute), false).FirstOrDefault() as
    46                                     DbDescriptionAttribute)?.Description;
    47                             enumTypeDbDescription += enumDbDescription + (isFlags ? " [是标志位枚举]" : string.Empty);
    48                             enumDbDescription =
    49                                 $@"( {(enumTypeDbDescription.IsNullOrWhiteSpace() ? "" : $@"{enumTypeDbDescription}; ")}{string.Join("; ", descList)} )";
    50                         }
    51 
    52                         property.AddAnnotation(DbDescriptionAnnotationName,
    53                             $@"{(propertyInfo.GetCustomAttribute(typeof(DbDescriptionAttribute)) as DbDescriptionAttribute)
    54                                 ?.Description}{(enumDbDescription.IsNullOrWhiteSpace() ? "" : $@" {enumDbDescription}")}");
    55                     }
    56                 }
    57             }
    58 
    59             return modelBuilder;
    60         }

      在OnModelCreating方法中调用ConfigDatabaseDescription方法即可将说明写入模型注解。其中的关键是AddAnnotation这个ef core提供的API,不清楚1.x和ef 6.x有没有这个功能。其中DbDescriptionAnnotationName就是个名称,随便取,只要不和已有注解重名即可。可以看到,这个方法同时支持扫描并生成枚举类型的说明,包括可空枚举。

      3、在迁移中读取模型注解并生成说明。有了之前的准备工作,到这里就好办了。

     1         public static MigrationBuilder ApplyDatabaseDescription(this MigrationBuilder migrationBuilder, Migration migration)
     2         {
     3             var defaultSchema = "dbo";
     4             var descriptionAnnotationName = ModelBuilderExtensions.DbDescriptionAnnotationName;
     5 
     6             foreach (var entityType in migration.TargetModel.GetEntityTypes())
     7             {
     8                 //添加表说明
     9                 var tableName = entityType.Relational().TableName;
    10                 var schema = entityType.Relational().Schema;
    11                 var tableDescriptionAnnotation = entityType.FindAnnotation(descriptionAnnotationName);
    12 
    13                 if (tableDescriptionAnnotation != null)
    14                 {
    15                     migrationBuilder.AddOrUpdateTableDescription(
    16                         tableName,
    17                         tableDescriptionAnnotation.Value.ToString(),
    18                         schema.IsNullOrEmpty() ? defaultSchema : schema);
    19                 }
    20 
    21                 //添加列说明
    22                 foreach (var property in entityType.GetProperties())
    23                 {
    24                     var columnDescriptionAnnotation = property.FindAnnotation(descriptionAnnotationName);
    25 
    26                     if (columnDescriptionAnnotation != null)
    27                     {
    28                         migrationBuilder.AddOrUpdateColumnDescription(
    29                             tableName,
    30                             property.Relational().ColumnName,
    31                             columnDescriptionAnnotation.Value.ToString(),
    32                             schema.IsNullOrEmpty() ? defaultSchema : schema);
    33                     }
    34                 }
    35             }
    36 
    37             return migrationBuilder;
    38         }

      在迁移的Up和Down方法末尾调用ApplyDatabaseDescription方法即可取出模型注解中的说明并生成和执行相应的sql。

      至此,一个好用的数据库说明管理就基本完成了。因为这个方法使用了大量ef core提供的API,所以基本上是完整支持ef core的各种实体映射,实测包括与实体类名、属性名不一致的表名、列名,(嵌套的)Owned类型属性(类似ef 6.x的复杂类型属性 Complex Type)、表拆分等。可以说基本上没有什么后顾之忧。这里的sql是以sqlserver为例,如果使用的是mysql或其他关系数据库,需要自行修改sql以及AddOrUpdateColumnDescription和AddOrUpdateTableDescription的逻辑。

      其中Owned类型属性在生成迁移时可能会生成错误代码,导致编译错误CS1061 "ReferenceOwnershipBuilder"未包含"HasAnnotation"的定义且……,只需要把HasAnnotation替换成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一下。

  • 相关阅读:
    Flask学习目录
    Android笔记
    PyQt学习目录
    学习目录
    Python学习目录一
    sqlserver STUFF & STR
    PowerShell+Jenkins,实现项目的自动化部署
    DotNet Core Backend development
    .Net Core 类注入单元测试
    团队项目之团队展示&选题
  • 原文地址:https://www.cnblogs.com/coredx/p/10026783.html
Copyright © 2011-2022 走看看