斯人若彩虹
排名
6
文章
6
粉丝
16
评论
8
{{item.articleTitle}}
{{item.blogName}} : {{item.content}}
ICP备案 :渝ICP备18016597号-1
网站信息:2018-2022TNBLOG.NET
技术交流:群号677373950
联系我们:contact@tnblog.net
欢迎加群交流技术

新版EF自定义表名主键规则,统一并发列与级联删除等规则处理

1266人阅读 2021/12/22 16:20 总访问:1638562 评论:0 收藏:0 手机
分类: EF

EF根据模型生成表的时候会默认加上复数,以前要想解决这个写法很简单,把这个复数规则删除掉即可

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{    
    //删除掉默认添加的复数形式规则
    modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
}

但是这种modelBuilder.Conventions这种写法已经不能使用了



  下面说一下在新版中如何实现,自定义表名规则



添加一些扩展方法备用:

public static class EFExtands
{
    public static IEnumerable<IMutableEntityType> EntityTypes(this ModelBuilder builder)
    {
        return builder.Model.GetEntityTypes();
    }

    public static IEnumerable<IMutableProperty> Properties(this ModelBuilder builder)
    {
        return builder.EntityTypes().SelectMany(entityType => entityType.GetProperties());
    }

    public static IEnumerable<IMutableProperty> Properties<T>(this ModelBuilder builder)
    {
        return builder.EntityTypes().SelectMany(entityType => entityType.GetProperties().Where(x => x.ClrType == typeof(T)));
    }

    public static void Configure(this IEnumerable<IMutableEntityType> entityTypes, Action<IMutableEntityType> convention)
    {
        foreach (var entityType in entityTypes)
        {
            convention(entityType);
        }
    }

    public static void Configure(this IEnumerable<IMutableProperty> propertyTypes, Action<IMutableProperty> convention)
    {
        foreach (var propertyType in propertyTypes)
        {
            convention(propertyType);
        }
    }
}

现在要实现删除表名的默认复数形式就很简单了,代码如下

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    //删除默认的表名复数规则,表名按照类名为标准
    modelBuilder.Model.GetEntityTypes().Configure(et => et.SetTableName(et.DisplayName()));
}

然后执行一次数据库迁移,就可以看到数据的表,已经没有自动加上复数了,和实体名称保持一致了


这样就可以直接实现了,但是如果我们想有一个特殊的命名规则呢,比如最简单的Table特性来自定义表名

用上面的写法是不行的,它直接统一的把生成的表名设置成了实体名一样的名称



要想自定义表名规则来解决这个问题,我们需要知道两个内容,一个是如何拿到Table特性对应的表名与实体对应的表名,就是能够拿到两种不同情况的表名,还有一个是如何区分实体上面有没有Table这个特性。当然我们分析这个不仅仅是为了解决这一个问题,其他类似的规则或者更特殊的自定义规则都这种通过这种思路去解决,灵活运用。

先思考第一点:如何拿到Table特性对应的表名与实体对应的表名,就是能够拿到两种不同情况的表名
其实上面那个设置表名规则我们已经看到了DisplayName就是实体的名字嘛

那另外一个特性对应的表名怎么拿呢,根据上面的方法我们可以顺势推出来其他可能的方法,实在不知道我们可以挨着分析一下可能的方法在调试一下即可,具体的方法过程就不细说了,我们直接看代码:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    //删除默认的表名复数规则,表名按照类名为标准
    //modelBuilder.Model.GetEntityTypes().Configure(et => et.SetTableName(et.DisplayName()));

    //分析表名规则
    modelBuilder.Model.GetEntityTypes().ToList().ForEach(a =>
    {
        string tableName = a.GetTableName();//默认的规则按照这个名称来实现的
        string defaultTableName = a.GetDefaultTableName();
        string displayName = a.DisplayName();//实体名称
        var typeName = a.GetType();
    });
}

这里我们猜测了一下三个可能的表名,GetTableName(),GetDefaultTableName(),DisplayName(),我们运气打断点调试一下OnModelCreating方法直接的过程就知道了

根据分析得知,GetTableName()就是默认的规则,比如默认给你加复数。

或者有Table特性的时候GetTableName就是加特性的名字。(注意在分析表名规则的时候先把方法设置规则的去掉,不然已经设置表名规则按照DisplayName标准来,那么下面在执行的时候三种方法获取的内容都完全一样,就没有分析的意义了)

到这一步为止我们第一步两个表名都已经能拿到了,第一步的问题算解决了。然后我们可以根据是否有Table特性来自定义决定使用什么命名规则,甚至是其他千奇百怪的规则都可以自定义。


然后是第二点如何确定实体类有没有table这个特性 
我们可以首先想到的是反射取特性,但是我们看到了FindAnnotation和GetAnnotations方法我们可以试一下,看微软注释是FindAnnotation找不到就返回空GetAnnotations找不到就报错,所以我们可以试一下FindAnnotation方法来验证一下我们的猜想。
微软官方对这个方法的解释:https://docs.microsoft.com/zh-tw/dotnet/api/microsoft.entityframeworkcore.infrastructure.iannotatable.findannotation?view=efcore-5.0

代码如下我们获取一下这个特性为空的试试,如果是三个就对了,因为我们是4个实体,只有一个student实体是有Table特性的

但是实际情况并不是我们猜想的那样,而是4个实体获取的都为空

后面才发现这个方法不是那样用的,不是我们想象的取实体上面有没有那个特性的,而是可以通过Relational:TableName,Relational:ColumnName,Relational:ColumnType来取表名取列明这些,比如这样:

在做其他的时候我们也可以尝试用一下这个方法但是这里用这个并不行。

所以我们还是考虑反射,反射来获取特性就比较简单的,我们随便来写两个类测试一下。

//反射取特性
object[] objArray = typeof(Student).GetCustomAttributes(typeof(TableAttribute), false);//结果是1个
object[] objcourseArray = typeof(Course).GetCustomAttributes(typeof(TableAttribute), false);//结果是0个

结果完全是没有猜想的那样,然后我们就可以实际用起来了

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    //没有TableAttribute特性的就使用和model一样的类名
    modelBuilder.Model.GetEntityTypes().Where(a => a.GetType().GetCustomAttributes(typeof(TableAttribute), false).Count() == 0)
                    .Configure(et => et.SetTableName(et.DisplayName()));

    //有TableAttribute特性的就使用和Table特性规定的类名
    modelBuilder.Model.GetEntityTypes().Where(a => a.GetType().GetCustomAttributes(typeof(TableAttribute), false).Count() > 0)
                    .Configure(et => et.SetTableName(et.GetTableName()));
}

但是我们会发现并没有成功,它并没有获取到TableAttribute特性,其实这里也是一个坑,因为这里的a并不是和实体一样的,是ef在实体上封装过的,要拿到实体的具体类型使用a.ClrType就可以了,这点我们可以调试得知,所以要实现这个功能最终代码如下:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    //没有TableAttribute特性的就使用和model一样的类名
    modelBuilder.Model.GetEntityTypes().Where(a => a.ClrType.GetCustomAttributes(typeof(TableAttribute), false).Count() == 0)
                    .Configure(et => et.SetTableName(et.DisplayName()));
    //有TableAttribute特性的就使用和Table特性规定的类名
    modelBuilder.Model.GetEntityTypes().Where(a => a.ClrType.GetCustomAttributes(typeof(TableAttribute), false).Count() > 0)
                    .Configure(et => et.SetTableName(et.GetTableName()));
}

还是比较简洁的,然后我们迁移一下数据库就能看到规则已经成功了。除了student表其他表名都和实体名一样



下面贴个第二种方法:勉强算第二种吧,其实本质都一样

string typestr = "System.ComponentModel.DataAnnotations.Schema.TableAttribute".Trim();
modelBuilder.Model.GetEntityTypes().ToList().ForEach(a =>
{
    IEnumerable<CustomAttributeData> m = a.ClrType.CustomAttributes;//拿到自定义属性

    foreach (var item in m)
    {
        string type = item.AttributeType.FullName.Trim();

        if (type == typestr)
        {
            a.SetTableName(a.GetTableName());
            return;
        }
    }
    a.SetTableName(a.GetDefaultTableName());

});



  当然其他的,比如处理并发冲突列的统一标注,类型判断等也比较方便



统一把Timestamp标注为ConcurrencyToken:

modelBuilder.Properties().Where(x => x.Name == "Timestamp").Configure(p => p.IsConcurrencyToken = true);

类型判断:

modelBuilder.Model.GetEntityTypes()            
    .Where(x => !x.ClrType.IsSubclassOf(typeof(ValueObject)))
    .Configure(et => et.SetTableName(et.DisplayName()));

或者:

modelBuilder.Model.GetEntityTypes()
    .Where(x => x.ClrType.BaseType == typeof(BaseEntity))
    .Configure(et => et.SetTableName(et.DisplayName()));


统一设置主键规则

modelBuilder.Properties()
    .Where(x => x.Name.ToLower() == "id")
    .Configure(p => p.SetColumnName(p.DeclaringEntityType.ShortName() + "Id"));

方法2:

modelBuilder.Model.GetEntityTypes().ToList().ForEach(a =>
{
    var t = a.GetProperties();
    foreach (var item in t)
    {
        string name = item.Name;
        string TableName = a.GetDefaultTableName();
        if (name.ToLower() == "id")
        {
            item.SetColumnName(TableName + "Id");
        }
    }
});



 新版ef级联删除



和以前的写法有点不同现在没有modelBuilder.Conventions这种用法了,单独开启级联删除的方法也和以前不太一样

以前的写法:

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{    
 //删除掉级联删除的规则
 modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();
}

以前单独的控制写法:

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    //关闭级联删除
    modelBuilder.Entity<Pet>().HasRequired(a => a.userinfo).WithMany(a => a.pets).HasForeignKey(a => a.UserId).WillCascadeOnDelete(false);
}

以前使用的是WillCascadeOnDelete方法,而且参数只有true和false,就是是否开启级联查询

.NET CORE中EF级联删除的写法
使用的是OnDelete,而且参数是枚举不止,ture和false

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Student>().Property(p => p.RowVersion).IsConcurrencyToken();
    modelBuilder.Entity<UserParent>().HasMany(a => a.Students).WithOne(a => a.UserParent).HasForeignKey(a => a.MyUserParentId).OnDelete(DeleteBehavior.NoAction);
}

DeleteBehavior.NoAction:就是不开启级联删除,会出发外键约束。
DeleteBehavior.Cascade:就是开启级联删除,是默认值
其他还有:DeleteBehavior.SetNull,DeleteBehavior.ClientSetNull,Restrict等


欢迎加群讨论技术,群号:677373950

评价