更新EntityFrameworkCore中稍微複雜的實體

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類的屬性。

在我的操作方法中,它接收更新的實體,我可以​​首先通過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:無法跟踪實體類型“Employee”的實例,因為已經跟踪了具有相同鍵的此類型的另一個實例。添加新實體時,對於大多數鍵類型,如果未設置鍵,則將創建唯一的臨時鍵值(即,如果為鍵屬性指定了其t ype的默認值)。如果要為新實體顯式設置鍵值,請確保它們不會與現有實體或為其他新實體生成的臨時值發生衝突。附加現有實體時,請確保只有一個具有給定鍵值的實體實例附加到上下文。

如果我檢索現有實體而不跟踪它,我可以避免此錯誤。

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類,除了具有公司的導航屬性外,還包括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();
}

一般承認的答案

基於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因此,它可以工作,因為公司中沒有更改,只有existingEmployee員工中的更改。

總之,如果要在填充導航屬性時使用Update ,則需要確保導航表示的實體具有所有數據而不僅僅是PK屬性值。

現在,如果您只有Company.Id ,並且無法獲得updatedEmployee填寫的其他屬性,那麼對於關係修正,您應該使用外鍵屬性(僅需要PK(或AK)值)而不是導航(需要完整實體) 。

正如問題評論所述:您應該將CompanyId屬性添加到Employee類。由於存在導航, 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; }
}

在更新操作期間,在以下結構中傳遞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了解的AsNoTracking ,如果您在上下文中加載實體,則無法使用updatedEmployee調用Update 。此時,您需要手動修改每個屬性,因為您需要對EF正在跟踪的實體應用更改。 Update功能給EF告知,這是修改過的實體,開始跟踪它並在SaveChanges做必要的事情。所以AsNoTracking在這種情況下是正確的。此外,如果從服務器檢索實體的目的是檢查員工是否存在,那麼您可以查詢_dbContext.Employees.Count(m => m.Id == id);並將返回值與1進行比較。這將從服務器獲取較少的數據,並避免實現實體。
  • 將屬性CompanyId添加到CLR類沒有任何害處,然後EF在後台創建一個作為shadow屬性。將有數據庫列來存儲FK屬性的值。要么為它定義屬性,要么為EF定義屬性。


Related

許可下: CC-BY-SA with attribution
不隸屬於 Stack Overflow
許可下: CC-BY-SA with attribution
不隸屬於 Stack Overflow