DbSet.Attach (объект) vs DbContext.Entry (объект) .State = EntityState.Modified

115

Когда я нахожусь в отдельном сценарии и получаю dto от клиента, который я сопоставляю с объектом, чтобы сохранить его, я делаю следующее:

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;этого, все свойства / столбцы будут обновлены)

Соответствующая документация: Add / Attach и Entity States .

Пример кода

Допустим, у вас есть следующая сущность:

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.

Как видите, оператор обновления обновляет только те значения, которые были фактически изменены после того, как вы прикрепили сущность к контексту. В зависимости от структуры вашей таблицы это может положительно сказаться на производительности.

Теперь, какой вариант лучше для вас, полностью зависит от того, что вы пытаетесь сделать.

sstan
источник
1
EF не создает предложение WHERE таким образом. Если вы прикрепили объект, созданный с помощью new (например, new 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, оно будет работать только с измененными свойствами.

Orhun
источник
0

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

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

  • Я использовал EF Core 3.1.3
  • я использовал QueryTrackingBehavior.NoTracking
  • Я использовал только атрибуты для сопоставления (см. Ниже)
  • Я использовал разные контексты, чтобы получить заказ и обновить заказ
  • Я вытирал всю базу данных для каждого теста

Это модели:

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 без new (см. Выше). На этот раз имя будет обновлено из-за нового экземпляра пользователя.

Обновление с изменением идентификаторов ссылок с помощью 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 вы можете обновлять все (я думаю, со всеми уровнями свойств), но вы должны следить за ссылками. Например: если User (OrderedByUser) будет dropDown, изменение значения с помощью dropDown может перезаписать весь объект User. В этом случае исходное значение dropDown-Value будет перезаписано вместо ссылки.

Для меня лучший случай - установить такие объекты, как OrderedByUser, в значение null и установить только order.OrderedByUserId на новое значение, если я хочу изменить только ссылку (независимо от того, EntityState или Attach).

Надеюсь, это поможет, я знаю, что текста много: D

StewieG
источник