在EntityFrameworkCore

时间:2017-02-05 14:26:23

标签: c# entity-framework-core

我正在尝试EntityFrameworkCore。我查看了文档,但无法找到一种方法来轻松更新与另一个实体相关的复杂实体。

这是一个简单的例子。我有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; }
}

公司是一个简单的类,而Employee只是稍微复杂一些,因为它包含一个引用Company类的属性。

在我接受更新实体的action方法中,我可以先按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();
}

使用此示例数据,我在Employees / 3上进行HTTP PUT调用。

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

这很有效。

但是,我希望避免以这种方式设置每个属性。有没有办法可以用一个简单的调用来替换现有实体?

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

当我尝试这个时,会出现这个错误:

  

System.InvalidOperationException:实体类型的实例   无法跟踪“员工”,因为此类型的另一个实例   已经跟踪了相同的密钥。添加新实体时,   对于大多数键类型,如果不是,将创建唯一的临时键值   key被设置(即,如果为key属性分配了默认值   它的意思。如果您明确设置新实体的键值,   确保它们不会与现有实体或临时值发生冲突   为其他新实体生成。附加现有实体时,   确保只有一个具有给定键值的实体实例   附在上下文中。

如果我检索现有实体而不跟踪它,我可以避免此错误。

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

这适用于简单实体,但如果此实体具有对其他实体的引用,则会为每个引用的实体生成UPDATE语句,这不在当前实体更新的范围内。 Update方法的文档也说明了这一点:

  

//开始在Microsoft.EntityFrameworkCore.EntityState.Modified状态下跟踪给定实体以及尚未被跟踪的任何其他可到达实体,以便在Microsoft.EntityFrameworkCore.DbContext.SaveChanges时在数据库中更新它们被称为。

在这种情况下,当我更新Employee实体时,我的公司实体将从

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

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

如何避免相关实体的更新?

更新

根据评论中的输入和接受的答案,这就是我最终的结果:

更新了Employee类,除了具有 Company 的导航属性外,还包括 CompanyId 的属性。我不喜欢这样做,因为公司ID有两种方式包含在Employee中,但这最适合EF。

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

现在我的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();
}

1 个答案:

答案 0 :(得分:2)

基于Update

的文档

价:Update

  

开始跟踪处于Modified状态的给定实体,以便在调用SaveChanges()时在数据库中更新它。   实体的所有属性都将标记为已修改。要仅将某些属性标记为已修改,请使用Attach(Object)开始跟踪处于Unchanged状态的实体,然后使用返回的EntityEntry将所需属性标记为已修改。   将执行导航属性的递归搜索以查找尚未被上下文跟踪的可到达实体。这些实体也将开始被上下文跟踪。如果可到达实体的主键值已设置,则将以“已修改”状态跟踪它。如果未设置主键值,则将在“已添加”状态下跟踪它。如果主键属性设置为属性类型的CLR默认值以外的任何值,则认为实体具有其主键值。

在您的情况下,您填写了updatedEmployee.Company导航属性。因此,当您致电context.Update(updatedEmployee)时,它将递归搜索所有导航。由于updatedEmployee.Company表示的实体具有PK属性集,因此EF会将其添加为已修改的实体。这里需要注意的是Company实体只有PK属性而不是其他实体。 (即Name为null)。因此,虽然EF确定已将id = 1的公司修改为具有Name = null并发出适当的更新语句。

当您自己更新导航时,您实际上是从服务器(已填充所有属性)中找到该公司并将其附加到existingEmployee.Company因此,由于公司中没有任何更改,因此只能更改{ {1}}。

总之,如果要在填写导航属性时使用existingEmployee,则需要确保导航所代表的实体具有所有数据而不仅仅是PK属性值。

现在,如果您只有Update可用,并且无法在Company.Id中填写其他属性,那么对于关系修正,您应该使用外键属性(仅需要PK(或AK)值)导航(需要一个完整的实体)。

如有问题的评论: 您应该将updatedEmployee属性添加到CompanyId类。由于存在导航,Employee仍然是非poco(复杂)实体。

Employee

在更新操作期间,在以下结构中传递public class Employee { public int Id { get; set; } public string Name { get; set; } public int? CompanyId {get; set; } public Company Company { get; set; } } 。 (看到这是相同数量的数据,只是结构有点不同。)

updatedEmployee

然后在您的操作中,您只需致电{ "Id": 3, "Name": "Road Runner", "CompanyId": 1, "Company": null //optional } 即可保存员工,但不会修改公司。

由于context.Update(updatedEmployee)是复杂的类,您仍然可以使用导航。如果您已为员工加载了热切加载(Employee),那么Include将具有相关的公司实体价值。

注意:

  • employee.Company仅向您_dbContext.Entry(<any entity>).Context提供,因此您可以直接撰写_dbContext
  • 当您使用_dbContext.Update(updatedEmployee)时,如果您在上下文中加载实体,则无法使用AsNoTracking来调用Update。此时,您需要手动修改每个属性,因为您需要对EF正在跟踪的实体应用更改。 updatedEmployee函数给EF讲述,这是修改后的实体,开始跟踪它并在Update处执行必要的操作。所以SaveChanges在这种情况下使用是正确的。此外,如果从服务器检索实体的目的是仅检查员工是否存在,那么您可以查询AsNoTracking并将返回值与1进行比较。这将从服务器获取较少的数据并避免实现实体。 / LI>
  • 如果不将属性_dbContext.Employees.Count(m => m.Id == id);添加到CLR类中,那么放置属性CompanyId没有任何害处,那么EF会在后台为您创建一个属性为shadow属性。将有数据库列来存储FK属性的值。要么为它定义属性,要么为EF定义属性。