EntityFramework CodeFirst:相同 table 多对多关系的 CASCADE DELETE
EntityFramework CodeFirst: CASCADE DELETE for same table many-to-many relationship
我在 EntityFramework 和同一实体的多对多关系中遇到条目删除问题。考虑这个简单的例子:
实体:
public class UserEntity {
// ...
public virtual Collection<UserEntity> Friends { get; set; }
}
流畅API配置:
modelBuilder.Entity<UserEntity>()
.HasMany(u => u.Friends)
.WithMany()
.Map(m =>
{
m.MapLeftKey("UserId");
m.MapRightKey("FriendId");
m.ToTable("FriendshipRelation");
});
- 我说得对吗,无法在 Fluent API 中定义
Cascade Delete
?
删除 UserEntity
的最佳方法是什么,例如 Foo
?
它现在寻找我,我必须 Clear
Foo
的 Friends
集合,然后我必须加载所有其他 UserEntities
,其中包含 Foo
in Friends
,然后从每个列表中删除 Foo
,然后再从 Users
中删除 Foo
。但这听起来太复杂了。
是否可以直接访问关系 table,以便我可以删除这样的条目
// Dummy code
var query = dbCtx.Set("FriendshipRelation").Where(x => x.UserId == Foo.Id || x.FriendId == Foo.Id);
dbCtx.Set("FriendshipRelation").RemoveRange(query);
谢谢!
更新01:
我知道这个问题的最佳解决方案是在调用 SaveChanges
:
之前执行原始 sql 语句
dbCtx.Database.ExecuteSqlCommand(
"delete from dbo.FriendshipRelation where UserId = @id or FriendId = @id",
new SqlParameter("id", Foo.Id));
但是这样做的缺点是,如果 SaveChanges
由于某种原因失败,FriendshipRelation
已经被删除并且无法回滚。还是我错了?
1) 我没有看到任何直接的方法来使用 FluentApi 控制多对多关系上的级联。
2) 我能想到的唯一可用的控制方法是使用 ManyToManyCascadeDeleteConvention
,我想它是默认启用的,至少对我来说是这样。我刚刚检查了我的一个迁移,包括多对多关系,确实 cascadeDelete: true
是两个键的地方。
编辑:抱歉,我刚刚发现 ManyToManyCascadeDeleteConvention
没有涵盖自引用案例。这个 related question 的回答说
You receive this error message because in SQL Server, a table cannot appear more than one time in a list of all the cascading referential actions that are started by either a DELETE or an UPDATE statement. For example, the tree of cascading referential actions must only have one path to a particular table on the cascading referential actions tree.
所以你最终不得不有一个自定义删除代码(就像你已经拥有的 sql 命令)并在 transaction scope.
中执行它
3) 您不应该能够从上下文中访问 table。通常由多对多关系创建的 table 是关系 DBMS 中实现的副产品,被认为是 weak table相关的 tables,这意味着如果删除了相关实体之一,则应级联删除其行。
我的建议是,首先检查您的迁移是否将 table 外键设置为级联删除。然后,如果由于某种原因你需要限制删除在多对多关系中有相关记录的记录,那么你只需在你的事务中检查它。
4) 为了做到这一点,如果您真的想要(FluentApi 默认启用 ManyToManyCascadeDeleteConvention
),请将 sql 命令和您的 SaveChanges 包含在事务范围内。
问题 1
答案很简单:
Entity Framework 在不知道哪些属性属于关系时无法定义级联删除。
此外,在 many:many 关系中还有第三个 table,负责管理关系。这个 table 必须至少有 2 个 FK。您应该为每个 FK 配置级联删除,而不是为 "entire table".
解决方案是创建 FriendshipRelation
实体。像这样:
public class UserFriendship
{
public int UserEntityId { get; set; } // the "maker" of the friendship
public int FriendEntityId { get; set; }´ // the "target" of the friendship
public UserEntity User { get; set; } // the "maker" of the friendship
public UserEntity Friend { get; set; } // the "target" of the friendship
}
现在,您必须更改 UserEntity
。它不是 UserEntity
的集合,而是 UserFriendship
的集合。像这样:
public class UserEntity
{
...
public virtual ICollection<UserFriendship> Friends { get; set; }
}
让我们看看映射:
modelBuilder.Entity<UserFriendship>()
.HasKey(i => new { i.UserEntityId, i.FriendEntityId });
modelBuilder.Entity<UserFriendship>()
.HasRequired(i => i.User)
.WithMany(i => i.Friends)
.HasForeignKey(i => i.UserEntityId)
.WillCascadeOnDelete(true); //the one
modelBuilder.Entity<UserFriendship>()
.HasRequired(i => i.Friend)
.WithMany()
.HasForeignKey(i => i.FriendEntityId)
.WillCascadeOnDelete(true); //the one
生成的迁移:
CreateTable(
"dbo.UserFriendships",
c => new
{
UserEntityId = c.Int(nullable: false),
FriendEntityId = c.Int(nullable: false),
})
.PrimaryKey(t => new { t.UserEntityId, t.FriendEntityId })
.ForeignKey("dbo.UserEntities", t => t.FriendEntityId, true)
.ForeignKey("dbo.UserEntities", t => t.UserEntityId, true)
.Index(t => t.UserEntityId)
.Index(t => t.FriendEntityId);
检索所有用户的好友:
var someUser = ctx.UserEntity
.Include(i => i.Friends.Select(x=> x.Friend))
.SingleOrDefault(i => i.UserEntityId == 1);
所有这一切都很好。但是,该映射存在问题(这也发生在您当前的映射中)。假设 "I" 是 UserEntity
:
- 我向 John 发出了好友请求 -- John 接受了
- 我向 Ann 发出了好友请求 -- Ann 接受了
- Richard 向我提出好友请求 -- 我接受了
当我取回我的 Friends
属性 时,它 returns "John"、"Ann" 但不是 "Richard"。为什么?因为 Richard 是关系中的 "maker" 而不是我。 Friends
属性 仅绑定到关系的一侧。
好的。我该如何解决这个问题?简单!改变你的 UserEntity
class:
public class UserEntity
{
//...
//friend request that I made
public virtual ICollection<UserFriendship> FriendRequestsMade { get; set; }
//friend request that I accepted
public virtual ICollection<UserFriendship> FriendRequestsAccepted { get; set; }
}
更新映射:
modelBuilder.Entity<UserFriendship>()
.HasRequired(i => i.User)
.WithMany(i => i.FriendRequestsMade)
.HasForeignKey(i => i.UserEntityId)
.WillCascadeOnDelete(false);
modelBuilder.Entity<UserFriendship>()
.HasRequired(i => i.Friend)
.WithMany(i => i.FriendRequestsAccepted)
.HasForeignKey(i => i.FriendEntityId)
.WillCascadeOnDelete(false);
不需要迁移。
检索所有用户的好友:
var someUser = ctx.UserEntity
.Include(i => i.FriendRequestsMade.Select(x=> x.Friend))
.Include(i => i.FriendRequestsAccepted.Select(x => x.User))
.SingleOrDefault(i => i.UserEntityId == 1);
问题 2
是的,您必须迭代集合并删除所有子对象。请参阅我在该主题中的回答
根据我的回答,只需创建一个 UserFriendship
数据库集:
public DbSet<UserFriendship> UserFriendships { get; set; }
现在您可以检索特定用户id的所有好友,只需一次删除所有好友,然后删除该用户。
问题 3
是的,这是可能的。您现在有一个 UserFriendship
数据库集。
希望对您有所帮助!
我在 EntityFramework 和同一实体的多对多关系中遇到条目删除问题。考虑这个简单的例子:
实体:
public class UserEntity {
// ...
public virtual Collection<UserEntity> Friends { get; set; }
}
流畅API配置:
modelBuilder.Entity<UserEntity>()
.HasMany(u => u.Friends)
.WithMany()
.Map(m =>
{
m.MapLeftKey("UserId");
m.MapRightKey("FriendId");
m.ToTable("FriendshipRelation");
});
- 我说得对吗,无法在 Fluent API 中定义
Cascade Delete
? 删除
UserEntity
的最佳方法是什么,例如Foo
?它现在寻找我,我必须
Clear
Foo
的Friends
集合,然后我必须加载所有其他UserEntities
,其中包含Foo
inFriends
,然后从每个列表中删除Foo
,然后再从Users
中删除Foo
。但这听起来太复杂了。是否可以直接访问关系 table,以便我可以删除这样的条目
// Dummy code var query = dbCtx.Set("FriendshipRelation").Where(x => x.UserId == Foo.Id || x.FriendId == Foo.Id); dbCtx.Set("FriendshipRelation").RemoveRange(query);
谢谢!
更新01:
我知道这个问题的最佳解决方案是在调用
之前执行原始 sql 语句SaveChanges
:dbCtx.Database.ExecuteSqlCommand( "delete from dbo.FriendshipRelation where UserId = @id or FriendId = @id", new SqlParameter("id", Foo.Id));
但是这样做的缺点是,如果
SaveChanges
由于某种原因失败,FriendshipRelation
已经被删除并且无法回滚。还是我错了?
1) 我没有看到任何直接的方法来使用 FluentApi 控制多对多关系上的级联。
2) 我能想到的唯一可用的控制方法是使用 ManyToManyCascadeDeleteConvention
,我想它是默认启用的,至少对我来说是这样。我刚刚检查了我的一个迁移,包括多对多关系,确实 cascadeDelete: true
是两个键的地方。
编辑:抱歉,我刚刚发现 ManyToManyCascadeDeleteConvention
没有涵盖自引用案例。这个 related question 的回答说
You receive this error message because in SQL Server, a table cannot appear more than one time in a list of all the cascading referential actions that are started by either a DELETE or an UPDATE statement. For example, the tree of cascading referential actions must only have one path to a particular table on the cascading referential actions tree.
所以你最终不得不有一个自定义删除代码(就像你已经拥有的 sql 命令)并在 transaction scope.
中执行它3) 您不应该能够从上下文中访问 table。通常由多对多关系创建的 table 是关系 DBMS 中实现的副产品,被认为是 weak table相关的 tables,这意味着如果删除了相关实体之一,则应级联删除其行。
我的建议是,首先检查您的迁移是否将 table 外键设置为级联删除。然后,如果由于某种原因你需要限制删除在多对多关系中有相关记录的记录,那么你只需在你的事务中检查它。
4) 为了做到这一点,如果您真的想要(FluentApi 默认启用 ManyToManyCascadeDeleteConvention
),请将 sql 命令和您的 SaveChanges 包含在事务范围内。
问题 1
答案很简单:
Entity Framework 在不知道哪些属性属于关系时无法定义级联删除。
此外,在 many:many 关系中还有第三个 table,负责管理关系。这个 table 必须至少有 2 个 FK。您应该为每个 FK 配置级联删除,而不是为 "entire table".
解决方案是创建 FriendshipRelation
实体。像这样:
public class UserFriendship
{
public int UserEntityId { get; set; } // the "maker" of the friendship
public int FriendEntityId { get; set; }´ // the "target" of the friendship
public UserEntity User { get; set; } // the "maker" of the friendship
public UserEntity Friend { get; set; } // the "target" of the friendship
}
现在,您必须更改 UserEntity
。它不是 UserEntity
的集合,而是 UserFriendship
的集合。像这样:
public class UserEntity
{
...
public virtual ICollection<UserFriendship> Friends { get; set; }
}
让我们看看映射:
modelBuilder.Entity<UserFriendship>()
.HasKey(i => new { i.UserEntityId, i.FriendEntityId });
modelBuilder.Entity<UserFriendship>()
.HasRequired(i => i.User)
.WithMany(i => i.Friends)
.HasForeignKey(i => i.UserEntityId)
.WillCascadeOnDelete(true); //the one
modelBuilder.Entity<UserFriendship>()
.HasRequired(i => i.Friend)
.WithMany()
.HasForeignKey(i => i.FriendEntityId)
.WillCascadeOnDelete(true); //the one
生成的迁移:
CreateTable(
"dbo.UserFriendships",
c => new
{
UserEntityId = c.Int(nullable: false),
FriendEntityId = c.Int(nullable: false),
})
.PrimaryKey(t => new { t.UserEntityId, t.FriendEntityId })
.ForeignKey("dbo.UserEntities", t => t.FriendEntityId, true)
.ForeignKey("dbo.UserEntities", t => t.UserEntityId, true)
.Index(t => t.UserEntityId)
.Index(t => t.FriendEntityId);
检索所有用户的好友:
var someUser = ctx.UserEntity
.Include(i => i.Friends.Select(x=> x.Friend))
.SingleOrDefault(i => i.UserEntityId == 1);
所有这一切都很好。但是,该映射存在问题(这也发生在您当前的映射中)。假设 "I" 是 UserEntity
:
- 我向 John 发出了好友请求 -- John 接受了
- 我向 Ann 发出了好友请求 -- Ann 接受了
- Richard 向我提出好友请求 -- 我接受了
当我取回我的 Friends
属性 时,它 returns "John"、"Ann" 但不是 "Richard"。为什么?因为 Richard 是关系中的 "maker" 而不是我。 Friends
属性 仅绑定到关系的一侧。
好的。我该如何解决这个问题?简单!改变你的 UserEntity
class:
public class UserEntity
{
//...
//friend request that I made
public virtual ICollection<UserFriendship> FriendRequestsMade { get; set; }
//friend request that I accepted
public virtual ICollection<UserFriendship> FriendRequestsAccepted { get; set; }
}
更新映射:
modelBuilder.Entity<UserFriendship>()
.HasRequired(i => i.User)
.WithMany(i => i.FriendRequestsMade)
.HasForeignKey(i => i.UserEntityId)
.WillCascadeOnDelete(false);
modelBuilder.Entity<UserFriendship>()
.HasRequired(i => i.Friend)
.WithMany(i => i.FriendRequestsAccepted)
.HasForeignKey(i => i.FriendEntityId)
.WillCascadeOnDelete(false);
不需要迁移。
检索所有用户的好友:
var someUser = ctx.UserEntity
.Include(i => i.FriendRequestsMade.Select(x=> x.Friend))
.Include(i => i.FriendRequestsAccepted.Select(x => x.User))
.SingleOrDefault(i => i.UserEntityId == 1);
问题 2
是的,您必须迭代集合并删除所有子对象。请参阅我在该主题中的回答
根据我的回答,只需创建一个 UserFriendship
数据库集:
public DbSet<UserFriendship> UserFriendships { get; set; }
现在您可以检索特定用户id的所有好友,只需一次删除所有好友,然后删除该用户。
问题 3
是的,这是可能的。您现在有一个 UserFriendship
数据库集。
希望对您有所帮助!