Entity Framework Core 删除包含已读取实体的实体

Posted

技术标签:

【中文标题】Entity Framework Core 删除包含已读取实体的实体【英文标题】:EntityFramework Core deletes entity with included entites that has been read 【发布时间】:2021-12-03 15:08:28 【问题描述】:

由于某种原因,当我从数据库中读取数据并在数据库上下文中创建 SaveChangesAsync() 时,已加载与其关系的实体将从数据库中删除。

Category tools = new("Tools");
ProductType p = new("Hammer", 25, tools);
Ware w1 = new("S1", p, new("Floor"));
Ware w2 = new("S2", p, new("Floor"));
Ware w3 = new("S3", p, new("Floor"));
Ware w4 = new("S4", p, new("Floor"));
Ware w5 = new("S5", p, new("Floor"));
p.AddWare(w1);
p.AddWare(w2);
p.AddWare(w3);
p.AddWare(w4);
p.AddWare(w5);

unitOfWork.ProductTypeRepository.Create(p);

ProductType selected = unitOfWork.ProductTypeRepository.GetByIdAsync(46).Result;
ProductType selected2 = unitOfWork.ProductTypeRepository.GetByIdAsyncWithRelationships(50).Result;
Console.WriteLine("____");
var done = unitOfWork.SaveChangesAsync().Result;
Console.WriteLine(done);

上面的区别是selected中的数据没有被删除,而selected2中的数据是。

两种方法如下

public async Task<ProductType> GetByIdAsync(int id)
        
            return await _shopDbContext.ProductTypes.SingleOrDefaultAsync(p => p.ProductTypeId == id);
        

public async Task<ProductType> GetByIdAsyncWithRelationships(int id)
        
            return await _shopDbContext.ProductTypes
                .Include(p => p.OfferProductTypes)
                    .ThenInclude(op => op.Offer)
                .Include(p => p.Wares)
                .Include(p => p.Category)
                .SingleOrDefaultAsync(p => p.ProductTypeId == id);
        

从我的 MSSQL 数据库服务器分析器中,我可以看到以下内容

exec sp_executesql N'SET NOCOUNT ON;
DELETE FROM [Ware]
WHERE [WareId] = @p0;
SELECT @@ROWCOUNT;

DELETE FROM [Ware]
WHERE [WareId] = @p1;
SELECT @@ROWCOUNT;

DELETE FROM [Ware]
WHERE [WareId] = @p2;
SELECT @@ROWCOUNT;

DELETE FROM [Ware]
WHERE [WareId] = @p3;
SELECT @@ROWCOUNT;

DELETE FROM [Ware]
WHERE [WareId] = @p4;
SELECT @@ROWCOUNT;

',N'@p0 int,@p1 int,@p2 int,@p3 int,@p4 int',@p0=246,@p1=247,@p2=248,@p3=249,@p4=250

exec sp_executesql N'SET NOCOUNT ON;
DELETE FROM [ProductTypes]
WHERE [ProductTypeId] = @p5;
SELECT @@ROWCOUNT;

',N'@p5 int',@p5=50

同时我在 UnitOfWork 的 SaveChangesAsync() 中的代码

public Task<int> SaveChangesAsync()

    var ct = _context.ChangeTracker;
    foreach(var e in ct.Entries())
    
        Console.WriteLine(e.Entity.GetType().Name + " : " +e.State);
    
    return _context.SaveChangesAsync();

显示以下内容

____
ProductType : Unchanged
0

它甚至不表示 ProductType 已与其商品和类别一起添加。数据库已更新。

但是,有时它确实会显示更改

ProductType : Added
Category : Added
Ware : Added
Location : Added
Ware : Added
Location : Added
Ware : Added
Location : Added
Ware : Added
Location : Added
Ware : Added
Location : Added
ProductType : Deleted
Ware : Deleted
Ware : Deleted
Ware : Deleted
Ware : Deleted
Ware : Deleted
ProductType : Unchanged
Category : Unchanged

这里是 MCVE 形式的 git 存储库 https://github.com/BenjaminElifLarsen/Shopping---MCVE 的链接。这些文件在 ShoppingCli 中是 program,在 Ipl.Repositories 中是存储库,在 Ipl.Services 中是 UnitOfWork

----- 更新 ----- 我刚刚尝试绕过我的工作单元并直接调用数据库上下文,并且使用 Include() 和 ThenInclude() 与相关实体一起加载的任何实体都将被删除。

【问题讨论】:

你能把它变成一个 MCVE 吗?我很高兴尝试在我的机器上运行它,看看我是否可以复制它;这可能是一个应该反馈给 EF 团队的错误。 总结我的回答的评论链:我怀疑您的运行时中还有其他线程正在运行导致删除。您显示的最后一个 sn-p 包含 3 条 ProductType 消息,并且 EF 上下文只能在给定实体的单个 SaveChanges 调用中执行一个操作,因此这些 ProductType 消息不可能引用同一实体相同的上下文和相同的SaveChanges 调用了三次。它可能有助于记录更多信息,例如跟踪器中每个实体的 JSON 序列化转储,有助于解释何时引用了哪个实体。 @CaiusJard 我会尽快把它变成一个 MCVE,这不是我最有经验的东西,所以可能需要一段时间。 @Flater 我将尝试添加更多日志记录。关于其他线程的可能性,应该只有一个线程在运行。 你真的必须把它变成minimal reproducible example。您的所有代码都在外部源中。大多数人会忽略您的问题,而且,如果外部来源发生变化,问题将毫无用处。 【参考方案1】:

您发布的代码都没有涉及删除操作,所以我不确定 DELETE 语句的来源,但它不在提供的代码中。我还认为你在试图解释为什么 selectedselected2 是不同的。

问题不在于您的实体已被“删除”。问题是其中一个查询可以使用更改跟踪器的缓存 (selected) 来回答,而另一个不能 (selected2)。

简而言之,EF 会缓存您使用的实体。无论您是附加一个新实体还是从数据库中获取一个实体,EF 都会根据其类型 + PK 值对其进行跟踪。如果你再次请求它,那么 EF 会给你缓存的版本。

unitOfWork.ProductTypeRepository.Create(p);

您已添加产品类型,但尚未保存更改,因此,添加的产品类型暂时存在于更改跟踪器(= 缓存)中,但尚未保存在数据库中。

_shopDbContext.ProductTypes.SingleOrDefaultAsync(p => p.ProductTypeId == id)

在这里,在您的第一个查询中,您根据实体的类型和 PK 值询问您的实体(并且仅此实体)。 EF 检查它的缓存,并注意到它已经缓存了这个实体。它是您在上一步中添加(但尚未保存)的实体。因此,EF 会返回对它已经知道的缓存实体的引用。 EF 甚至从不费心与数据库对话。

_shopDbContext.ProductTypes
    .Include(p => p.OfferProductTypes)
      .ThenInclude(op => op.Offer)
    .Include(p => p.Wares)
    .Include(p => p.Category)
    .SingleOrDefaultAsync(p => p.ProductTypeId == id);

在这里,在您的第二个查询中,您要求的不仅仅是您的主要实体。您正在查询其所有相关实体。

这本质上不是变更跟踪器可以解释的。即使它知道属于该产品类型的多个实体(例如商品),它(a) 也不能确定它知道所有 相关商品(可能有更多在数据库中),并且 (b) 必须根据产品类型 FK 查找类别,而不是 ware PK,这不是 EF 缓存构建的目的。

因此,EF 必须始终访问数据库以获取您的数据。并且 EF 不执行混合方法,它使用一些缓存和一些真实数据。它总是一个或另一个。由于 一些 数据需要从实际数据库中获取,因此 所有 请求的数据正在从数据库中获取。

因为 EF 现在正在访问数据库,所以数据库不返回任何内容,因为您从未将新产品类型保存到数据库中(还)。因此,EF 告诉您此产品类型不存在,因为数据库正确地声明它不存在(在数据库中)。

【讨论】:

只是试图合理化问题中呈现的内容,以及答案中呈现的内容 - 您似乎是在说创建一个实体,将其添加到上下文中,然后查询上下文一秒钟(不同的)实体(具有不同的 PK)将导致来自第二个实体的相关数据被删除,这是预期的吗? @CaiusJard:你好像漏掉了第一段。问题中提供的代码不包括代表 EF 的任何删除操作,因此我无法解释是谁触发了这些删除。我只能说这不是因为提供的代码。但是,selected2 出现的问题似乎不包含selected 确实包含的数据,这不是由删除操作引起的。 EF 如何将其更改跟踪器作为未保存更改的缓存和用于检索实体的缓存来处理,已经解释了这一点。 如果我不向数据库添加任何数据,问题仍然存在。如果我只是在数据库中查询其中的两个实体,则加载了相关实体的那个将始终被删除。程序正在接收实体及其相关实体。 好的,但即使 Ben 在从数据库中提取数据时使用的所有 ID 也是不同的。我无法弄清楚答案如何与问题相符。这是有用的信息,但投诉似乎本质上是“我添加了 ID 为 25 的实体,查询了 ID 为 46 的实体,查询了实体 50 并且所有 50 的相关数据都被删除了 - 为什么?”.. 我不确定是什么问题回答答案,但添加一个 ent 会导致 CT 内存中的另一个被删除似乎出乎意料 @CaiusJard:那么有两个问题同时发生。一个我可以根据提供的代码解释,一个我不能。我回答了我能回答的那个。我从来没有声称删除没有发生,只是提供的代码不是执行删除的代码。 EF 不会删除对象,除非得到指示(通过Remove、取消引用交叉连接或迁移),这些都不会在提供的代码中发生。【参考方案2】:

好的,所以这需要深入研究,但我相信我已经得出了合乎逻辑的解释:

您的数据库实体有点不稳定,并且行为方式 EF 无法容忍

这是正常情况下发生的情况(这不是 EF 所做的确切代码,为简单起见,这是一种伪解释):

您调用一个查询,EF 运行它,将接收到的数据转换为对象并将它们连接到一个图表中。您的查询本质上是这样的:

        await ctx.ProductTypes
            .Include(p => p.OfferProductTypes)
            .Include(p => p.Wares)
            .Include(p => p.Category)
            .SingleOrDefaultAsync(p => p.ProductTypeId == 4);

EF 会生成一个ProductType,EF 会生成一个Category,EF 会生成5 个Wares。 CategoryProductType 的父级,ProductTypeWare 的父级

在创建 ProductTypeCategory 之后,EF 希望在 ProductTypeCategory 之间建立双向关系,因此它设置了 theProductType.Category = theCategory,并且因为它是 many:1 (pt:c)保存 ProductTypes 和集合 theCategory.ProductTypes = theCollectionOfProductTypes 的集合。然后它用它知道的所有 ProductType 实例填充theCollectionOfProductTypes

在伪代码术语中,EF 是这样做的:

ProductType theProductType = get(...) //gets product type 4, we don't care so much how
Category theCategory = get(...) //gets the category, we don't care so much how

List<ProductType> theCollectionOfProductTypes = new(); //it's probably not a list, but it doesn't matter

theProductType.Category = theCategory;
theCategory.ProductTypes = theCollectionOfProductTypes;

theCollectionOfProductTypes.Add(theProductType);

到目前为止和我在一起?问题来了:

当 EF 为您的 Category 提供它创建的 ProductTypes 集合时,您从中创建了另一个(单独的)集合:

    public IEnumerable<ProductType> ProductTypes  
      get => _productType; 
      private set => _productType = value.ToHashSet(); 
    

跳过使这个 IEnumerable 成为一个坏主意的部分,因为您无法写入 IEnumerable(这使得将 ProductTypes 添加到您的类别变得更加困难)并直截了当地找到 ToHashSet 它,您'已经将你的图表与 EF 的现实脱节。如果我们添加一点来保存 EF 给你的东西:

    public IEnumerable<ProductType> ProductTypes 
        get => _productType;
        private set  
            _productType = value.ToHashSet();
            _whatEfHolds = value;
        
    
    private object _whatEfHolds;

您可以看到您的类别的“产品类型列表”有 0 个项目,而在连接图表时填充的一个 EF 有 1 个项目(我们下载的 ProductType id 4)

一切都会好起来的,直到我们要求 EF 检测您的图表与其自己的图表之间的变化;当它拉出您的 Category 的 ProductTypes 属性时,它将收到 0 个实体并假设您已将它们删除:

dbug: 2021-10-20 12:34:56.789 CoreEventId.CollectionChangeDetected[10804] (Microsoft.EntityFrameworkCore.ChangeTracking)
      0 entities were added and 1 entities were removed from navigation 'Category.ProductTypes' on entity with key 'CategoryId: 4'.
dbug: 2021-10-20 12:34:56.789 CoreEventId.StateChanged[10807] (Microsoft.EntityFrameworkCore.ChangeTracking)
      The 'ProductType' entity with key 'ProductTypeId: 4' tracked by 'ShopDbContext' changed state from 'Unchanged' to 'Modified'.
dbug: 2021-10-20 12:34:56.789 CoreEventId.CascadeDeleteOrphan[10003] (Microsoft.EntityFrameworkCore.Update)
      An entity of type 'ProductType' with key 'ProductTypeId: 4' changed to 'Deleted' state due to severed required relationship to its parent entity of type 'Category'.

EF 将您的 ProductType 标记为已删除,然后也将其级联到商品

该怎么办?就我个人而言,我认为您的数据库中有太多的逻辑和混乱;它们应该是更简单的事务,只保存数据,并且可能在部分类中具有一些用于稍微高级逻辑的方法(但可能其中很多会被放入您的映射器中)。 Category 应该看起来更像这样(注意,它被称为 TestCategories 因为我是从 DB 中获取它的,这就是 DB 表的名称):

public partial class TestCategories

    public TestCategories()
    
        ProductTypes = new HashSet<ProductTypes>();
    

    public int CategoryId  get; set; 
    public string Name  get; set; 

    public virtual ICollection<ProductTypes> ProductTypes  get; set; 

这样的 ProductType:

public partial class ProductTypes

    public ProductTypes()
    
        OfferProductType = new HashSet<OfferProductType>();
        Ware = new HashSet<Ware>();
    

    public int ProductTypeId  get; set; 
    public string Type  get; set; 
    public int Price  get; set; 
    public int CategoryId  get; set; 

    public virtual TestCategories Category  get; set; 
    public virtual ICollection<OfferProductType> OfferProductType  get; set; 
    public virtual ICollection<Ware> Ware  get; set; 

但主要的收获;小心/避免在 EF 在填充对象图时提供给您的 ents 上做任何事情(例如 ToHashSet

【讨论】:

解决了这个问题。非常感谢您的帮助。我使用 HashSet 的原因是我读过这是防止实体重复的好主意,但现在我知道不要使用它。 不是不使用HashSet; EF 自己的脚手架类使用它/EF 给你的集合是一个 HashSet。问题是因为您使用了它给您的哈希集,并制作了另一个哈希集,这在概念上等同于 Clear() 在保存之前对其进行处理(这会断开关系,从而使 EF 删除记录)。如果您调用 ToList() 或任何其他导致集合的另一个实例(与 EF 构建的集合分开),您会遇到同样的问题 要开始使用 EF 并了解如何最好地塑造您的实体,首先创建 DB(任何 DB)可能是明智的,然后让 EF 从 DB 中构建实体,然后看看它是如何建造它们的。 尽量不要偏离该模式太多 - 让您的数据库实体保持简单。如果他们需要用神秘的逻辑以奇怪的方式填充,把这个逻辑放在一些外部映射类中,这样你的实体图就很容易理解和解释,以免你最终遇到和这里一样的情况 我提到的代码中的另一件事 - 您已将持有者变量设为 IEnumerable&lt;whatever&gt;,然后您在其中存储了一个哈希集 - 您可以这样做那是因为哈希集是可枚举的,但是在添加实体方面,使用它会更加痛苦;你不能说myCategory.ProductTypes.Add(someProductType),因为你不能添加到可枚举中。你必须记住它是一个 HashSet 并转换它:(myCategory.ProductTypes as HashSet&lt;ProductType).Add(someProductType) “我每次都可以这样做”您可能会说,但这就像将所有内容保存在 object 中并在每次您想使用它时投射它:object o = new StringBuilder(); (o as StringBuilder).Append("..."); - 没有人这样做;他们只是使用通常更容易使用的表面类型。如果你看看我放在最后的两个类 - 我没有写那些,EF 写了它们,所以你可以将它们视为“EF 团队声称易于使用的模式”。跨度>

以上是关于Entity Framework Core 删除包含已读取实体的实体的主要内容,如果未能解决你的问题,请参考以下文章

在 Entity Framework Core 中重命名外键而不删除数据

Entity Framework Core 迁移命令

Entity Framework Core中更改跟踪工作原理

Entity Framework Core中更改跟踪工作原理

使用 Key Vault 和包管理器控制台为 Entity Framework Core 连接和数据库迁移

Entity Framework Core 7中高效地进行批量数据插入