我正在尝试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();
}
答案 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定义属性。