EntityFramework CodeFirst:同一张表的多对多关系CASCADE DELETE

13
我有一个使用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");
    });
  1. Am I correct, that it is not possible to define the Cascade Delete in Fluent API?
  2. What is the best way to delete a UserEntity, for instance Foo?

    It looks for me now, I have to Clear the Foo's Friends Collection, then I have to load all other UserEntities, which contain Foo in Friends, and then remove Foo from each list, before I remove Foo from Users. But it sounds too complicateda.

  3. Is it possible to access the relational table directly, so that I can remove entries like this

    // Dummy code
    var query = dbCtx.Set("FriendshipRelation").Where(x => x.UserId == Foo.Id || x.FriendId == Foo.Id);
    dbCtx.Set("FriendshipRelation").RemoveRange(query);
    
谢谢! 更新01:
  1. My best solution for this problem for know is just to execute the raw sql statement before I call SaveChanges:

    dbCtx.Database.ExecuteSqlCommand(
        "delete from dbo.FriendshipRelation where UserId = @id or FriendId = @id",
        new SqlParameter("id", Foo.Id));
    

    But the disadvantage of this, is that, if SaveChanges failes for some reason, the FriendshipRelation are already removed and could not be rolled back. Or am I wrong?


级联删除应该在你的迁移文件中定义,而不是在你的 Fluent 映射中。 - Ben Robinson
@BenRobinson,据我所知,您可以使用Fluent API为“多对一”关系定义级联删除,并使用“WillCascadeOnDelete”方法。 - tenbits
@BenRobinson 那是非常误导人的,当然级联删除可以在流畅的 API 中进行配置。 - user2697817
你的SQL语句在update1中是否正常工作?我的意思是,如果你有一个ID为57的朋友,而其他人也有一个FriendId为57且ID为11的朋友,再加上另一个人(Mr x)有FriendId为11的朋友;那么当你写“delete from dbo.FriendshipRelation where UserId = 57 or FriendId = 57”时,Mr x将会失去一个不存在的朋友。我说得对吗? - Arash
@Arashjo,实际上不是这样的,如果我们删除一个ID为57的用户。这意味着我们必须在FriendshipRelation中删除与此ID的所有关系。也就是说,我们删除所有与UserId = 57的用户的好友,并删除所有其他用户与此用户的关系,其中FriendId = 57。换句话说,我们删除了我所有的朋友,并删除了我作为朋友的所有关系。 - tenbits
2个回答

13

问题一

答案非常简单:

当Entity Framework不知道哪些属性属于关系时,它无法定义级联删除。

此外,在多对多关系中,有一个第三个表负责管理关系。该表必须至少有两个外键。您应为每个外键配置级联删除,而不是为“整个表”配置级联删除。

解决方案是创建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);

所有这些都很好,但是有一个问题存在于映射中(这在您当前的映射中也会发生)。假设“我”是一个UserEntity:
  • 我向约翰发送了好友请求,约翰接受了
  • 我向安妮发送了好友请求,安妮接受了
  • 理查德向我发送了好友请求,我接受了
当我检索我的Friends属性时,它返回“John”,“Ann”,但不返回“Richard”。为什么?因为Richard是关系的“制造者”,而非我。 Friends属性仅绑定到关系的一侧。
好的。我该怎么解决?很容易!更改您的UserEntity类:
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 dbset:

public DbSet<UserFriendship> UserFriendships { get; set; }

现在您可以检索特定用户ID的所有好友,一次性删除所有好友,然后删除该用户。
问题3
是的,这是可能的。您现在拥有一个UserFriendship数据库集。
希望对您有所帮助!

谢谢,Fabio。看起来使用另一个表确实比自引用更好,这样我们可以更好地控制实体关系。 - tenbits
欢迎,@tenbits。如果这篇帖子解决了你的问题,请记得将其标记为答案,这样可以帮助未来遇到同样问题的人。 - Fabio Luz

1

1) 我没有看到使用FluentApi控制多对多关系的级联的直接方法。

2) 我所能想到的唯一可用的控制方式是使用ManyToManyCascadeDeleteConvention,我猜它默认是启用的,至少对我来说是这样。我刚刚检查了一个包含多对多关系的迁移,确实cascadeDelete:true对于两个键都存在。

编辑:对不起,我刚刚发现ManyToManyCascadeDeleteConvention不能覆盖自引用情况。这个相关问题的答案说:

您会收到此错误消息,因为在SQL Server中,表不能在由DELETE或UPDATE语句启动的所有级联引用操作列表中出现超过一次。例如,级联引用操作树上到特定表的路径必须只有一条。

因此,您最终需要有一个自定义的删除代码(例如您已经拥有的SQL命令),并在事务范围内执行它。

3)您不应该能够从上下文访问该表。通常,由多对多关系创建的表是关系型数据库管理系统中实现的副产品,并且相对于相关表而言被认为是表,这意味着如果删除其中一个相关实体,则其行应该被级联删除。

我的建议是,首先检查您的迁移是否将表外键设置为级联删除。然后,如果由于某种原因您需要限制在多对多关系中具有相关记录的记录的删除,则只需在事务中进行检查即可。

4)为了做到这一点,如果您真的想要(FluentApi默认启用ManyToManyCascadeDeleteConvention),则需要在SQL命令和SaveChanges中包含事务范围。


谢谢 @Green Magic,您关于混合使用 raw SQLLinqtransition scope 的建议非常有用。 - tenbits

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接