首页 > 解决方案 > 使用具有存储库和工作单元模式的实体框架更新一对多关系中的子级

问题描述

我正在开发一个将现有 WPF 应用程序迁移到 Web 的项目。数据库已经在这里了。已使用现有项目中的存储过程完成更新。

我有这样的课

class Person
{
    int ID;
    string Name;
    string Address;

    virtual IList<Contact> Contacts;
}

class Contact
{
    int ID;
    int PersonID;
    virtual Person Person;
    string Carrier;
    string ContactNumber;
}

我需要能够修改through ContactNumberContactPersonRepository

存储库类似于

IRepository<Person> _personRepository;

添加一个新人可以使用

_personRepsitory.Add(person)
_unitOfWork.commit()

但无法使用更新

_personRepository.Update(person)
_unitOfWork.commit()

实体框架抛出一个错误,指出子项不可为空,应在更新前将其删除。但是我被告知要执行更新而不使用实体框架删除子级。可以在不删除现有子项的情况下使用此更新吗?如果是这样,怎么做?如果没有,还有什么其他选择?

标签: c#entity-frameworkentity-framework-coreone-to-many

解决方案


这将取决于该PersonRepository.Update()方法的作用。工作单元模式旨在包装存储库将使用的 DbContext 的范围。当实体(在您的案例中的人员及其相关联系人)从工作单元/DbContext 的范围之外引入时,这些类型的问题通常会出现。

对于 WPF / Windows 应用程序,DbContext 范围通常更长寿,其中实体基于页面加载之类的一个操作加载,并且当该页面可见时,该 DbContext 实例保持活动状态,因此可以使用对上下文执行进一步的事件加载的实体。对于 Web 应用程序,这必须考虑一些不同的事情。

DbContexts 的生命周期应该很短,并且在通常与单个请求的生命周期相关联的 Web 应用程序中,或者更短。(它永远不应该更长)许多示例遵循的默认行为是,当加载页面时,查询 DbContext 实例,然后将实体馈送到视图以用作模型。然后,当提交表单或进行 Ajax 调用时,该模型将传递回控制器操作。这里的问题是,虽然它看起来类似于 WPF 代码,因为操作接收看起来像 EF 实体的东西,但它们实际上得到的是反序列化的 POCO,而不是跟踪的实体。该请求的 DbContext 对该对象实例一无所知。这意味着像更改跟踪这样的细节完全缺失,并且出于所有密集目的,那个“实体” 并且任何相关细节都被视为新数据,否则不能被信任代表实际数据状态。(即,如果数据不是急切加载等,则不能保证完整。)

因此,在进行更新时,Web 应用程序要考虑的重要因素是:传入的不是被跟踪的实体,因此您不能将其视为一个实体。一种选择是将其附加到 DbContext,并将其实体状态设置为Modified. 出于三个原因,我不建议使用这种方法。

  1. 这“信任”传入的数据没有被篡改。Web 调试工具允许恶意操纵在 Ajax 中传递的数据或表单帖子。
  2. 如果数据有可能的相关数据,这些相关实体也必须全部附加,否则您最终会遇到异常、重复数据等问题。
  3. 即使在附加实体时,您也必须检查 DbContext 是否可能已经在跟踪实例,否则您会遇到情况异常。

最后一点是令人讨厌的,它可能导致错误地难以重现。假设您更新具有一个或多个订单的客户,并且每个订单引用一个或多个产品。当您将这些订单传递给一个方法时,如果两个订单引用相同的“产品”,反序列化的对象将包含对“产品 A”的两个不同的引用。将“产品 A”作为第一个订单的一部分附加会成功,但现在 DbContext 正在跟踪该实例,从第二个订单附加“产品 A”将失败。您必须始终检查现有的跟踪参考并在找到时替换这些参考。如果没有重复引用或 DbContext 没有以其他方式跟踪引用,则不会发生错误。(情境运行时错误)

要处理更新,如果您必须传递实体,那么您必须将这些实体视为完全不同的对象。(DTO 或 ViewModel)这意味着为了安全起见,加载当前数据状态并将传入数据中的相关值复制到跟踪的实例中并保存更改。这可确保:

  1. 您可以控制哪些数据可以并且不应该被覆盖。
  2. 您加载完整的适用数据状态并仅更新您期望的内容,不会因数据返回或返回而感到意外。(分离实体没有重复数据风险)
  3. 现有的被跟踪实体并不令人意外。

即使是没有相关实体并且您很想Attach()将状态设置为Modified或调用的简单情况,额外的好处Update()是,当跨复制值并利用 EF 的更改跟踪时,它只会生成并运行更新语句,如果值实际上变化,并且仅适用于实际变化的值。Update()EntityState.Modified始终导致更新所有列的更新语句,无论它们是否更改。这可能会对使用审计检查之类的东西产生不良影响/成本。

那么一个典型的 Update 方法应该是什么样子,在一个非常基本的层面上:

public Person Update(Person person)
{
    if (person == null) throw new ArgumentNullException("person");
    var existingPerson = _context.Persons
        .Include(x => x.Contacts)
        .Single(x => x.PersonId == person.PersonId);

    existingPerson.Name = person.Name;
    // only update fields expected to be changed.

    foreach(var contact in person.Contacts)
    {
        var existingContact = existingPerson.Contacts.SingleOrDefault(x => x.ContactId == contact.ContactId);
        // handle whether a contact exists or not, insert a new contact or update, etc.
    }
    _context.SaveChanges();
    return existingPerson;
}

更好的是,因为这可能会导致难以确定联系人何时可以被编辑、添加或删除,它可以帮助组织更精细的操作。例如:

AddContact(personId, contact);
UpdateContact(personId, contact);
RemoveContact(personId, contactId);

这些操作可以加载人员和相关联系人数据,而无需重复发送整个人员详细信息的开销,并使用提供的详细信息执行特定操作。

不会乱附加未跟踪的实例,并且通常可以避免意外篡改。理想情况下,这些方法将严格处理视图模型,而不是传入/返回实体。


推荐阅读