Удаление данных
131Работа с базами данных в .NET Framework --- Entity Framework 6 --- Удаление данных
Исходный проектЧтобы удалить объект, используя Entity Framework, можно использовать метод Remove() на объекте DbSet. Если удаляются данные, которые еще не были вставлены в базу данных, то изменения не сохраняются. Для удаленного объекта DbContext больше не отслеживает изменения. Запрос удаления в базе данных выполняется при сохранении изменений с помощью метода DbContext.SaveChanges().
Удаление одного объекта
Давайте реализуем метод DeleteOrder(), который будет удалять заказ с идентификатором 8 из таблицы Orders. Мы используем все тот же пример консольного приложения, который рассматривали ранее.
public static void DeleteOrder()
{
SampleContext context = new SampleContext();
Order order = context.Orders
.Where(o => o.OrderId == 8)
.FirstOrDefault();
context.Orders.Remove(order);
context.SaveChanges();
}
Этот пример использует LINQ-запрос на загрузку данных заказа с идентификатором, равным 8. Затем этот заказ удаляется из коллекции DbSet с использованием метода Remove(). Entity Framework сгенерирует следующий SQL-код для удаления этой записи:
DELETE [dbo].[Orders]
WHERE ([OrderId] = @0)
Это очень простой запрос DELETE, который использует для удаления значение первичного ключа для удаления записи заказа из таблицы Orders.
Удаление без загрузки данных
Возможно у вас возникнет вполне логичный вопрос по предыдущему примеру – зачем загружать данные заказа, создавая дополнительный ненужный запрос, если нам известен идентификатор заказа? Действительно, мы можем создать новый объект Order в коде, передав ему нужный идентификатор, затем прикрепив его к контексту используя метод DbSet.Attach() и затем вызвав метод Remove(). Ниже показан пример, аналогичный предыдущему, но без дополнительного запроса на загрузку данных:
public static void DeleteOrder()
{
SampleContext context = new SampleContext();
Order order = new Order
{
OrderId = 8
};
context.Orders.Attach(order);
context.Orders.Remove(order);
context.SaveChanges();
}
Этот подход следует использовать осторожно. Если в базе данных не существует заказа с идентификатором 8, то код сгенерирует исключение DbUpdateConcurrencyException. В первом примере будет также сгенерировано исключение, но NullReferenceException, если order при загрузке будет иметь значение null (т.е. отсутствовать в базе данных). Мы могли бы просто добавить проверку на значение null перед вызовом метода Remove(). Во втором же примере для обработки исключения нужно явно определять блоки кода try-catch.
Удаление связанных данных
При удалении объектов, которые имеют связанные данные, вам может понадобиться обновить соответствующие данные для удаления, чтобы успешно выполнить запрос. Необходимые обновления соответствующих данных будут зависеть от того, является ли отношение между связанными таблицами обязательным или необязательным. Обязательное отношение (required) означает, что запись в зависимой таблице не может существовать в базе данных без соответствующей записи в базовой таблице. Тип отношения в базе данных определяется тем, поддерживает ли внешний ключ зависимой таблицы значения NULL.
Напомню, что Code-First вводит обязательное отношение между таблицами в следующих трех случаях:
Если в классе модели явно определено свойство для внешнего ключа и тип этого свойства не поддерживает null.
Если навигационное свойство помечено атрибутом Required.
Если использованы соответствующие настройки в Fluent API для настройки отношений - методы HasRequired(), WithRequired(), WithRequiredDependent(), WithRequiredPrincipal() и IsRequired().
В нашей модели в классе Order на текущей момент явно определен внешний ключ UserId, поэтому Entity Framework будет создавать отношение между таблицами как обязательное, и внешний ключ будет иметь ограничение NOT NULL в таблице Orders. Для удаления объекта Customer, в данном случае нужно выполнить следующий код:
public static void DeleteCustomer()
{
SampleContext context = new SampleContext();
Customer customer = context.Customers
.Where(c => c.CustomerId == 6)
.FirstOrDefault();
context.Entry(customer)
.Collection(c => c.Orders)
.Load();
context.Customers.Remove(customer);
context.SaveChanges();
}
В этом примере мы загружаем данные покупателя с идентификатором равным 6. В моей базе этот покупатель имеет два заказа, поэтому в этом примере будет создано три оператора DELETE. В этом примере эти заказы загружаются используя явную загрузку (я привел ее здесь для разнообразия, раньше мы в основном использовали прямую загрузку с помощью метода Include()).
Стоит отметить, что Entity Framework по умолчанию при создании базы данных и использовании обязательных отношений подключает возможность каскадного удаления для связанных таблиц. Поэтому этот запрос можно оптимизировать, удалив загрузку связанных заказов, т.к. в нашей модели как раз используются обязательные отношения между таблицами:
Customer customer = context.Customers
.Where(c => c.CustomerId == 8)
.FirstOrDefault();
context.Customers.Remove(customer);
context.SaveChanges();
В этом примере будет сгенерирован один оператор DELETE для покупателя, а удаление связанных с ним данных будет возлагаться на базу данных. Стоит отметить, что этот код сгенерирует ошибку, если в базе данных не было настроено каскадное удаление для внешнего ключа (например, если вы создавали базу данных вручную и использовали подход Database-First или Code-Second). Для удаления данных в таком случае вам нужно будет явно определять загрузку связанных данных. Настройка каскадного удаления для таблиц в T-SQL описана в статье “Transact-SQL - создание таблиц” в разделе описания опций ON DELETE и ON UPDATE.
При создании необязательного отношения между таблицами, Entity Framework не использует каскадное удаление для них. Напомню, что необязательное отношение между таблицами создается, когда вы не определяете внешних ключей в классе модели (или определяете с помощью обнуляемых типов, например int?), используя только навигационные свойства для создания отношений, или используете методы HasOptional() и WithOptional() из Fluent API. Поэтому для таких связей нужно будет явно загружать связанные данные.
Стоит отметить, что если вы удаляете только базовый элемент, без удаления связанных, при необязательных отношениях связанные элементы не будут удаляться, а будет просто изменяться значение внешнего ключа на NULL. Т.е. если бы у нас между таблицами Customer и Orders было настроено необязательное отношение, то при запуске первого примера в этом разделе генерировалась бы одна инструкция DELETE для удаления покупателя и две инструкции UPDATE для обновления связанных с ним заказов. Чтобы их тоже явно удалить, нужно вызвать метод Remove() для объектов этих заказов.
Давайте теперь рассмотрим как удалять зависимые данные без удаления базового объекта. Например, мы хотим удалить для пользователя с идентификатором 4 принадлежащий ему заказ с именем “Товар 4”. Для этого мы можем выполнить следующий код:
public static void DeleteOrderFromCustomer()
{
SampleContext context = new SampleContext();
var tempCustomer = context.Customers
.Where(c => c.CustomerId == 4)
.Select(c => new
{
custid = c.CustomerId,
orderid = c.Orders.Where(o => o.ProductName == "Товар 4")
.Select(o => o.OrderId)
.FirstOrDefault()
})
.FirstOrDefault();
Customer customer = new Customer
{
CustomerId = tempCustomer.custid,
Orders = new List<Order>
{
new Order {
OrderId = tempCustomer.orderid,
UserId = tempCustomer.custid
}
}
};
context.Customers.Attach(customer);
context.Entry(customer.Orders.First()).State = EntityState.Deleted;
context.SaveChanges();
}
В этом примере мы показали, как максимально оптимизировать запрос на удаление только одного связанного объекта. Очевидно, что при такой задаче использовать каскадное удаление не получится, поэтому нужно явно извлечь связанный заказ. Оптимизация заключается в том, что мы загружаем только первичные ключи для покупателя и связанного с ним заказа, при этом используя один запрос SELECT. Затем мы создаем новый объект Customer, инициализируем его данными из анонимного объекта, который получили при запросе, и прикрепляем этот объект, как сущность модели. Затем мы указываем объекту заказа состояние EntityState.Deleted, что указывает Entity Framework, что этот объект нужно будет удалить при сохранении изменений в базе данных.
Универсальный метод удаления данных
По традиции, давайте определим универсальный метод удаления данных, не зависящий от конкретного типа модели. Для создания универсальных методов ранее мы определили специальный класс Repository. Давайте добавим в него метод Delete:
public void Delete<TEntity>(TEntity entity)
where TEntity : class
{
// Настройки контекста
SampleContext context = new SampleContext();
context.Database.Log = (s => System.Diagnostics.Debug.WriteLine(s));
context.Entry<TEntity>(entity).State = EntityState.Deleted;
context.SaveChanges();
}