DbSet.Attach (сутність) проти DbContext.Entry (сутність) .State = EntityState.Modified


115

Коли я перебуваю в окремому сценарії і отримую точку від клієнта, якого я перетворюю на сутність, щоб зберегти його, я роблю це:

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

Для чого тоді DbSet.Attach(entity)

або чому я повинен використовувати метод .Attach, коли EntityState.Modified вже приєднує об'єкт?


Краще додайте інформацію про версію, про це вже питали раніше. Мені не ясно, чи заслуговує це нове запитання.
Хенк Холтерман

Відповіді:


278

Коли ви це робите context.Entry(entity).State = EntityState.Modified;, ви не тільки приєднуєте сутність до DbContext, а й маркуєте всю сутність як брудну. Це означає, що коли ви зробите це context.SaveChanges(), EF генерує оператор оновлення, який оновить усі поля сутності.

Це не завжди бажано.

З іншого боку, DbSet.Attach(entity)додає суб'єкт до контексту, не позначаючи його брудним. Це рівнозначно робитиcontext.Entry(entity).State = EntityState.Unchanged;

Приєднуючи цей спосіб, якщо ви не переходите до оновлення властивості сутності, наступного разу, коли ви звернетесь context.SaveChanges(), EF не буде генерувати оновлення бази даних для цього об'єкта.

Навіть якщо ви плануєте робити оновлення для об'єкта, якщо у сукупності є багато властивостей (колонки db), але ви хочете лише оновити декілька, вам може бути вигідним зробити DbSet.Attach(entity), а потім оновити лише декілька властивостей які потребують оновлення. Це зробити таким чином, створить більш ефективну заяву оновлення від EF. EF оновлює лише зміни, які ви змінили (на відміну від context.Entry(entity).State = EntityState.Modified;них, усі властивості / стовпці будуть оновлені)

Відповідна документація: Додавання / додавання та сутність держав .

Приклад коду

Скажімо, у вас є така особа:

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

Якщо ваш код виглядає так:

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

Створений SQL буде виглядати приблизно так:

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

Зверніть увагу, як вищезазначений оператор оновлення буде оновлювати всі стовпці, незалежно від того, змінили ви значення чи ні.

На відміну від цього, якщо ваш код використовує "звичайний" додавання, як це:

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

Тоді створена операція оновлення відрізняється:

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

Як бачимо, оператор оновлення оновлює лише ті значення, які були фактично змінені після приєднання сутності до контексту. Залежно від структури вашої таблиці, це може мати позитивний вплив на ефективність.

Тепер, який варіант для вас краще, повністю залежить від того, що ви намагаєтеся зробити.


1
EF не генерує пункт WHERE таким чином. Якщо ви додали об'єкт, створений за допомогою нового (тобто нового Entity ()), і встановите його на змінене, вам слід встановити всі початкові поля через оптимістичне блокування. Запит WHERE, сформований у запиті UPDATE, зазвичай містить усі вихідні поля (не тільки Id), тому якщо ви цього не зробите, EF викине виключення з одночасністю.
bubi

3
@budi: Дякую за відгук. Я повторно перевірявся, щоб бути впевненим, і для базового об'єкта він поводиться так, як я описав, із WHEREпунктом, що містить лише первинний ключ, і без перевірки сумісності. Щоб перевірити паралельність, мені потрібно чітко налаштувати стовпчик як маркер паралельності або rowVersion. У цьому випадку в WHEREпункті буде міститись лише первинний ключ та стовпець жетону паралельності, а не всі поля. Якщо ваші тести покажуть інакше, я хотів би почути про це.
sstan

як я можу динамічно знайти властивість відьом змінити?
Navid_pdp11

2
@ Navid_pdp11 DbContext.Entry(person).CurrentValuesі DbContext.Entry(person).OriginalValues.
Шиммі Вайцхандлер

це може бути трохи поза темою, але якщо я використовую шаблон сховища, я повинен створити сховище для кожної моделі, оскільки кожна модель має певну сутність, яка повинна знаходитись у нерозглянутому стані під час вставлення нової записи в db, тому я не можу мати загальне сховище, яке приєднує об'єкти до контексту під час вставки. Як ти найкраще справляється з цим?
jayasurya_j

3

Під час використання DbSet.Updateметоду Entity Framework позначає всі властивості вашої сутності як EntityState.Modified, тому відстежує їх. Якщо ви хочете змінити лише деякі свої властивості, не всі, скористайтеся DbSet.Attach. Цей метод забезпечує всі ваші властивості EntityState.Unchanged, тому ви повинні зробити свої властивості, які ви хочете оновити EntityState.Modified. Таким чином, коли програма потрапляє на DbContext.SaveChanges, вона працюватиме лише з модифікованими властивостями.


0

Просто на додаток (до позначеної відповіді) є важлива різниця між context.Entry(entity).State = EntityState.Unchangedта context.Attach(entity)(в EF Core):

Я зробив кілька тестів, щоб зрозуміти це більше самостійно (тому це включає також загальне еталонне тестування), тому це мій тестовий сценарій:

  • Я використовував EF Core 3.1.3
  • я використав QueryTrackingBehavior.NoTracking
  • Я використовував лише атрибути для картографування (див. Нижче)
  • Я використовував різні контексти для отримання замовлення та оновлення замовлення
  • Я витер цілий db для кожного тесту

Це моделі:

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; }
}

Це (оригінальні) дані тесту в базі даних: введіть тут опис зображення

Щоб отримати замовлення:

order = db.Orders.Include(o => o.OrderPositions).Include(o => o.OrderedByUser).FirstOrDefault();

Тепер тести:

Просте оновлення з 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

Просте оновлення з додатком :

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

Оновлення, змінивши дочірні ідентифікатори за допомогою 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

Оновлення зі зміною дочірніх ідентифікаторів за допомогою додатку :

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

Примітка. Це викидає виняток, незалежно від того, чи було змінено Id чи було встановлено початкове значення, схоже, що стан Id встановлено на "змінено", і це заборонено (тому що це первинний ключ)

Оновіть, змінюючи дочірні ідентифікатори як нові (немає різниці між EntityState та 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

Примітка. Дивіться різницю оновлення з EntityState без нового (вище). Цього разу Ім'я буде оновлено через новий екземпляр користувача.

Оновлення зі зміною контрольних ідентифікаторів за допомогою 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

Оновлення зі зміною контрольних ідентифікаторів з додатком :

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

Примітка: Посилання буде змінено на Користувач 3, але також буде оновлено користувач 1, я думаю, це тому, що значення order.OrderedByUser.Idне змінилося (це все ще 1).

Висновок Завдяки EntityState ви маєте більше контролю, але вам доведеться самостійно оновлювати підвластивості (другий рівень). За допомогою програми Attach ви можете оновити все (я думаю, з усіма рівнями властивостей), але вам слід слідкувати за посиланнями. Наприклад, наприклад, якщо користувач (OrdersByUser) буде dropDown, зміна значення через dropDown може перезаписати весь User-об'єкт. У цьому випадку початкова dropDown-Value буде замінена замість посилання.

Для мене найкращим випадком є ​​встановлення таких об’єктів, як OrdersByUser, і встановити лише порядок.OrderedByUserId на нове значення, якщо я хочу лише змінити посилання (незалежно від того, EntityState чи Attach).

Сподіваюся, це допомагає, я знаю, що це багато тексту: D

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.