Обновление данных

74 Исходный проект

Обновление существующих объектов в базе данных реализуется также просто, как и вставка данных. Для этого нужно сначала извлечь нужный объект из базы данных, затем изменить одно или несколько значений его свойств, а затем сохранить изменения в базе данных используя метод DbContext.SaveChanges().

Обновление одного объекта

Допустим в таблице Customers существуют покупатели с фамилиями “Иванов”. Мы хотим поменять фамилии этих покупателей на “Сидоров”. Сделать это можно с помощью следующего кода:

public static void UpdateIvanov()
{
    SampleContext context = new SampleContext();

    var customer = context.Customers
        // Загрузить покупателя с фамилией "Иванов"
        .Where(c => c.LastName == "Иванов")
        .FirstOrDefault();

    // Внести изменения
    customer.LastName = "Сидоров";

    // Сохранить изменения
    context.SaveChanges();
}

Этот пример использует запрос LINQ для загрузки первого попавшегося покупателя с фамилией Иванов. Затем мы изменяем фамилию пользователя и вызываем метод SaveChanges(), при котором Entity Framework определяет, что объект сущности изменился и его нужно обновить используя следующий SQL-запрос:

UPDATE [dbo].[Customers]
SET [LastName] = @0
WHERE ([CustomerId] = @1)

-- @0: 'Сидоров' (Type = String, Size = -1)
-- @1: '5' (Type = Int32)

Как описывалось в предыдущей статье, для указания на изменение данных можно использовать модель состояний Entity Framework. Состояние сущностного объекта указывается через свойство State класса DbEntityEntry, который в свою очередь доступен через свойство DbSet.Entry. Для указания на то, что объект изменился используется значение EntityState.Modified. Ниже показан пример, как мы можем обновить фамилии всех Ивановых в таблице, а не только первого попавшегося:

public static void UpdateIvanov()
{
   SampleContext context = new SampleContext();

   IEnumerable<Customer> customers = context.Customers
       // Загрузить всех покупателей с фамилией "Иванов"
       .Where(c => c.LastName == "Иванов")
       .AsEnumerable()
       // Поменять им всем фамилию
       .Select(c => {
           c.LastName = "Сидоров";
           return c;
       });

   foreach (Customer customer in customers)
   {
       // Указать, что запись изменилась
       context.Entry(customer).State = EntityState.Modified;
   }

   // Сохранить изменения
   context.SaveChanges();
}

Этот код сгенерирует по одной инструкции UPDATE для каждого найденного покупателя с фамилией Иванов. Стоит отметить, что в показанных выше запросах мы загружали все данные пользователя, а во втором примере при использовании состояний, мы обновляли все данные, хотя достаточно лишь обновить только фамилию. (При автоматическом обновлении свойств модели, как в первом примере, Entity Framework будет обновлять только столбец LastName, как показано в сгенерированном SQL-коде). Класс DbEntityEntry содержит метод Property(), в котором можно явно указать свойство модели, которое нужно обновлять. Ниже показан более оптимизированный запрос для смены фамилий всех пользователей, в котором мы загружаем только их идентификаторы и обновляем только фамилии:

public static void UpdateIvanov()
{
    SampleContext context = new SampleContext();

    IEnumerable<Customer> customers = context.Customers
        // Загрузить всех покупателей с фамилией "Иванов"
        .Where(c => c.LastName == "Иванов")
        .Select(c => c.CustomerId)
        .AsEnumerable()
        // Поменять им всем фамилию
        .Select(id => new Customer {
            CustomerId = id,
            LastName = "Сидоров"
        });

    foreach (Customer customer in customers)
    {
        // Указать, что запись изменилась
        context.Customers.Attach(customer);
        context.Entry(customer)
            .Property(c => c.LastName).IsModified = true;
    }

    // Сохранить изменения
    context.SaveChanges();
}

Метод Property() возвращает объект DbPropertyEntry, в свойстве IsModified которого мы указываем, что состояние объекта изменилось. Это аналогично использованию свойства State класса DbEntityEntry. Обратите внимание на использование метода DbSet.Attach() при перечислении. Этот метод указывает, что объект нужно явно прикрепить в сущностной модели данных, т.к. он был создан вручную, а не загружен из базы данных. Действительно, следующий запрос LINQ to Entities:

context.Customers
    // Загрузить всех покупателей с фамилией "Иванов"
     .Where(c => c.LastName == "Иванов")
     .Select(c => c.CustomerId)
     .AsEnumerable()

фактически извлекает коллекцию идентификаторов имеющую тип IEnumerable<int>, а следующий за ним запрос LINQ to Objects уже создает коллекцию IEnumerable<Customer> в памяти приложения на основе идентификаторов. Как упоминалось ранее, метод AsEnumerable() в контексте использования в Entity Framework позволяет логически разделить один запрос на запрос к базе данных (LINQ to Entities) и простой запрос для работы с коллекциями C# (LINQ to Objects). На выходе, Entity Framework фактически ничего не знает об объектах коллекции customers, поэтому их нужно вручную прикрепить к объекту контекста используя метод Attach().

Обновление связанных объектов

Обновление связанных объектов происходит также как и их вставка. Мы используем навигационное свойство или внешний ключ для изменения связанного объекта, а затем сохраняем эти изменения в базе данных.

Давайте рассмотрим обновление связанных объектов в контексте нашего примера. Допустим мы допустили ошибку при определении заказа с идентификатором OrderId = 3, и нам нужно переместить этот заказ к другому пользователю (в моей тестовой базе данных этот заказ принадлежит покупателю с CustomerId = 4, у вас соответственно это может отличаться). Ниже показан пример, как это можно сделать с использованием навигационного свойства:

public static void UpdateOrder()
{
    SampleContext context = new SampleContext();

    Customer customer = context.Customers
        .Where(c => c.CustomerId == 3)
        .FirstOrDefault();

    Order order = context.Orders
        .Where(o => o.OrderId == 3)
        .FirstOrDefault();

    order.Customer = customer;

    context.SaveChanges();
}

В этом примере мы сначала выбираем покупателя, кому будет принадлежать новый заказ, затем выбираем сам заказ, после чего обновляем ссылку на покупателя через навигационное свойство Order.Customer. Это говорит Entity Framework о том, что нужно сгенерировать инструкцию UPDATE для обновления соответствующей записи в таблице Orders:

UPDATE [dbo].[Orders]
SET [UserId] = @0
WHERE ([OrderId] = @1)

Entity Framework автоматически ищет измененные свойства в прикрепленных сущностных объектах и обновляет только их, как говорилось в предыдущем разделе. В данном примере обновляется внешний ключ UserId, который мы явно определили в предыдущей статье.

Стоит отметить, что в этом примере мы использовали навигационное свойство на стороне зависимой таблицы Orders. Можно обновить связанные объекты также использовав навигационное свойство Customer.Orders на стороне базовой таблицы, но для этого нужно обязательно при выборке данных покупателя включить и выборку связанных с ним заказов с помощью прямой загрузки, чтобы Entity Framework мог отслеживать изменения в коллекции заказов пользователя:

Customer customer = context.Customers
    .Include(c => c.Orders)
    .Where(c => c.CustomerId == 3)
    .FirstOrDefault();

Order order = context.Orders
    .Where(o => o.OrderId == 3)
    .FirstOrDefault();

customer.Orders.Add(order);

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

Мы можем еще более оптимизировать первый пример, используя в запросах загрузку только идентификатора пользователя, идентификатора и внешнего ключа заказа, чтобы не выбирать слишком много данных. Как говорилось в предыдущем разделе, такие выбранные данные нужно будет явно прикрепить к контексту используя метод DbSet.Attach(). Ниже показан соответствующий пример:

public static void UpdateOrder()
{
    SampleContext context = new SampleContext();

    // Загрузить только идентификатор покупателя
    int id = context.Customers
        .Where(c => c.CustomerId == 3)
        .Select(c => c.CustomerId)
        .FirstOrDefault();

    Customer customer = new Customer { CustomerId = id };

    // Загрузить идентификатор и значение внешнего ключа
    // для заказа
    var tempOrder = context.Orders
        .Where(o => o.OrderId == 3)
        .Select(o => new {
            id = o.OrderId,
            userId = o.UserId
        })
        .FirstOrDefault();

    Order order = new Order
    {
        OrderId = tempOrder.id,
        UserId = tempOrder.userId
    };

    // Обновить внешний ключ
    order.UserId = customer.CustomerId;

    // Прикрепить сущность к контексту и указать, что
    // изменился только внешний ключ
    context.Orders.Attach(order);
    context.Entry(order).Property(o => o.UserId).IsModified = true;

    context.SaveChanges();
}

Сгенерированный SQL-оператор UPDATE для этого примера не отличается от того, который мы приводили выше, но при этом значительно увеличивается производительность двух операторов SELECT для выборки только ключей.

Универсальный метод обновления

В предыдущих статьях мы добавили универсальные методы загрузки данных и вставки данных. Их универсальность заключается в том, что они не привязаны к конкретному классу модели, а используют обобщения. Напомню, что для этих целей в нашем проекте мы создали класс Repository, файл которого находится в папке Model. Давайте добавим такой же метод для обновления данных:

public static void Update<TEntity>(TEntity entity, DbContext context) 
    where TEntity : class
{
    // Настройки контекста
    context.Database.Log = (s => System.Diagnostics.Debug.WriteLine(s));

    context.Entry<TEntity>(entity).State = EntityState.Modified;
    context.SaveChanges();
}

Этот метод использует настройки состояний для обновления сущностных данных. Теперь мы можем использовать этот универсальный метод в нашем приложении.

Если вы работаете с Entity Framework и создаете приложение ASP.NET, на последнем этапе разработки вам нужно озаботиться выбором хостинга и доменного имени для сайта. Вы должны будете зарегистрировать домен и перенести его на веб-хостинг для дальнейшей работы веб-приложения.

Пройди тесты
Лучший чат для C# программистов