目录
EFCore中如何移除主外键关系
场景介绍
我用EFCore写了一个blog程序,我要通过写文章来分享自己的知识,我定义了一个Article
用来存放文章信息,我还定义了一个Category
用来存放文章的分类,Category
与Article
是一对的关系。我的代码实现如下:
Article
public class Article
{
public int Id {get;set;}
public int CategoryId {get;set;}
//导航属性,efcore会自动创建主外键关系
public Category Category {get;set;}
}
Category
public class Category
{
public int Id {get;set;}
//导航属性,efcore会自动创建主外键关系
public List<Article> Articles { get; set; }
}
MyBlogDbContext
public class MyBlogDbContext:DbContext
{
public MyBlogDbContext(DbContextOptions options):base(options)
{}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
//Article
var articleBuilder = modelBuilder.Entity<Article>();
articleBuilder.ToTable("Article");
articleBuilder.HasKey(article => article.Id);
articleBuilder.HasIndex(article => article.CategoryId);
//Category
var categoryBuilder=modelBuilder.Entity<Category>();
categoryBuilder.ToTable("Category");
categoryBuilder.HasKey(category => category.Id);
}
}
主外键关系的问题
- 当我想添加一片文章的时候,主外键要求我先添加这个文章的分类才允许我添加文章
- 当我想删除一个分类的时候,主要建会将我的文章也删除
- 总之,级联给我带来了很多烦恼
解决办法
- 修改数据,禁用级联功能
- 删除我们代码中的导航属性,阻止生成级联关系
以上两种办法都不是我想要的:
我不想去操作数据库,因为我用了code first,ef会去操作数据库,所以我不想去修改数据库的级联功能(实际项目中我还是回去禁用数据库的级联关系)。
我也不想去删除导航属性,因为我想用ef core的Include功能。
解决思路
在不修改数据的设置,也不删除导航属性的前提下实现禁用级联功能,我的做法是禁止级联关系的生成,可能你会说你这等于变相修改了sql,但是我确实没有写sql删除级联关系,也没有删除导航属性,总之,我的目的达到了,效果还不错。那么我是这么实现的?
禁止级联关系的生成
我要做的是取翻看EFCore的代码,找到真正生成级联sql的地方然后重写,幸运的是我找到了,这个类就是SqlServerMigrationsSqlGenerator
,我的实现如下:
CustomMigrationsSqlGeneratore
public class CustomMigrationsSqlGeneratore : SqlServerMigrationsSqlGenerator
{
public CustomMigrationsSqlGeneratore( MigrationsSqlGeneratorDependencies dependencies, IMigrationsAnnotationProvider migrationsAnnotations) : base(dependencies, migrationsAnnotations)
{
}
//重写这个方法
protected override void Generate(CreateTableOperation operation, IModel model, MigrationCommandListBuilder builder)
{
//删除级联关系
RemoveForeignKeysHelper.ExecuForeignKeys(operation);
base.Generate(operation, model, builder);
}
}
RemoveForeignKeysHelper
public class RemoveForeignKeysHelper
{
//定义个全局变量,用来存储需要移除的级联属性
internal static ConcurrentDictionary<string, List<string>> RemoveForeignKeys = new ConcurrentDictionary<string, List<string>>();
public static void ExecuForeignKeys(CreateTableOperation operation)
{
if (RemoveForeignKeys.TryGetValue(operation.Name, out List<string> columns))
{
operation.ForeignKeys
.Where(item => item.Columns.Intersect(columns).Count() > 0)
.ToList()
.ForEach(item => operation.ForeignKeys.Remove(item));
}
}
}
EntityTypeBuilderExtensions
为EntityTypeBuilder添加扩展方法,通过扩展方法纪录那些需要被移除的级联关系
public static class EntityTypeBuilderExtensions
{
public static EntityTypeBuilder<T> RemoeForeignKey<T>(this EntityTypeBuilder<T> builder,string name) where T : class
{
var tableName = builder.Metadata.FindAnnotation("Relational:TableName").Value.ToString();
RemoveForeignKeysHelper.RemoveForeignKeys.AddOrUpdate(tableName, new List<string> { name },(value,values)=> {
values.Add(name);
return values.Distinct().ToList();
});
return builder;
}
}
DbContextOptionsBuilderExtensions
通过依赖注入,将生产sql的服务替换成我们自己的
public static class DbContextOptionsBuilderExtensions
{
public static DbContextOptionsBuilder UseRemoveForeignKeyService(this DbContextOptionsBuilder options)
{
options.ReplaceService<IMigrationsSqlGenerator, CustomMigrationsSqlGeneratore>();
return options;
}
}
到此,所有的代码都已经搞定,我们来看看怎么在我们的代码中引入这些功能。
首先,我们在创建Model的时候设置我要移除的级联关系,修改我们之前定义的MyBlogDbContext
类
MyBlogDbContext
public class MyBlogDbContext:DbContext
{
public MyBlogDbContext(DbContextOptions options):base(options)
{}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
//Article
var articleBuilder = modelBuilder.Entity<Article>();
articleBuilder.ToTable("Article");
articleBuilder.HasKey(article => article.Id);
articleBuilder.HasIndex(article => article.CategoryId);
///你没有看错就是这么顺滑
articleBuilder.RemoeForeignKey("CategoryId");
//Category
var categoryBuilder=modelBuilder.Entity<Category>();
categoryBuilder.ToTable("Category");
categoryBuilder.HasKey(category => category.Id);
}
}
然后,将IMigrationsSqlGenerator
替换成我们自定义的类CustomMigrationsSqlGeneratore
//AddDbContxt记得吧,在Startup中或者在你自己扩展的IServiceCollection方法中
service.AddDbContext<MicroBlogDbContext>(options =>
{
//核心操作就在这里
options.UseRemoveForeignKeyService();
options.UseMySql(connStr,config=> {
config.CharSetBehavior(CharSetBehavior.AppendToAllColumns);
config.AnsiCharSet(CharSet.Latin1);
config.UnicodeCharSet(CharSet.Utf8mb4);
});
});
完!
MicroFX.EntityFrameworkCore.RemoveForeignKey扩展
我写了个扩展,目前支持mysql和sqlserver,如果有机会我也会实现其它数据库的扩展。
MicroFX.EntityFrameworkCore.RemoveForeignKey