EntityFramework Core 2.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#的特性可以优雅的解决这个问题。

     [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; }
}

  2、读取特性并应用到迁移中。不过我并不打算让迁移直接读取特性,首先在迁移过程中实体类型并不会载入,从模型获取实体类型结果是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[].IsEnum))
{
var @enum = propertyType.IsDerivedFrom(typeof(Nullable<>))
? propertyType.GenericTypeArguments[]
: propertyType; var descList = new List<string>();
foreach (var field in @enum?.GetFields() ?? new FieldInfo[])
{
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就是个名称,随便取,只要不和已有注解重名即可。可以看到,这个方法同时支持扫描并生成枚举类型的说明,包括可空枚举。

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

         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;
}

  在迁移的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文档注释。

  效果预览:

EntityFramework Core 2.x (ef core) 在迁移中自动生成数据库表和列说明EntityFramework Core 2.x (ef core) 在迁移中自动生成数据库表和列说明EntityFramework Core 2.x (ef core) 在迁移中自动生成数据库表和列说明EntityFramework Core 2.x (ef core) 在迁移中自动生成数据库表和列说明EntityFramework Core 2.x (ef core) 在迁移中自动生成数据库表和列说明EntityFramework Core 2.x (ef core) 在迁移中自动生成数据库表和列说明EntityFramework Core 2.x (ef core) 在迁移中自动生成数据库表和列说明

更新(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一下。

上一篇:Java实现文件的RSA和DES加密算法


下一篇:一个网页从输入URL到页面加载完成的过程中都发生了什么事情?