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");
    });
  1. 我说得对吗,无法在 Fluent API 中定义 Cascade Delete
  2. 删除 UserEntity 的最佳方法是什么,例如 Foo

    它现在寻找我,我必须 Clear FooFriends 集合,然后我必须加载所有其他 UserEntities,其中包含 Foo in Friends,然后从每个列表中删除 Foo,然后再从 Users 中删除 Foo。但这听起来太复杂了。

  3. 是否可以直接访问关系 table,以便我可以删除这样的条目

    // Dummy code
    var query = dbCtx.Set("FriendshipRelation").Where(x => x.UserId == Foo.Id || x.FriendId == Foo.Id);
    dbCtx.Set("FriendshipRelation").RemoveRange(query);
    

谢谢!

更新01:

  1. 我知道这个问题的最佳解决方案是在调用 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 数据库集。

希望对您有所帮助!