简体   繁体   English

在EntityFrameworkCore中更新稍微复杂的实体

[英]Updating a slightly complex entity in EntityFrameworkCore

I am trying out EntityFrameworkCore . 我正在尝试EntityFrameworkCore I looked at the documentation, but couldn't find a way to easily update a complex entity that is related to another entity. 我查看了文档,但找不到轻松更新与另一个实体相关的复杂实体的方法。

Here is a simple example. 这是一个简单的例子。 I have 2 classes - Company & Employee. 我有2节课-公司和员工。

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

public class Employee
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Company Company { get; set; }
}

Company is a simple class, and Employee is only slightly complex, as it contains a property with reference to the Company class. Company是一个简单的类,而Employee则稍微复杂一些,因为它包含一个与Company类有关的属性。

In my action method, which takes in the updated entity, I could first look up the existing entity by id, and then set each property on it before I call SaveChanges. 在接收更新后的实体的操作方法中,我可以先通过id查找现有实体,然后在调用SaveChanges之前在其上设置每个属性。

[HttpPut]
public IActionResult Update(int id, [FromBody]Employee updatedEmployee)
{
    if (updatedEmployee == null || updatedEmployee.Id != id)
        return BadRequest();

    var existingEmployee = _dbContext.Employees
                             .FirstOrDefault(m => m.Id == id);
    if (existingEmployee == null)
        return NotFound();

    existingEmployee.Name = updatedEmployee.Name;

    if (updatedEmployee.Company == null)
        existingEmployee.Company = null; //as this is not a PATCH            
    else
    {
        var existingCompany = _dbContext.Companies.FirstOrDefault(m =>
                                m.Id == updatedEmployee.Company.Id);
        existingEmployee.Company = existingCompany;
    }

    _dbContext.SaveChanges();

    return NoContent();
}

With this sample data, I make an HTTP PUT call on Employees/3. 使用此样本数据,我在Employees / 3上进行了HTTP PUT调用。

{
    "id": 3,
    "name": "Road Runner",
    "company":
    {
        "id": 1
    }
}

And that works. 那行得通。

But, I hope to avoid having to set each property this way. 但是,我希望避免以这种方式设置每个属性。 Is there a way I could replace the existing entity with the new one, with a simple call such as this? 有没有办法通过这样的简单调用将现有实体替换为新实体?

_dbContext.Entry(existingEmployee).Context.Update(updatedEmployee);

When I try this, it gives this error: 当我尝试此操作时,会出现此错误:

System.InvalidOperationException: The instance of entity type 'Employee' cannot be tracked because another instance of this type with the same key is already being tracked. System.InvalidOperationException:无法跟踪实体类型“ Employee”的实例,因为已经跟踪了具有相同键的该类型的另一个实例。 When adding new entities, for most key types a unique tem porary key value will be created if no key is set (ie if the key property is assigned the default value for its t ype). 添加新实体时,对于大多数键类型,如果未设置任何键(即,如果为键属性指定了其类型的默认值),则将创建唯一的临时键值。 If you are explicitly setting key values for new entities, ensure they do not collide with existing entities or temporary values generated for other new entities. 如果您为新实体明确设置键值,请确保它们不与现有实体或为其他新实体生成的临时值冲突。 When attaching existing entities, ensure that only one entity instance with a given key value is attached to the context. 附加现有实体时,请确保仅将一个具有给定键值的实体实例附加到上下文。

I can avoid this error if I retrieve the existing entity without tracking it. 如果检索现有实体而不进行跟踪,则可以避免此错误。

var existingEmployee = _dbContext.Employees.AsNoTracking()
                         .FirstOrDefault(m => m.Id == id);

And this works for simple entities, but if this entity has references to other entities, this causes an UPDATE statement for each of those referenced entities as well, which is not within the scope of the current entity update. 这适用于简单实体,但是如果该实体具有对其他实体的引用,则这也会对每个被引用实体产生UPDATE语句,这不在当前实体更新的范围之内。 The documentation for the Update method says that as well: Update方法的文档也指出:

// Begins tracking the given entity, and any other reachable entities that are not already being tracked, in the Microsoft.EntityFrameworkCore.EntityState.Modified state such that they will be updated in the database when Microsoft.EntityFrameworkCore.DbContext.SaveChanges is called. //开始以Microsoft.EntityFrameworkCore.EntityState.Modified状态跟踪给定的实体以及尚未跟踪的任何其他可达实体,以便在调用Microsoft.EntityFrameworkCore.DbContext.SaveChanges时在数据库中对其进行更新。

In this case, when I update the Employee entity, my Company entity changes from 在这种情况下,当我更新Employee实体时,我的Company实体从

{
  "id": 1,
  "name": "Acme Products"
}

to

{
  "id": 1,
  "name": null
}

How can I avoid the updates on the related entities? 如何避免在相关实体上进行更新?


UPDATE : 更新

Based on the inputs in the comments and the accepted answer, this is what I ended up with: 根据评论中的输入和可接受的答案,这就是我最终得到的结果:

Updated Employee class to include a property for CompanyId in addition to having a navigational property for Company . 更新了Employee类,除了具有Company的导航属性外,还包括CompanyId的属性。 I don't like doing this as there are 2 ways in which the company id is contained within Employee, but this is what works best with EF. 我不喜欢这样做,因为在Employee中包含公司ID的方式有两种,但这是最适合EF的方法。

public class Employee
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int CompanyId { get; set; }
    public Company Company { get; set; }
}

And now my Update simply becomes: 现在我的Update就变成:

[HttpPut]
public IActionResult Update(int id, [FromBody]Employee updatedEmployee)
{
    if (updatedEmployee == null || updatedEmployee.Id != id)
        return BadRequest();

    var existingEmployeeCount = _dbContext.Employees.Count(m => m.Id == id);
    if (existingEmployeeCount != 1)
        return NotFound();

    _dbContext.Update(updatedEmployee);

    _dbContext.SaveChanges();

    return NoContent();
}

Based on documentation of Update 根据Update文档

Ref: Update 参考: 更新

Begins tracking the given entity in the Modified state such that it will be updated in the database when SaveChanges() is called. 开始跟踪处于Modified状态的给定实体,以便在调用SaveChanges()时在数据库中对其进行更新。 All properties of the entity will be marked as modified. 实体的所有属性将被标记为已修改。 To mark only some properties as modified, use Attach(Object) to begin tracking the entity in the Unchanged state and then use the returned EntityEntry to mark the desired properties as modified. 若要仅将某些属性标记为已修改,请使用Attach(Object)开始跟踪处于“未更改”状态的实体,然后使用返回的EntityEntry将所需的属性标记为已修改。 A recursive search of the navigation properties will be performed to find reachable entities that are not already being tracked by the context. 将对导航属性执行递归搜索,以找到上下文尚未跟踪的可到达实体。 These entities will also begin to be tracked by the context. 这些实体也将开始被上下文跟踪。 If a reachable entity has its primary key value set then it will be tracked in the Modified state. 如果可访问实体设置了其主键值,则将以“已修改”状态对其进行跟踪。 If the primary key value is not set then it will be tracked in the Added state. 如果未设置主键值,则将在“已添加”状态下对其进行跟踪。 An entity is considered to have its primary key value set if the primary key property is set to anything other than the CLR default for the property type. 如果主键属性设置为属性类型的CLR默认值以外的其他值,则认为该实体已设置其主键值。

In your case, you have updatedEmployee.Company navigation property filled in. So when you call context.Update(updatedEmployee) it will recursively search through all navigations. 对于您的情况,您已填充了updatedEmployee.Company导航属性。因此,当您调用context.Update(updatedEmployee) ,它将递归搜索所有导航。 Since the entity represented by updatedEmployee.Company has PK property set, EF will add it as modified entity. 由于由updatedEmployee.Company表示的实体已设置PK属性,因此EF会将其添加为已修改实体。 A point to notice here is Company entity has only PK property filled in not others. 需要注意的一点是, Company实体仅填充了PK属性,而没有其他属性。 (ie Name is null). (即名称为空)。 Therefore while EF determines that Company with id=1 has been modified to have Name=null and issues appropriate update statement. 因此,当EF确定ID = 1的Company已被修改为Name = null并发出适当的更新语句时。

When you are updating navigation by yourself, then you are actually finding the company from server (with all properties populated) and attaching that to existingEmployee.Company Therefore it works since there are no changes in Company, only changes in existingEmployee . 当您自己更新导航时,您实际上是在服务器上找到公司(填充了所有属性)并将其附加到existingEmployee.Company因此,这是可行的,因为Company中没有任何更改,而只existingEmployee

In summary, if you want to use Update while having a navigation property filled in then you need to make sure that entity represented by navigation has all data and not just PK property value. 总而言之,如果要在填充导航属性的同时使用Update ,则需要确保导航表示的实体具有所有数据,而不仅仅是PK属性值。

Now if you have only Company.Id available to you and cannot get other properties filled in updatedEmployee then for relationship fixup you should use foreign key property (which needs PK(or AK) values only) instead of navigation (which requires a full entity). 现在,如果只有您可用的Company.Id ,并且无法在updatedEmployee填充其他属性,则对于关系修正,应该使用外键属性(仅需要PK(或AK)值),而不要导航(需要完整的实体) 。

As said in question comments: You should add CompanyId property to Employee class. 正如有问题的评论所说:您应该将CompanyId属性添加到Employee类。 Employee is still non-poco (complex) entity due to navigation present. 由于存在导航, Employee仍然是非poco(复杂)实体。

public class Employee
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int? CompanyId {get; set; }
    public Company Company { get; set; }
}

During update action pass updatedEmployee in following structure. 在更新操作期间,按以下结构传递updatedEmployee (See this is the same amount of data, just structured bit differently.) (请参见这是相同数量的数据,只是结构有所不同。)

{
    "Id": 3,
    "Name": "Road Runner",
    "CompanyId": 1,
    "Company": null //optional
}

Then in your action, you can just call context.Update(updatedEmployee) and it will save employee but not modify the company. 然后,在您的操作中,您只需调用context.Update(updatedEmployee) ,它将保存员工但不会修改公司。

Due to Employee being complex class, you can still use the navigation. 由于Employee是复杂的类,因此您仍然可以使用导航。 If you have loaded employee with eager loading ( Include ) then employee.Company will have relevant Company entity value. 如果您已向急切加载的员工( Include )加载了员工,则employee.Company将具有相关的Company实体值。

Notes: 笔记:

  • _dbContext.Entry(<any entity>).Context gives you _dbContext only so you can write just _dbContext.Update(updatedEmployee) directly. _dbContext.Entry(<any entity>).Context仅给您_dbContext因此您可以直接编写_dbContext.Update(updatedEmployee)
  • As you figured out with AsNoTracking , if you load the entity in the context, then you cannot call Update with updatedEmployee . 正如您使用AsNoTracking所弄清楚的那样,如果您在上下文中加载实体,则无法使用updatedEmployee调用Update At that point you need to modify each property manually because you need to apply changes to the entity being tracked by EF. 此时,您需要手动修改每个属性,因为您需要将更改应用于EF跟踪的实体。 Update function gives EF telling, this is modified entity, start tracking it and do necessary things at SaveChanges . Update函数提供EF通知,它是已修改的实体,开始对其进行跟踪,并在SaveChanges处进行必要的操作。 So AsNoTracking is right to use in this case. 因此,在这种情况下,可以使用AsNoTracking Further, if the purpose of retrieving entity from server for you is to check existence of employee only, then you can query _dbContext.Employees.Count(m => m.Id == id); 此外,如果从服务器检索实体的目的仅是检查雇员的存在,则可以查询_dbContext.Employees.Count(m => m.Id == id); and compare return value to 1. This fetches lesser data from the server and avoids materializing the entity. 并将返回值与1比较。这将从服务器获取较少的数据,并避免实现该实体。
  • There is no harm in putting property CompanyId if you don't add it to CLR class then EF creates one for you in background as shadow property. 如果不将属性CompanyId添加到CLR类中,则放置属性CompanyId不会有任何危害,然后EF在后台为您创建一个作为shadow属性。 There will be database column to store value of FK property. 将有一个数据库列来存储FK属性的值。 Either you define property for it or EF will. 您可以为它定义属性,也可以定义为EF。

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

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