简体   繁体   English

EF Core Attach/AttachRange 方法不起作用

[英]EF Core Attach/AttachRange methods not working

I 'm using EF Core 3.1.10.我正在使用 EF Core 3.1.10。 I have the following entities:我有以下实体:

public class Request {
    public int Id { get; set; }
    public string Title { get; set; }
    public string ProjectId { get; set; }
    public List<RequestAttachment> Attachments { get; set; } = new List<RequestAttachment> ();
}

public class RequestAttachment {
    public int Id { get; set; }
    public int RequestId { get; set; }
    public Request Request { get; set; }
    public byte[] FileStream { get; set; }
    public string Filename { get; set; }
    public RequestAttachmentType RequestAttachmentType { get; set; }
    public int RequestAttachmentTypeId { get; set; }
}

public class RequestAttachmentType {
    public int Id { get; set; }
    public string Name { get; set; }
}

In my repository, I have a simple Update method:在我的存储库中,我有一个简单的更新方法:

    public async Task UpdateRequest (Request aRequest) {
        // I'm attaching aRequest.Attachments because they already exist in the database and I don 't want to update them here
        // Option 1 Not working
        // aRequest.Attachments.ForEach (a => theContext.RequestAttachments.Attach (a));
    
        // Option 2 Not working
        // theContext.RequestAttachments.AttachRange (aRequest.Attachments);
    
        // Option 3 Working
        aRequest.Attachments.ForEach (a => theContext.Entry (a).State = EntityState.Unchanged);
    
        theContext.Requests.Update(aRequest);
        await theContext.SaveChangesAsync ();
    }

Note that I'm attaching "aRequest.Attachments" because I don 't want to update Attachments.请注意,我附加了“aRequest.Attachments”,因为我不想更新附件。 I only want to update aRequest.我只想更新一个请求。 "aRequest.Attachments" already exist in the database that's why I 'm using Attach so they don't get re-added. “aRequest.Attachments”已经存在于数据库中,这就是我使用 Attach 的原因,因此它们不会被重新添加。 But Attach and AttachRange do not work when a request has more than one attachment.但是当一个请求有多个附件时,Attach 和 AttachRange 不起作用。 It throws the following error:它引发以下错误:

The instance of entity type 'RequestAttachmentType' cannot be tracked because another instance with the key value '{Id: 1}' is already being tracked.无法跟踪实体类型“RequestAttachmentType”的实例,因为已在跟踪另一个具有键值“{Id: 1}”的实例。 When attaching existing entities, ensure that only one entity instance with a given key value is attached.附加现有实体时,请确保仅附加一个具有给定键值的实体实例。

I don 't understand this error because I did not explicitly attach "RequestAttachmentType".我不明白这个错误,因为我没有明确附加“RequestAttachmentType”。 The only thing I did was attaching its parent "aRequest.Attachments".我唯一做的就是附加它的父级“aRequest.Attachments”。

When I set the state manually like I did in Option 3, no error was thrown.当我像在选项 3 中那样手动设置 state 时,没有引发错误。 I thought Attach is equivalent to theContext.Entry (a).State = EntityState.Unchanged.我认为 Attach 相当于 theContext.Entry (a).State = EntityState.Unchanged。 Why option 3 works but option 1 and 2 do not?为什么选项 3 有效,但选项 1 和 2 无效?

Try this:尝试这个:


var itemExist = await theContext.Requests.FirstOrDefaultAsync ( i=>i.Id == aRequest.Id);
 
 if (itemExist !=null)
{
var attachments=aRequest.Attachments;
aRequest.Attachments=null;
  theContext.Entry(itemExist ).CurrentValues.SetValues(aRequest);
await theContext.SaveChangesAsync();
aRequest.Attachments=attachments;
}


Working with detached entity graphs is going to continue to cause all kinds of headaches like this.使用分离的实体图将继续引起各种头痛。 Not only do you need to handle the scenario that you don't want to update/duplicate related entities, but you have to also handle cases where the DbContext is already tracking the entity you want to update.您不仅需要处理不想更新/复制相关实体的情况,还必须处理 DbContext 已经在跟踪要更新的实体的情况。 Sergey was on the right track there.谢尔盖在那里走上了正确的道路。

The problem is that you have a complete graph:问题是你有一个完整的图表:

  • Request要求
    • Atachment附件
      • AttachmentType附件类型
    • Attachment附件
      • AttachmentType附件类型

where you want to update details in Request and the Attachments...您要在请求和附件中更新详细信息的位置...

One issue with "Update" is that it will dive the graph to look for entities that might need to be added/updated. “更新”的一个问题是它会深入图表以查找可能需要添加/更新的实体。 On its own with a detached graph this will usually result in duplicate items being created.单独使用分离图通常会导致创建重复的项目。 Hence "attaching" them first.因此,首先“附加”它们。 The trouble here is where the DbContext is already tracking one or more entities in the graph.这里的问题是 DbContext 已经在跟踪图中的一个或多个实体。 One key detail to remember about EF is that References are everything.关于 EF 要记住的一个关键细节是引用就是一切。 Deserializing entity graphs is a painful exercise.反序列化实体图是一项痛苦的练习。

For example lets say we deserialize a Request Id 1, with 2 attachments, #1, and #2, where both have an AttachmentType of "Document" (AttachmentType ID = 14)例如,假设我们反序列化带有 2 个附件 #1 和 #2 的请求 ID 1,其中两个附件的附件类型均为“文档”(附件类型 ID = 14)

What you will end up is something that looks like:你最终会得到如下所示的东西:

Document 
{
    ID:1 
    ...
    Attachments
    {
        Attachment 
        {     
            ID:1 
            ...
            AttachmentType
            {
                ID: 14
            }
        }
        Attachment 
        {     
            ID:2 
            ...
            AttachmentType
            {
                ID: 14
            }
        }
    }
}

Without considering what the DbContext may or may not already be tracking prior to looking at these entities, there is already a problem.在查看这些实体之前,如果不考虑 DbContext 可能跟踪或未跟踪的内容,则已经存在问题。 Attachment ID 1 and 2 are distinct objects, however they both reference an AttachmentType ID 14. When de-serialized, these will be 2 completely distinct references to objects that have an ID of 14.附件 ID 1 和 2 是不同的对象,但是它们都引用 AttachmentType ID 14。反序列化时,这些将是对 ID 为 14 的对象的 2 个完全不同的引用。

A common surprise is where test code appears to work fine because the two attachments had different attachment types, but then fails unexpectedly when they happen to have the same type.一个常见的惊喜是测试代码似乎可以正常工作,因为两个附件具有不同的附件类型,但是当它们碰巧具有相同类型时会意外失败。 The first attachment would have the DbContext tracking the first attachment's "Type".第一个附件将使 DbContext 跟踪第一个附件的“类型”。 If the second attachment's Type was a different ID, then attaching that 2nd type would succeed so long as the Context wasn't tracking it.如果第二个附件的类型是不同的 ID,那么只要上下文不跟踪它,附加第二个类型就会成功。 However, when set to the same ID the "already tracking entity with the same ID" pops up.但是,当设置为相同 ID 时,会弹出“已在跟踪具有相同 ID 的实体”。

When dealing with disconnected entities you need to be very deliberate about references and explicitly handle whenever the DbContext is tracking a reference.在处理断开连接的实体时,您需要非常谨慎地处理引用,并在 DbContext 跟踪引用时显式处理。 This means consulting the DbSet Local caches:这意味着咨询 DbSet Local缓存:

public async Task UpdateRequest (Request aRequest) 
{
    var existingRequest = theContext.Requests.Local.SingleOrDefault(x => x.Id = aRequest.Id);
    if (existingRequest != null)
    {
        // copy values from aRequest -> existingRequest or Leverage something like automapper.Map(aRequest, existingRequest)
    }
    else
    {  
       theContext.Requests.Attach(aRequest);
       theContext.Entity(aRequest).State = EntityState.Modified; // Danger Will Robinson, make 100% sure your entity from client is validated!! This overwrites everything.
    }
    
    foreach(var attachment in aRequest)
    {
        var existingAttachment = theContext.Attachments.Local.SingleOrDefault(x => x.Id == attachment.Id);
        // Look for a reference to the attachment type. If found, use it, if not attach and use that...
        var existingAttachmentType = theContext.AttachmentTypes.Local.SingleOrDefault(x => x.Id == attachment.AttachmentType.Id);
        if (existingAttachmentType == null)
        {  
            theContext.AttachmentTypes.Attach(attachment.AttachmentType);
            existingAttachmentType = attachment.AttachmentType;
        }

        if(existingAttachment != null)
        {
             // copy values across.
             AttachmentType = existingAttachmentType; // in case we change the attachment type for this attachment.
        }
        else
        {
            theContext.Attachments.Attach(attachment);
            theContext.Entity(attachment).State = EntityState.Modified;
            attachment.AttachmentType = existingAttachmentType;
        }   
    }              
    await theContext.SaveChangesAsync ();
}

Needless to say this is a lot of messing around to check and replace references to either get the DbContext to track detached entities or replace the references with tracked entities.不用说,检查和替换引用以获取 DbContext 以跟踪分离的实体或用跟踪的实体替换引用是很多麻烦。

A simpler option is to leverage Automapper to establish a configuration for what fields can be updated from a source (ideally a ViewModel, but you can use an entity graph as a source) to a destination.一个更简单的选择是利用 Automapper 为可以从源(理想情况下为 ViewModel,但您可以使用实体图作为源)更新到目标的字段建立配置。 (Entities tracked by the DbContext) (由 DbContext 跟踪的实体)

Step 1: Configure Automapper with the rules about what to update for a Request -> Attachments graph.. (Not shown)第 1 步:配置 Automapper 并使用有关更新请求内容的规则 -> 附件图..(未显示)

Step 2: Load tracked entity graph, and the applicable AttachmentTypes:第 2 步:加载跟踪的实体图和适用的 AttachmentType:

var existingRequest = theContext.Requests
    .Include(x => x.Attachments)
        .ThenInclude(x => x.AttachmentType)
    .Single(x => x.Id == aRequest.Id);
var referencedAttachmentTypeIds = aRequest.Attachments.Select(x => x.AttachmentTypeId)
    .Distinct().ToList();
var referencedAttachmentTypes = theContext.AttachmentTypes
    .Where(x => referencedAttachmentTypeIds.Contains(x.Id))
    .ToList();

Getting the list of attachment types only applies if we can change an attachment's type, or are adding attachments.获取附件类型列表仅适用于我们可以更改附件类型或添加附件的情况。

Step 3: Leverage Automapper to copy across values第 3 步:利用 Automapper 跨值复制

mapper.Map(aRequest, existingRequest);

If Attachments can be updated, added, and/or removed you will need to handle those scenarios against the existingRequest.如果可以更新、添加和/或删除附件,您将需要针对现有请求处理这些场景。 Here we reference the loaded set of AttachmentTypes.这里我们引用加载的 AttachmentTypes 集。

Step 4: Save Changes.第 4 步:保存更改。

The primary benefits of this approach is that you do away with the constant checking for existing references and the consequences of missing a check.这种方法的主要好处是消除了对现有引用的不断检查以及错过检查的后果。 You also configure the rules about what values can legally be overwritten when calling the Automapper Map call so only values you expect are copied from the source to the existing data record.您还可以配置有关在调用Map调用时可以合法覆盖哪些值的规则,以便仅将您期望的值从源复制到现有数据记录。 This also results in faster Update queries as EF will only build statements for the values that actually changed, where using Update or EntityState.Modified result in SQL UPDATE statements that update every column.这也导致更快的更新查询,因为 EF 只会为实际更改的值构建语句,其中使用UpdateEntityState.Modified导致更新每一列的 SQL UPDATE语句。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM