实体类型的实例无法被追踪,因为另一个具有该键值的实例正在被追踪。

7

我正在尝试使用EntityFrameWork Core和.Net Core 3.1实现CRUD。在更新操作中,我遇到了一个问题,即不能使用修改后的值更新上下文。

我正在使用Postman发起请求,如下所示的代码中,我正在尝试检查该客户是否存在,如果存在,则将修改后的对象传递给上下文。

enter image description here

函数代码

   [FunctionName("EditCustomer")]
    public async Task<IActionResult> Run(
    [HttpTrigger(AuthorizationLevel.Anonymous,"post", Route = "update-customer")] HttpRequest req)
    {
        var customer = JsonConvert.DeserializeObject<CustomerViewModel>(new StreamReader(req.Body).ReadToEnd());
        await _repo.UpdateCustomer(customer);
        return new OkResult();
    }

存储库方法

  public async Task UpdateCustomer(CustomerViewModel customerViewModel)
    {
        if (customerViewModel.CustomerId != null)
        {
            var customer = _context.Customers.Where(c => c.CustomerId.Equals(customerViewModel.CustomerId)).FirstOrDefault();

            if (customer == null)
            {
                throw new Exception("customer not found");
            }
            else
            {
                _context.Customers.Update(_mapper.Map<Customers>(customerViewModel));
                await _context.SaveChangesAsync();

            }
        }
       
    }

映射

   public class CustomerManagerProfile : Profile
    {
        public CustomerManagerProfile()
        {
            CreateMap<CustomerDetails, CustomerDetailsViewModel>().ReverseMap();
            CreateMap<CustomerOrders, CustomerOrdersViewModel>().ReverseMap();
            CreateMap<CustomerOrderDetails, OrderDetailsViewModel>().ReverseMap();
            CreateMap<Customers, CustomerViewModel>().ReverseMap();
        }
    }

解决方案

public async Task UpdateCustomer(CustomerViewModel customerViewModel)
    {
        if (customerViewModel.CustomerId != null)
        {
            var customer = _context.Customers.Where(c => c.CustomerId.Equals(customerViewModel.CustomerId)).FirstOrDefault();

            if (customer == null)
            {
                throw new Exception("customer not found");
            }
            else
            {
                var customerModel = _mapper.Map<Customers>(customerViewModel);
                _context.Entry<Customers>(customer).State = EntityState.Detached;
                _context.Entry<Customers>(customerModel).State = EntityState.Modified;
                await _context.SaveChangesAsync();

            }
        }
}
  

     

阅读了这篇文章后,我已经实现了解决方案。更新了我的帖子。它有效。您能告诉我这是否是正确的方法吗? - Tom
是的,现在就可以了。您也可以使用 FirstOrDefaultAsync() - Rao Arman
当我使用FirstOrDefaultAsync时,它会抱怨无法将'System.Threading.Tasks.Task<SRL.Data.Models.Customers>'转换为'SRL.Data.Models.Customers'。SRL.CustomerManager - Tom
1
永远不要将VM映射到实体。Automapper是为了从实体到VM、VM到DTO或DTO(数据传输对象)到VM(视图模型)进行映射而创建的。 - zolty13
显示剩余5条评论
3个回答

14

Entity Framework会为您跟踪实体。简单地说,把它看作是为每个表维护一个字典(dictionary),其中字典键等于实体的主键。

问题在于,在词典中无法添加两个具有相同键的项目,而相同的逻辑也适用于EF的更改跟踪器。

现在来看看您的仓储(repository):

var customer = _context
                  .Customers
                  .Where(c => c.CustomerId.Equals(customerViewModel.CustomerId))
                  .FirstOrDefault();

从数据库中检索到的客户信息会被获取,并且更改跟踪器将其放入其字典中。

var mappedCustomer = _mapper.Map<Customers>(customerViewModel);
_context.Customers.Update();

为了方便我的解释,我将您的代码分为两个步骤。

需要注意的是,EF只能保存对已跟踪对象的更改。因此,当您调用Update时,EF会执行以下检查:

  • 这个对象是否和我在变化追踪器中拥有的对象相同(引用相等)?
  • 如果是,则它已经在我的变化追踪器中。
  • 如果不是,则将此对象添加到我的变化追踪器中。

在您的情况下,mappedCustomer是一个不同于customer的对象,因此EF尝试将mappedCustomer添加到变化追踪器中。由于customer已经在其中,并且customermappedCustomer具有相同的PK值,这就产生了冲突。

您看到的异常是该冲突的结果。

由于您不需要实际跟踪原始的customer对象(因为EF在获取后不会对其进行任何操作),最短的解决方案是告诉EF不要跟踪customer

var customer = _context
                  .Customers
                  .AsNoTracking()
                  .Where(c => c.CustomerId.Equals(customerViewModel.CustomerId))
                  .FirstOrDefault();

由于现在customer不再放入更改跟踪器中,因此mappedCustomer将不再引起冲突。

然而,实际上您根本不需要获取此客户。您只想知道它是否存在。因此,我们可以这样做,而不是让EF获取整个customer对象:

bool customerExists = _context
                        .Customers
                        .Any(c => c.CustomerId.Equals(customerViewModel.CustomerId));

这也解决了该问题,因为您从未获取原始customer,因此它不会被跟踪。 它还可以在此过程中为您节省一些带宽。 单独而言,它确实微不足道,但如果您在代码库中重复此改进,则可能变得更加显着。


5
你可以通过避免像这样在检索时跟踪 Customers 来进行最简单的调整:
var customer = _context
    .Customers
    .AsNoTracking() // This method tells EF not to track results of the query.
    .Where(c => c.CustomerId.Equals(customerViewModel.CustomerId))
    .FirstOrDefault();

从代码中并不完全清楚,但我猜测您的映射器返回一个具有相同ID的Customer的新实例,这使得EF感到困惑。如果您改为修改同一实例,则.Update()的调用也应该能正常工作:

var customer = _context.Customers.Where(c => c.CustomerId.Equals(customerViewModel.CustomerId)).FirstOrDefault();
customer.Name = "UpdatedName"; // An example.
_context.Customers.Update(customer);
await _context.SaveChangesAsync();

事实上,如果您追踪您的客户(Customer),您甚至不需要显式调用.Update()方法,跟踪的目的是了解实体所做的更改,并应保存到数据库中。因此,以下代码也可以正常工作:
// Customer is being tracked by default.
var customer = _context.Customers.Where(c => c.CustomerId.Equals(customerViewModel.CustomerId)).FirstOrDefault();
customer.Name = "UpdatedName"; // An example.
await _context.SaveChangesAsync();
编辑:
您自己提供的解决方案是通过跟踪查询结果(实例Customer),然后在写入数据库之前停止跟踪它(也称为分离它),而是开始跟踪表示更新的实例Customer并将其标记为已修改。显然,这也可以工作,但只是一种效率和优雅程度较低的方式。
事实上,如果您使用这种奇怪的方法,我不认为需要检索您的Customer。当然,您可以直接:

if (!(await _context.Customers.AnyAsync(c => c.CustomerId == customerViewModel.CustomerId)))
{
    throw new Exception("customer not found");
}

var customerModel = _mapper.Map<Customers>(customerViewModel);
_context.Customers.Update(customerModel);
await _context.SaveChangesAsync();

正如您所提到的修改同一实例 customer.Name = "UpdatedName"; // 一个例子。在这一点上,我如何知道哪个属性已被修改。如果我需要设置所有属性分配,那么映射器的作用是什么。 - Tom
我刚刚更新了帖子,介绍了我解决问题的方法。 - Tom
@Tom 你说得对,你不知道哪些属性是不同的。有几种方法可以采取:相等性检查(“如果值不同,则设置值”),或者仅附加新对象而不是旧对象(这确实会导致所有属性被“更改”,但通常不是很重要)。 - Flater

-1

你使用 AutoMapper 的方式不正确。它并不是为了将视图模型或 DTO 映射到实体类而创建的。这会带来很多问题,你现在只面临其中之一。

如果你的应用程序有更复杂的业务逻辑(不仅仅是更新所有字段),那么管理、测试和调试你的代码中实际发生的事情将会非常困难。当你想进行 CRUD 以外的其他更新时,你应该编写自己的逻辑,并进行一些业务验证。

如果我是你,我会在 Customer 类中创建一个 UpdateFields 方法,用于更新它们,最后调用 SaveChanges。这取决于你是否使用贫血实体(反)模式。如果你不想让你的实体类有任何方法,你可以创建一个手动映射 VM 到实体的方法,并进行一些领域验证。


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