Вставка данных

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

В предыдущих статьях мы уже видели как загрузить данные из базы данных в память приложения с использованием Entity Framework. Вы уже могли наглядно убедиться в главном преимуществе объектно-реляционной модели (Object Relational Mapper - ORM) Entity Framework, которое заключается во взаимодействии с базой данных с помощью управляемого кода C# без использования SQL-инструкций. Выборка данных – это только половина доступных возможностей для работы с базой данных. Большинству приложений также необходимо вносить изменения в эти данные, путем вставки, удаления или обновления данных, а затем отражать все эти изменения в базе данных.

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

Метод SaveChanges() будет вызывать обновление в источнике данных, который отвечает за перевод изменений в сущностных объектах в правильные инструкции SQL, которые выполняются с базой данных. Т.к. Entity Framework известно о связях между таблицами, определяемых с помощью навигационных свойств, этот фреймворк обеспечит создание нужных SQL-инструкций, чтобы эти изменения применялись в правильном порядке. В контексте нашего примера, вы можете, например, удалить покупателя. Entity Framework автоматически распознает связь между классами модели Customer и Order и удалит все связанные с покупателем заказы. Это говорит о том, что Entity Framework позволяет вносить изменения, которые автоматически влияют на отдельные объекты, на пару связанных объектов или на весь граф объектов.

Вставка одного объекта

Добавление нового объекта с помощью Entity Framework является простой операцией нужно только создать новый экземпляр вашего объекта и добавить его в коллекцию сущностных объектов DbSet с помощью метода Add(). Ниже показан пример, как мы можем вставить нового пользователя в базу данных (мы используем все тот же проект консольного приложения с которым работали в предыдущих статьях):

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

    // Создать нового покупателя
    Customer customer = new Customer
    {
        FirstName = "Иван",
        LastName = "Иванов",
        Age = 30
    };

    // Добавить в DbSet
    context.Customers.Add(customer);

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

При сохранении изменений в базе данных с помощью метода SaveChanges(), Entity Framework сгенерирует следующий SQL-запрос:

INSERT [dbo].[Customers]([FirstName], [LastName], [Email], [Age], [Photo])
    VALUES (@0, @1, NULL, @2, NULL)
    
SELECT [CustomerId]
    FROM [dbo].[Customers]
    WHERE @@ROWCOUNT > 0 AND [CustomerId] = scope_identity()

Обратите внимание на отображение нашего управляемого кода на этот SQL-запрос. Для вставки значений используются переменные @0, @1 и @2. Они инициализируются при компиляции приложения. Также, после инструкции INSERT следует инструкция SELECT, которая выбирает значение первичного ключа (в нашем случае CustomerId) для вновь вставленной записи. Благодаря этому, Entity Framework передает значение этого ключа переменной customer в коде. Т.е. после вставки данных, мы можем изменить или удалить этот объект, не извлекая его снова из базы. Чтобы убедиться, что EF инициализирует свойство первичного ключа в объекте модели при вставке в базу данных, можно изменить немного предыдущий пример:

public static void AddNewCustomer()
{
     // ...

    Console.WriteLine("ID до сохранения в базе: {0}",customer.CustomerId);

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

    Console.WriteLine("ID после сохранения в базе: ", customer.CustomerId);
}

Ниже показан вывод в консоли после запуска этого примера:

Изменение идентификатора объекта при его вставке в базу данных

Вставка связанных объектов

Связи между таблицами в Entity Framework описываются с помощью навигационных свойств. В нашей модели существует связь один-ко-многим между классами Customer и Order, которая выражается с помощью пары навигационных свойств Customer.Orders и Order.Customer. Если навигационное свойство имеет тип ссылки, то для вставки связанных данных нужно просто инициализировать это свойство. Если навигационное свойство имеет тип коллекции, то нужно использовать ее метод Add() для добавления новой записи. Следует помнить, что изменения могут быть сделаны с одного или с двух концов связи (если используется пара навигационных свойств).

Давайте предположим, что нам нужно вставить новый заказ для добавленного в предыдущем примере пользователя “Иван Иванов”:

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

    // Нужно извлечь сначала покупателя, 
    // которому добавляется заказ
    Customer ivan = context.Customers
        .Where(c => c.LastName == "Иванов")
        .FirstOrDefault();

    // Создаем заказ
    Order order = new Order
    {
        ProductName = "Яблоки",
        Quantity = 5,
        PurchaseDate = DateTime.Now,
        // Ссылка на покупателя в навигационном свойстве
        Customer = ivan
    };

    context.Orders.Add(order);

    context.SaveChanges();
}

В этом примере мы извлекаем пользователя с фамилией “Иванов” и добавляем для него новый заказ. Как видите, мы извлекаем все данные пользователя в первом запросе. Если таблица Customers содержит много столбцов с большим объемом данных, то такой подход плохо влияет на производительность. В таком случае можно извлечь только первичный ключ для покупателя (свойство CustomerId), а экземпляру класса Order добавить не ссылку в навигационном поле, а инициализировать внешний ключ. В примере нашей модели мы не определяли явно внешний ключ в классе Order, поэтому Code-First генерировал внешний ключ автоматически. Чтобы использовать описанный выше прием, мы должны будем добавить явно внешний ключ в класс Order:

public class Order
{
    // ...

    // Внешний ключ
    [ForeignKey("Customer")]
    public int UserId { get; set; }

    // Навигационные свойства
    public Customer Customer { get; set; }
    public List<OrderLines> Lines { get; set; }
}

Перед тем, как перейти к следующему примеру вы должны воссоздать базу данных с этой новой моделью, используя инициализаторы базы данных или удаляя ее вручную, как это описывалось при обсуждении Code-First. Теперь можно будет использовать этот внешний ключ в экземпляре класса Order:

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

    // Извлечь только Id покупателя
    int idIvan = context.Customers
        .Where(c => c.LastName == "Иванов")
        .Select(c => c.CustomerId)
        .FirstOrDefault();

    // Создаем заказ
    Order order = new Order
    {
        ProductName = "Яблоки",
        Quantity = 5,
        PurchaseDate = DateTime.Now,
        // Ссылка на покупателя во внешнем ключе
        UserId = idIvan
    };

    context.Orders.Add(order);

    context.SaveChanges();
}

В предыдущих примерах мы добавляли новый связанный объект для уже существующего базового объекта. В примере ниже показано, как при создании базового объекта, можно добавить сразу связанные объекты в одном запросе. Здесь мы создаем нового покупателя Петр Петров с двумя новыми заказами:

Customer customer = new Customer
{
    FirstName = "Петр",
    LastName = "Петров",
    Age = 30,
    Orders = new List<Order>
    {
        new Order {
            ProductName = "Яблоки",
            Quantity = 8,
            PurchaseDate = DateTime.Now
        },
        new Order {
            ProductName = "Апельсины",
            Quantity = 5,
            PurchaseDate = DateTime.Now
        }
    }
};

context.Customers.Add(customer);

context.SaveChanges();

Для этого примера Entity Framework создаст три SQL-запроса INSERT – один для вставки нового покупателя, а два для вставки связанных с ним заказов.

Реализация шаблона “найти или вставить”

В вашем приложении возможно понадобится реализовать простой шаблон “найти или вставить” (find or add), в котором вы проверяете, существует ли объект данных в базе, если нет, то создаете новый объект. Т.к. метод DbSet.Add() возвращает экземпляр сущностного класса модели, реализовать этот шаблон с помощью Entity Framework довольно просто:

public static Customer FindOrAdd(int id)
{
    SampleContext context = new SampleContext();

    return context.Customers.Find(id)
        ?? context.Customers.Add(new Customer
          {
              FirstName = "...",
              LastName = "...",
              Age = 40
          });
}

Напомню, что метод Find() ищет переданное ему значение в первичных ключах таблицы. В этом примере используется оператор C# ??, который проверяет на null выражение стоящее слева от него, и если оно возвращает null, то вызывается выражение справа.

Создание универсального метода вставки

В предыдущей статье, где мы рассматривали создание универсального метода для загрузки данных, мы создали новый класс Repository и обобщенный метод Select<TEntity>(). Давайте создадим подобный метод Insert() для универсальной вставки данных в базу:

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

    context.Entry(entity).State = EntityState.Added;
    context.SaveChanges();
}

/// <summary>
/// Запись нескольких полей в БД
/// </summary>
public static void Inserts<TEntity>(IEnumerable<TEntity> entities) where TEntity : class
{
    // Настройки контекста
    SampleContext context = new SampleContext();
    
    // Отключаем отслеживание и проверку изменений для оптимизации вставки множества полей
    context.Configuration.AutoDetectChangesEnabled = false;
    context.Configuration.ValidateOnSaveEnabled = false;

    context.Database.Log = (s => System.Diagnostics.Debug.WriteLine(s));


    foreach (TEntity entity in entities)
        context.Entry(entity).State = EntityState.Added;
    context.SaveChanges();

    context.Configuration.AutoDetectChangesEnabled = true;
    context.Configuration.ValidateOnSaveEnabled = true;
}

В этом примере мы создаем два метода, первый из которых служит для вставки одного объекта, а второй вставит коллекцию объектов. Для вставки объекта entity используется средство состояний Entity Framework – альтернативное средство вставки объектов, благодаря которому мы указываем состояние сущностного объекта из перечисления EntityState. В нашем случае мы используем состояние EntityState.Added.

Обратите внимание, что в методе Inserts() мы отключили некоторые настройки конфигурации класса контекста. В частности свойство AutoDetectChangesEnabled отключает создание уведомлений при изменении сущностных данных, а ValidateOnSaveEnabled отключает проверку достоверности запроса при сохранении. Предполагается, что в этом методе могут вставляться тысячи записей, поэтому нужно оптимизировать эти настройки.

Теперь вы можете использовать эти методы для создания простых запросов, не беспокоясь о деталях реализации вставки данных:

static void Main()
{
    Repository.Inserts(
        new List<Customer>
        {
            new Customer {
                FirstName = "Сидор",
                LastName = "Сидоров",
                Age = 23
            },
            new Customer {
                FirstName = "Павел",
                LastName = "Васин",
                Age = 20
            }
        });
}
Пройди тесты
Лучший чат для C# программистов