【问题标题】:EntityFramework CodeFirst: CASCADE DELETE for same table many-to-many relationship实体框架代码优先:同一张表多对多关系的 CASCADE DELETE
【发布时间】:2023-04-04 13:17:01
【问题描述】:

我遇到了 EntityFramework 的条目删除问题以及同一实体的多对多关系。考虑这个简单的例子:

实体:

public class UserEntity {
    // ...
    public virtual Collection<UserEntity> Friends { get; set; }
}

Fluent 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)的最佳方法是什么?

    它现在找我,我要ClearFooFriends 集合,然后我必须加载所有其他UserEntities,其中包含Foo 中的Foo,然后删除Foo 从每个列表中,在我从 Users 中删除 Foo 之前。但这听起来太复杂了。

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

    // 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 已经被删除并且无法回滚。还是我错了?

【问题讨论】:

  • 级联删除是在您的迁移中定义的,而不是在您的流利映射中。
  • @BenRobinson,嗯,据我所知,您可以使用 WillCascadeOnDelete 方法在 Fluent API 中为 many:one 关系定义级联删除。
  • @BenRobinson 这很误导,当然可以在fluent API中配置级联删除
  • 您的 sql 语句在您的 update1 中是否正常工作?我的意思是,如果您有 id=57 的朋友,而其他人有朋友 FriendId=57 和 id=11,而其他人(x 先生)也有 FriendId =11;那么当你写“从 dbo.FriendshipRelation 中删除 UserId = 57 或 FriendId = 57”时。 x先生有一个不存在的朋友。我说的对吗?
  • @Arashjo,实际上不是,如果我们删除 ID 为 57 的用户。这意味着我们必须删除 FriendshipRelation 中与此 ID 的所有关系。这意味着,我们会删除所有与UserId = 57 为好友的用户,以及与此用户与FriendId = 57 的所有其他用户 关系。换句话说,我们删除了我所有的朋友,并删除了我是朋友的所有关系。

标签: c# entity-framework ef-code-first entity-framework-6 code-first


【解决方案1】:

问题 1

答案很简单:

实体框架在不知道哪些属性属于关系时无法定义级联删除。

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

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

  • 我向 John 提出了好友请求 -- John 接受了
  • 我向 Ann 提出了好友请求 -- Ann 接受了
  • Richard 向我提出了好友请求 -- 我接受了

当我检索我的Friends 属性时,它返回“John”、“Ann”,但不返回“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

是的,您必须迭代集合并删除所有子对象。在此线程中查看我的答案Cleanly updating a hierarchy in Entity Framework

按照我的回答,只需创建一个UserFriendship dbset:

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

现在您可以检索特定用户ID的所有朋友,只需一次性删除所有朋友,然后删除该用户。

问题 3

是的,这是可能的。你现在有一个UserFriendship dbset。

希望对你有帮助!

【讨论】:

  • 谢谢,Fabio,使用一个表而不是自引用似乎确实更好,这样我们就可以更好地控制实体关系。
  • 不客气,@tenbits。如果它解决了您的问题,请记住将帖子标记为答案。所以,你将来会帮助人们。
【解决方案2】:

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

2) 我能想到的唯一可用方法是使用ManyToManyCascadeDeleteConvention,我猜这是默认启用的,至少对我来说是这样。我刚刚检查了我的一个迁移,包括多对多关系,实际上cascadeDelete: true 是两个键。

编辑:对不起,我刚刚发现ManyToManyCascadeDeleteConvention 不包括自引用案例。这个related question 的回答说

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

所以你最终不得不有一个自定义的删除代码(就像你已经拥有的 sql 命令)并在 transaction scope 中执行它。

3) 您应该无法从上下文中访问该表。通常由多对多关系创建的表是关系 DBMS 中实现的副产品,相对于相关表被认为是 weak 表,这意味着它的行应该是如果删除了相关实体之一,则级联删除。

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

4) 为了做到这一点,如果你真的想要(FluentApi 默认启用ManyToManyCascadeDeleteConvention),就是将 sql 命令和你的 SaveChanges 包含在事务范围内。

【讨论】:

  • 谢谢@Green Magic,根据transition scope 混合raw SQLLinq 的建议很有用。
猜你喜欢
  • 1970-01-01
  • 2013-02-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-03-14
  • 1970-01-01
  • 2011-07-20
相关资源
最近更新 更多