DbSet.Attach (entity) vs DbContext.Entry (entity) .State = EntityState.Modified

115

Kiedy jestem w scenariuszu odłączonym i pobieram dto od klienta, który mapuję na jednostkę, aby ją zapisać, robię to:

context.Entry(entity).State = EntityState.Modified;
context.SaveChanges();

Po co wtedy DbSet.Attach(entity)

lub dlaczego powinienem używać metody .Attach, gdy EntityState.Modified już dołącza jednostkę?

Elisabeth
źródło
Lepiej dodaj trochę informacji o wersji, o to pytano wcześniej. Nie wiem, czy to zasługuje na nowe pytanie.
Henk Holterman

Odpowiedzi:

278

Kiedy to robisz context.Entry(entity).State = EntityState.Modified;, nie tylko przywiązujesz byt do bytu DbContext, ale także oznaczasz całą istotę jako brudną. Oznacza to, że gdy to zrobisz context.SaveChanges(), EF wygeneruje instrukcję aktualizacji, która zaktualizuje wszystkie pola jednostki.

Nie zawsze jest to pożądane.

Z drugiej strony, DbSet.Attach(entity)dołącza jednostkę do kontekstu bez oznaczania go jako brudnego. To jest równoznaczne z działaniemcontext.Entry(entity).State = EntityState.Unchanged;

W przypadku dołączania w ten sposób, chyba że następnie przystąpisz do aktualizowania właściwości jednostki, następnym razem, gdy wywołasz context.SaveChanges(), EF nie wygeneruje aktualizacji bazy danych dla tej jednostki.

Nawet jeśli planujesz aktualizację encji, jeśli encja ma wiele właściwości (kolumn bazy danych), ale chcesz zaktualizować tylko kilka, może okazać się korzystne wykonanie a DbSet.Attach(entity), a następnie zaktualizowanie tylko kilku właściwości które wymagają aktualizacji. Zrobienie tego w ten sposób spowoduje wygenerowanie bardziej wydajnej instrukcji aktualizacji z EF. EF zaktualizuje tylko zmodyfikowane właściwości (w przeciwieństwie do tego, context.Entry(entity).State = EntityState.Modified;które spowoduje zaktualizowanie wszystkich właściwości / kolumn)

Odpowiednia dokumentacja: Dodaj / Dołącz i Stany jednostek .

Przykład kodu

Załóżmy, że masz następującą jednostkę:

public class Person
{
    public int Id { get; set; } // primary key
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

Jeśli Twój kod wygląda tak:

context.Entry(personEntity).State = EntityState.Modified;
context.SaveChanges();

Wygenerowany kod SQL będzie wyglądał mniej więcej tak:

UPDATE person
SET FirstName = 'whatever first name is',
    LastName = 'whatever last name is'
WHERE Id = 123; -- whatever Id is.

Zwróć uwagę, jak powyższa instrukcja aktualizacji zaktualizuje wszystkie kolumny, niezależnie od tego, czy faktycznie zmieniłeś wartości, czy nie.

Z drugiej strony, jeśli Twój kod używa „normalnego” załącznika w następujący sposób:

context.People.Attach(personEntity); // State = Unchanged
personEntity.FirstName = "John"; // State = Modified, and only the FirstName property is dirty.
context.SaveChanges();

Wtedy wygenerowana instrukcja aktualizacji jest inna:

UPDATE person
SET FirstName = 'John'
WHERE Id = 123; -- whatever Id is.

Jak widać, instrukcja update aktualizuje tylko te wartości, które zostały faktycznie zmienione po dołączeniu jednostki do kontekstu. W zależności od struktury tabeli może to mieć pozytywny wpływ na wydajność.

Teraz, która opcja jest dla Ciebie lepsza, zależy całkowicie od tego, co próbujesz zrobić.

sstan
źródło
1
EF nie generuje klauzuli WHERE w ten sposób. Jeśli dołączyłeś obiekt utworzony za pomocą nowego (tj. New Entity ()) i ustawiłeś go na zmodyfikowany, musisz ustawić wszystkie oryginalne pola z powodu optymistycznej blokady. Klauzula WHERE wygenerowana w zapytaniu UPDATE zwykle zawiera wszystkie oryginalne pola (nie tylko Id), więc jeśli tego nie zrobisz, EF zgłosi wyjątek współbieżności.
bubi
3
@budi: Dziękuję za twoją opinię. Dla pewności ponownie przetestowałem i dla jednostki podstawowej zachowuje się tak, jak opisałem, z WHEREklauzulą ​​zawierającą tylko klucz podstawowy i bez sprawdzania współbieżności. Aby mieć sprawdzanie współbieżności, muszę jawnie skonfigurować kolumnę jako token współbieżności lub rowVersion. W takim przypadku WHEREklauzula będzie zawierała tylko klucz podstawowy i kolumnę tokenu współbieżności, a nie wszystkie pola. Jeśli twoje testy wykażą coś innego, chciałbym o tym usłyszeć.
sstan
jak mogę dynamicznie znaleźć modyfikację właściwości czarownicy?
Navid_pdp11
2
@ Navid_pdp11 DbContext.Entry(person).CurrentValuesi DbContext.Entry(person).OriginalValues.
Shimmy Weitzhandler
może być nieco poza tematem, ale jeśli używam wzorca repozytorium, muszę utworzyć repozytorium dla każdego modelu, ponieważ każdy model ma jakąś jednostkę, która musi być w stanie nieśledzonym podczas wstawiania nowego rekordu w db, więc nie mogę mieć ogólne repozytorium, które dołącza jednostki do kontekstu podczas wstawiania. Jak sobie z tym najlepiej radzisz?
jayasurya_j
3

Podczas korzystania z DbSet.Updatemetody Entity Framework oznacza wszystkie właściwości jednostki jako EntityState.Modified, więc śledzi je. Jeśli chcesz zmienić tylko niektóre właściwości, a nie wszystkie, użyj DbSet.Attach. Ta metoda tworzy wszystkie twoje właściwości EntityState.Unchanged, więc musisz określić właściwości, które chcesz zaktualizować EntityState.Modified. Dlatego gdy aplikacja osiągnie wartość DbContext.SaveChanges, będzie obsługiwać tylko zmodyfikowane właściwości.

Orhun
źródło
0

Tylko dodatkowo (do zaznaczonej odpowiedzi) jest istotna różnica między context.Entry(entity).State = EntityState.Unchangedi 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: wprowadź opis obrazu tutaj

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.Idjest 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

StewieG
źródło