Tylko dodatkowo (do zaznaczonej odpowiedzi) jest istotna różnica między context.Entry(entity).State = EntityState.Unchanged
i context.Attach(entity)
(w EF Core):
Zrobiłem kilka testów, aby lepiej to zrozumieć (dlatego obejmuje to również ogólne testy referencyjne), więc oto mój scenariusz testowy:
- Użyłem EF Core 3.1.3
- użyłem
QueryTrackingBehavior.NoTracking
- Użyłem tylko atrybutów do mapowania (patrz poniżej)
- Użyłem różnych kontekstów, aby uzyskać zamówienie i zaktualizować zamówienie
- W każdym teście wyczyściłem całą db
Oto modele:
public class Order
{
public int Id { get; set; }
public string Comment { get; set; }
public string ShippingAddress { get; set; }
public DateTime? OrderDate { get; set; }
public List<OrderPos> OrderPositions { get; set; }
[ForeignKey("OrderedByUserId")]
public User OrderedByUser { get; set; }
public int? OrderedByUserId { get; set; }
}
public class OrderPos
{
public int Id { get; set; }
public string ArticleNo { get; set; }
public int Quantity { get; set; }
[ForeignKey("OrderId")]
public Order Order { get; set; }
public int? OrderId { get; set; }
}
public class User
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
Oto (oryginalne) dane testowe w bazie danych:
Aby otrzymać zamówienie:
order = db.Orders.Include(o => o.OrderPositions).Include(o => o.OrderedByUser).FirstOrDefault();
Teraz testy:
Prosta aktualizacja za pomocą EntityState :
db.Entry(order).State = EntityState.Unchanged;
order.ShippingAddress = "Germany"; // will be UPDATED
order.OrderedByUser.FirstName = "William (CHANGED)"; // will be IGNORED
order.OrderPositions[0].ArticleNo = "K-1234 (CHANGED)"; // will be IGNORED
order.OrderPositions.Add(new OrderPos { ArticleNo = "T-5555 (NEW)", Quantity = 5 }); // will be INSERTED
db.SaveChanges();
// Will generate SQL in 2 Calls:
// INSERT INTO [OrderPositions] ([ArticleNo], [OrderId], [Quantity]) VALUES ('T-5555', 1, 5)
// UPDATE [Orders] SET [ShippingAddress] = 'Germany' WHERE [Id] = 1
Prosta aktualizacja z załącznikiem :
db.Attach(order);
order.ShippingAddress = "Germany"; // will be UPDATED
order.OrderedByUser.FirstName = "William (CHANGED)"; // will be UPDATED
order.OrderPositions[0].ArticleNo = "K-1234 (CHANGED)"; // will be UPDATED
order.OrderPositions.Add(new OrderPos { ArticleNo = "T-5555 (NEW)", Quantity = 5 }); // will be INSERTED
db.SaveChanges();
// Will generate SQL in 1 Call:
// UPDATE [OrderPositions] SET [ArticleNo] = 'K-1234' WHERE [Id] = 1
// INSERT INTO [OrderPositions] ([ArticleNo], [OrderId], [Quantity]) VALUES ('T-5555 (NEW)', 1, 5)
// UPDATE [Orders] SET [ShippingAddress] = 'Germany' WHERE [Id] = 1
// UPDATE [Users] SET [FirstName] = 'William (CHANGED)' WHERE [Id] = 1
Aktualizacja ze zmianą Child- ID z EntityState :
db.Entry(order).State = EntityState.Unchanged;
order.ShippingAddress = "Germany"; // will be UPDATED
order.OrderedByUser.Id = 3; // will be IGNORED
order.OrderedByUser.FirstName = "William (CHANGED)"; // will be IGNORED
order.OrderPositions[0].Id = 3; // will be IGNORED
order.OrderPositions[0].ArticleNo = "K-1234 (CHANGED)"; // will be IGNORED
order.OrderPositions.Add(new OrderPos { ArticleNo = "T-5555 (NEW)", Quantity = 5 }); // will be INSERTED
db.SaveChanges();
// Will generate SQL in 2 Calls:
// INSERT INTO [OrderPositions] ([ArticleNo], [OrderId], [Quantity]) VALUES ('T-5555', 1, 5)
// UPDATE [Orders] SET [ShippingAddress] = 'Germany' WHERE [Id] = 1
Aktualizacja ze zmianą identyfikatorów dzieci z załącznikiem :
db.Attach(order);
order.ShippingAddress = "Germany"; // would be UPDATED
order.OrderedByUser.Id = 3; // will throw EXCEPTION
order.OrderedByUser.FirstName = "William (CHANGED)"; // would be UPDATED
order.OrderPositions[0].Id = 3; // will throw EXCEPTION
order.OrderPositions[0].ArticleNo = "K-1234 (CHANGED)"; // would be UPDATED
order.OrderPositions.Add(new OrderPos { ArticleNo = "T-5555 (NEW)", Quantity = 5 }); // would be INSERTED
db.SaveChanges();
// Throws Exception: The property 'Id' on entity type 'User' is part of a key and so cannot be modified or marked as modified. To change the principal of an existing entity with an identifying foreign key first delete the dependent and invoke 'SaveChanges' then associate the dependent with the new principal.)
Uwaga: zgłasza wyjątek, bez względu na to, czy identyfikator został zmieniony, czy został ustawiony na oryginalną wartość, wygląda na to, że stan Id jest ustawiony na „zmieniony” i jest to niedozwolone (ponieważ jest to klucz podstawowy)
Aktualizacja ze zmianą identyfikatorów podrzędnych jako nowych (bez różnicy między EntityState i Attach):
db.Attach(order); // or db.Entry(order).State = EntityState.Unchanged;
order.OrderedByUser = new User();
order.OrderedByUser.Id = 3; // // Reference will be UPDATED
order.OrderedByUser.FirstName = "William (CHANGED)"; // will be UPDATED (on User 3)
db.SaveChanges();
// Will generate SQL in 2 Calls:
// UPDATE [Orders] SET [OrderedByUserId] = 3, [ShippingAddress] = 'Germany' WHERE [Id] = 1
// UPDATE [Users] SET [FirstName] = 'William (CHANGED)' WHERE [Id] = 3
Uwaga: zobacz różnicę w porównaniu z aktualizacją z EntityState bez nowego (powyżej). Tym razem nazwa zostanie zaktualizowana z powodu nowej instancji użytkownika.
Aktualizacja ze zmianą identyfikatorów referencyjnych z EntityState :
db.Entry(order).State = EntityState.Unchanged;
order.ShippingAddress = "Germany"; // will be UPDATED
order.OrderedByUserId = 3; // will be UPDATED
order.OrderedByUser.Id = 2; // will be IGNORED
order.OrderedByUser.FirstName = "William (CHANGED)"; // will be IGNORED
order.OrderPositions[0].Id = 3; // will be IGNORED
order.OrderPositions[0].ArticleNo = "K-1234 (CHANGED)"; // will be IGNORED
order.OrderPositions.Add(new OrderPos { ArticleNo = "T-5555 (NEW)", Quantity = 5 }); // will be INSERTED
db.SaveChanges();
// Will generate SQL in 2 Calls:
// INSERT INTO [OrderPositions] ([ArticleNo], [OrderId], [Quantity]) VALUES ('T-5555', 1, 5)
// UPDATE [Orders] SET [OrderedByUserId] = 3, [ShippingAddress] = 'Germany' WHERE [Id] = 1
Aktualizacja ze zmianą identyfikatorów referencyjnych z załączeniem :
db.Attach(order);
order.ShippingAddress = "Germany"; // will be UPDATED
order.OrderedByUserId = 3; // will be UPDATED
order.OrderedByUser.FirstName = "William (CHANGED)"; // will be UPDATED (on FIRST User!)
order.OrderPositions[0].ArticleNo = "K-1234 (CHANGED)"; // will be UPDATED
order.OrderPositions.Add(new OrderPos { ArticleNo = "T-5555 (NEW)", Quantity = 5 }); // will be INSERTED
db.SaveChanges();
// Will generate SQL in 1 Call:
// UPDATE [OrderPositions] SET [ArticleNo] = 'K-1234' WHERE [Id] = 1
// INSERT INTO [OrderPositions] ([ArticleNo], [OrderId], [Quantity]) VALUES ('T-5555 (NEW)', 1, 5)
// UPDATE [Orders] SET [OrderedByUserId] = 3, [ShippingAddress] = 'Germany' WHERE [Id] = 1
// UPDATE [Users] SET [FirstName] = 'William (CHANGED)' WHERE [Id] = 1
Uwaga: odniesienie zostanie zmienione na użytkownika 3, ale również użytkownik 1 zostanie zaktualizowany, myślę, że dzieje się tak, ponieważ order.OrderedByUser.Id
jest niezmieniony (nadal jest 1).
Wniosek
Dzięki EntityState masz większą kontrolę, ale musisz samodzielnie aktualizować właściwości podrzędne (drugiego poziomu). Za pomocą Attach możesz zaktualizować wszystko (chyba ze wszystkimi poziomami właściwości), ale musisz mieć oko na referencje. Na przykład: Jeśli User (OrderedByUser) byłby dropDown, zmiana wartości za pomocą dropDown może spowodować nadpisanie całego obiektu użytkownika. W takim przypadku oryginalna wartość dropDown zostanie nadpisana zamiast odwołania.
Dla mnie najlepszym przypadkiem jest ustawienie obiektów takich jak OrderedByUser na null i ustawienie tylko order.OrderedByUserId na nową wartość, jeśli chcę tylko zmienić odniesienie (bez względu na to, czy EntityState czy Attach).
Mam nadzieję, że to pomoże, wiem, że to dużo tekstu: D