我正在使用 .NET Core 構建 API,并嘗試將記錄插入到關系中的許多表中。我有表格 Client、Shipment、ShipmentDetail 和 Product
當我嘗試將 shippingDetails 插入到貨件中時出現錯誤。錯誤是:INSERT 陳述句與 FOREIGN KEY 約束“FK_ShipmentDetails_Products_ProductId”沖突。沖突發生在資料庫“MyDb”、表“dbo.Products”、列“Id”中。
課程:
public class Client
{
public int ClientId { get; set; }
public string Name { get; set; } = String.Empty;
public string Adress { get; set; }
public List<Shipment> Shipments { get; set; }
}
public class Product : IEntity
{
public int Id { get; set; }
public string Name { get; set; }
}
public class Shipment
{
public int ShipmentId { get; set; }
public DateTime TimeCreated { get; set; }
public string ShippingAddress { get; set; }
public int DeliveryCost { get; set; }
public string OrderNumber { get; set; }
public DateTime EstimatedDate { get; set; }
public int ClientId { get; set; }
public Client Client { get; set; }
public List<ShipmentDetail>? ShipmentDetails { get; set; }
public List<ShipmentStatu>? ShipmentStatus { get; set; }
}
public class ShipmentDetail
{
public int Id { get; set; }
public string DeliveryNumber { get; set; }
public string TrackingNumber { get; set; }
public Shipment Shipment { get; set; }
public int ShipmentId { get; set; }
public Product Product { get; set; }
public int ProductId { get; set; }
}
我試過的
[HttpPost]
public async Task<ActionResult<List<Shipment>>> AddShipments(ShipmentDto request)
{
var client = _context.Clients.FirstOrDefault(p => p.Name == request.ClientName)
?? new Client
{
Name = request.ClientName,
Adress = request.ClientAdress
};
var newShipment = new Shipment
{
TimeCreated = Convert.ToDateTime(DateTime.Now.GetDateTimeFormats()[0]),
ShippingAddress = request.ShippingAddress,
DeliveryCost = request.DeliveryCost,
OrderNumber = "x" DateTime.Now.ToString("MMddyy"),
EstimatedDate = Convert.ToDateTime(DateTime.Now.AddDays(2).GetDateTimeFormats()[0]),
ShipmentDetails = new List<ShipmentDetail>{
new ShipmentDetail{
DeliveryNumber = request.DeliveryNumber ,
TrackingNumber=request.TrackingNumber,
}
}
};
if(client.Shipments == null)
client.Shipments = new List<Shipment>();
client.Shipments.Add(newShipment);
if(client.ClientId == default)
_context.Clients.Add(client);
else _context.Clients.Update(client)
_context.SaveChanges();
return Ok(newShipment);
}
有人有想法嗎?親切的問候...
uj5u.com熱心網友回復:
使用相關集合時的幾個問題和建議。
首先,當加載物體打算與他們的孩子一起作業時,一定要急切地加載他們的孩子:
var client = _context.Clients
.Include(p => p.Shipments)
.SingleOrDefault(p => p.Name == request.ClientName)
?? new Client
{
Name = request.ClientName,
Adress = request.ClientAdress
};
如果您期望 1 或 0 個結果,請使用SingleOrDefault()而不是FirstOrDefault(). 使用“First”風格的方法可以隱藏錯誤的資料假設,即您的代碼選擇性不足,從而可能導致錯誤的資料被更新。正確使用“單一”方法會在運行時斷言這一點,并會在遇到假定唯一性的無效資料狀態時拋出。
如果您知道可能有多個匹配項并且只想要第一個特定匹配項,那么您可以使用FirstOrDefault()但應該包含一個OrderBy()以確保選擇是可預測和可重復的。我知道 Nick Chapas 最近發布了一段視頻,介紹了SingleOrDefault()與 相比在哪里會產生性能成本FirstOrDefault(),但這僅適用于將它用于記憶體中的集合。當 EF 構建一個查詢時,FirstOrDefault生成一個等價的查詢。TOP(1)SingleOrDefaultTOP(2)
接下來,您應該避免在物體構造之外的物體中“設定”集合參考。EF 與代理一起作業以進行更改跟蹤,因此將集合設定為新的List<T>可能會與現有物體混淆。我建議將 Client 中的集合參考更改為:
public virtual ICollection<ShipmentDetail> ShipmentDetails { get; internal set; } = new List<ShipmentDetail>();
public virtual ICollection<ShipmentStatus> ShipmentStatus { get; internal set; } = new List<ShipmentStatus>();
現在,您可以在新的 Client 實體上立即使用這些集合,或者無需擔心 #null 檢查即可訪問它們。代碼庫中的任何地方都不應該有任何代碼試圖將它們設定為新集合。
生成的代碼最終看起來像:
var client = _context.Clients
.Include(p => p.Shipments)
.SingleOrDefault(p => p.Name == request.ClientName)
?? new Client
{
Name = request.ClientName,
Adress = request.ClientAdress
};
var newShipment = new Shipment
{
TimeCreated = Convert.ToDateTime(DateTime.Now.GetDateTimeFormats()[0]),
ShippingAddress = request.ShippingAddress,
DeliveryCost = request.DeliveryCost,
OrderNumber = "x" DateTime.Now.ToString("MMddyy"),
EstimatedDate = Convert.ToDateTime(DateTime.Now.AddDays(2).GetDateTimeFormats()[0]),
}
};
newShipment.ShipmentDetails.Add(new ShipmentDetail
{
DeliveryNumber = request.DeliveryNumber,
TrackingNumber = request.TrackingNumber
});
client.Shipments.Add(newShipment);
if(client.ClientId == default)
_context.Clients.Add(client);
_context.SaveChanges();
return Ok(newShipment);
這里另一個值得注意的是,我們不需要呼叫_context.Update,而是讓 EF 的更改跟蹤管理跟蹤客戶端和相關物體中發生的詳細資訊。在使用 DTO / ViewModel 時,我還建議保持一致并避免在其位置發送物體,因此我會考慮在對 DTO 的回應中更新回傳的物件。
編輯:將產品關聯到新的 ShipmentDetail 的示例。假設您為新的貨件詳細資訊選擇了一個產品,產品 ID #35,并且一個 ShipmentDetail 包含一個產品集合。(多對多關系)如果它也包含單個產品參考,則適用。(多對一)我們不想做的是:
var product = new Product{ Id = 35 };
shipmentDetail.Products.Add(product);
// ...
_context.SaveChanges();
這將導致已經存在 Product ID #35 的行的兩種情況之一。當 EF 嘗試插入具有相同 ID 的產品時,我們會收到重復 PK 的例外,或者它將嘗試插入 ID 為 #645 的新產品,或者如果該表是下一個可用的產品 ID,則它會嘗試插入新產品在它的 PK 上設定一個 Identity 列。當一個完整的產品“物體”作為 POST 請求的一部分被反序列化并添加到新物體時,這是一個常見問題。DbContext 沒有跟蹤該實體,因此它將其視為一個新實體。
一種在部分/大部分時間都有效的解決方法是將產品附加到 DbContext:
var product = new Product{ Id = 35 };
_context.Attach(product);
shipmentDetail.Products.Add(product);
// ...
_context.SaveChanges();
這有時會起作用,但在 DbContext 已經恰好跟蹤該特定產品 ID 的情況下,它可能會導致例外。
處理該場景的最安全方法是獲取 Product 并以這種方式關聯它:
var product = _context.Products.Single(x => x.Id == 35);
shipmentDetail.Products.Add(product);
// ...
_context.SaveChanges();
這可確保 EF 將現有產品與貨件詳細資訊相關聯。開發人員可以避免這樣做,因為這意味著要訪問資料庫。但是,按 ID 加載物體非常快,而且它的好處是可以斷言 ID #35 實際上是一個有效的產品 ID。
回到附加場景。有一種更安全的方法可以在沒有 DB 往返的情況下執行此操作,但它仍然不是萬無一失的:
var product = _context.Products.Local.SingleOrDefault(x => x.Id == 35);
if (product == null)
{
product = new Product{ Id = 35 };
_context.Attach(product);
}
shipmentDetail.Products.Add(product);
// ... _context.SaveChanges();
然而,這種方法的缺點(除了添加更多代碼只是為了避免訪問資料庫)是仍然沒有斷言產品 ID #35 確實存在于資料庫中。如果沒有,上面的代碼將在SaveChanges.
在多對一場景中,如果 Shipment Detail 包含產品 ID 和產品參考,快速插入的方法是在插入的新 ShipmentDetail 記錄上設定 ProductId 欄位:
shipmentDetail.ProductId == productId;
_context.SaveChanges();
不利的一面是,由于上述相同的原因,我們無法在不加載產品或附加它的情況下填充產品參考(在檢查沒有已跟蹤的參考之后)這意味著任何可能檢查我們剛剛創建的發貨詳細資訊的進一步代碼都不會能夠訪問任何與產品相關的詳細資訊,直到重新加載貨件詳細資訊。(比如如果我們將新創建的 Shipment Detail 傳回給視圖,或者給 Automapper 構建一個視圖模型)這就是為什么去 DB 獲取和關聯 Product 的性能投資是值得的,它是最安全的,最少的代碼,最有用的。
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/507006.html
