Создание запросов

73

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

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

В качестве примера проекта, на котором мы будем тестировать наши запросы, мы будем использовать проект консольного приложения C#. Ранее, мы использовали для демонстрации примеров веб-приложение ASP.NET, которое было создано в статье “Использование Code-First”. Очевидно, что при рассмотрении примеров работы с данными проект веб-приложения будет громоздким, т.к. для каждого примера нужно будет редактировать разметку веб формы. Поэтому создайте новый проект, используя шаблон Console Application, как показано на рисунке ниже:

Создание нового консольного приложения для демонстрации работы с Entity Framework

После этого нужно будет установить библиотеку Entity Framework в этот проект. Стоит отметить, что DbContext API является частью сборки EntityFramework.dll, поэтому он автоматически добавляется при установке Entity Framework в ваш проект. Для установки Entity Framework используется диспетчер пакетов NuGet, как говорилось ранее. Этот диспетчер позволяет быстро загружать нужные сборки из интернета в проект Visual Studio. Для вызова этого диспетчера можно использовать команду меню Tools --> Library Package Manager --> Manage Nuget Packages. В открывшемся диалоговом окне вы можете найти расширение Entity Framework и установить его в свой проект:

Установка Entity Framework в проект

Теперь добавьте в проект папку Model, в которую добавьте новый класс Model.cs, содержащий модель данных. Модель сущностных классов должна выглядеть следующим образом:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ExamplesEF
{
    public class Customer
    {
        public int CustomerId { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Email { get; set; }
        public int Age { get; set; }

        [Column(TypeName = "Image")]
        public byte[] Photo { get; set; }

        // Ссылка на заказы
        public List<Order> Orders { get; set; }
    }

    public class Order
    {
        public int OrderId { get; set; }
        public string ProductName { get; set; }
        public string Description { get; set; }
        public int Quantity { get; set; }
        public DateTime PurchaseDate { get; set; }

        // Ссылка на покупателя
        public Customer Customer { get; set; }
    }
}

Здесь используется простая модель с двумя классами, описывающими покупателя и его заказы. Мы использовали эту модель при обсуждении настроек классов с помощью Fluent API и аннотаций данных при рассмотрении подхода Code-First на протяжении нескольких предыдущих статей.

Стоит отметить, что мы будем использовать подход Code-First при рассмотрении работы с данными в Entity Framework, хотя эта работа не зависит от конкретного подхода. При любом подходе модель данных использует класс контекста, унаследованный от DbContext, соответственно запросы для использования данных будут одинаковыми. Я использую подход Code-First, т.к. он обеспечивает настройку структуры базы данных из управляемого кода C#. Вы можете использовать подходы Model-First или Database-First при работе с примерами.

Далее необходимо будет добавить класс контекста, связывающий сущностные классы с базой данных. Для этого добавьте новый файл SampleContext.cs в папку Model, имеющий следующее содержимое:

using System.Data.Entity;

namespace ExamplesEF
{
    public class SampleContext : DbContext
    {
        public SampleContext() : base("MyShop")
        {
            // Указывает EF, что если модель изменилась,
            // нужно воссоздать базу данных с новой структурой
            Database.SetInitializer(
                new DropCreateDatabaseIfModelChanges<SampleContext>());
        }

        public DbSet<Customer> Customers { get; set; }
        public DbSet<Order> Orders { get; set; }
    }
}

После этого вы можете добавить следующий код в программу:

static void Main()
{
    SampleContext context = new SampleContext();
    context.Customers.Add(new Customer()
    {
        FirstName = "Вася",
        Age = 20
    });

    context.SaveChanges();
}

В этом примере добавляется новый покупатель в таблицу Customers. Теперь, если вы запустите программу Entity Framework создаст базу данных MyShop на локальном сервере SQLEXPRESS (если эта база данных еще не была создана). В последующих статьях мы будем показывать примеры, которые используются в этом консольном приложении. Далее мы опишем некоторые особенности запросов, которые используются в Entity Framework.

Использование запросов Entity SQL

Запросы Entity SQL используют SQL-подобный синтаксис, при этом взаимодействие с базой данных происходит с помощью вызова SQL-команд. Фактически такой подход к созданию запросов в Entity Framework идентичен использованию ADO.NET, поэтому в последующих примерах мы не будем никогда его использовать. Но давайте рассмотрим простой пример использования такого запроса в Entity Framework:

using System;
using System.Linq;
using System.Data.Common;
using System.Data.SqlClient;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Core.Objects;

namespace ExamplesEF
{
    class Program
    {
        static void Main()
        {
            // Получить объект ObjectContext из DbContext
            ObjectContext context = 
                (new SampleContext() as IObjectContextAdapter).ObjectContext;
            
            // Создать объект подключения и команду
            SqlConnection connection = new SqlConnection(
                @"Data Source=.\SQLEXPRESS;Initial Catalog=MyShop");

            // Создать запрос
            ObjectQuery<DbDataRecord> Customers =
                context.CreateQuery<DbDataRecord>("SELECT c.FirstName FROM Customers AS c");
            
            // Отобразить имя первого покупателя в таблице Cusomers
            Console.WriteLine(Customers != null ? 
                Customers.First()["FirstName"].ToString()
                : "Таблица пустая");
        }
    }
}

Как видите, Entity SQL напрямую не поддерживается в современных версиях Entity Framework, т.к. нам нужно использовать старый класс контекста ObjectContext. Мы получили экземпляр этого класса из объекта SampleContext, путем приведения его к интерфейсу IObjectContextAdapter. Затем мы использовали SQL-команды для загрузки данных из таблицы базы данных. Очевидно, что данный подход не использует одно из главных преимуществ Entity Framework – использование объектной модели базы данных, поэтому мы не будем его использовать далее.

Стоит отметить, что в DbContext API есть способ выполнять произвольные SQL-инструкции. Для этого служит метод ExecuteSqlCommand() класса Database, объект которого доступен через одноименное свойство класса контекста. Но при этом, этот метод был создан для того, чтобы вы могли выполнить произвольную SQL-инструкцию для обращения к базе данных (например, вы можете изменить кодировку таблицы или добавить триггер), но этот метод не стоит использовать для извлечения или работы с данными.

Использование LINQ

Базовым подходом к написанию запросов в Entity Framework является использование расширения языка C# - LINQ, которое предоставляет набор методов для работы с коллекциями. Подход с использованием LINQ в Entity Framework мы рассматривали в разделе LINQ to Entities, при этом этот раздел был написан еще до появления версии EF 4.1, поэтому там мы использовали класс контекста ObjectContext. Далее мы будем рассматривать запросы LINQ to Entities при использовании класса контекста, унаследованного от DbContext.

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

Стоит отметить, что расширение LINQ не является чем-то специфическим для Entity Framework и может повсеместно использоваться в коде приложения для работы с коллекциями. В контексте Entity Framework, коллекции являются множеством строк таблицы, с которыми можно работать посредством LINQ. Давайте реализуем функциональность предыдущего примера с использованием LINQ-запроса:

using System;
using System.Linq;

namespace ExamplesEF
{
    class Program
    {
        static void Main()
        {
            // Создаем экземпляр класса контекста 
            SampleContext context = new SampleContext();
            
            // Используем LINQ-запрос для извлечения первого заказчика
            var name = context.Customers
                              .Select(c => c.FirstName)
                              .FirstOrDefault();

            Console.WriteLine(name);
        }
    }
}

Этот пример гораздо проще и понятней, чем тот, что мы использовали при демонстрации Entity SQL. Здесь мы использовали метод расширения LINQ – FirstOrDefault(), который выбирает первую запись из коллекции (она же таблица, в понимании EF) Customers. Стоит отметить, что базовые методы расширения LINQ находятся в пространстве имен System.Linq, при этом Entity Framework также предлагает некоторые методы расширения, которые находятся в пространстве имен System.Data.Entity.

Стоит отметить также, что LINQ поддерживает использования синтаксиса SQL в запросах (синтаксис с вызовом цепочки методов, как в предыдущем примере, называется синтаксисом точечной нотации). Эта возможность удобна для программистов, тесно работающих с базами данных и хорошо знающих языки запросов, например T-SQL. Ниже показан пример использования синтаксиса SQL, которые работает также, как и показанные выше примеры:

string name = (from customer in context.Customers
               select customer.FirstName)
               .FirstOrDefault();
                          
Console.WriteLine(name);

Стоит отметить что этот синтаксис довольно ограничен, поэтому в этом примере нам пришлось вызвать метод FirstOrDefault() с использованием синтаксиса точечной нотации, т.к. для этого метода не определен соответствующий псевдоним.

После рассмотрения примеров использования LINQ у вас наверняка возникнет вопрос, как Entity Framework интерпретирует эти запросы на связанные запросы SQL? Ответ на этот вопрос довольно простой в контексте нашего примера. Вызов коллекции Customers объекта контекста говорит EF, что нужно сгенерировать запрос SELECT для выборки всех записей из таблицы. После этого EF видит вызов метода Select(c => c.FirstName), который указывает, что нас интересует только имя пользователя, хранящееся в столбце FirstName. Затем следует вызов метода FirstOrDefault(), что говорит EF, что нам не нужны все записи из таблицы Customers, а нужна только первая запись. В результате Entity Framework сгенерирует следующий SQL-код для обращения к базе данных:

SELECT TOP (1) 
    [c].[FirstName] AS [FirstName]
    FROM [dbo].[Customers] AS [c]

Если вы хотите просматривать генерируемый SQL-код для LINQ-запросов, то вы можете использовать журнал логов операций к базе данных, который можно включить с помощью свойства Database.Log. Этому свойству передается делегат, который можно реализовать с помощью лямбда-выражения и указать, куда нужно записывать лог операций. Использование этого свойства показано в примере ниже:

static void Main()
{
    // Создаем экземпляр класса контекста 
    SampleContext context = new SampleContext();

    context.Database.Log = (s => Console.WriteLine(s));
    
    // Используем LINQ-запрос для извлечения первого заказчика
    var name = context.Customers
      .Select(c => c.FirstName)
      .FirstOrDefault();


    Console.WriteLine(name);
}

После запуска этого примера, в консоль будет выведена SQL-команда для этого запроса. Также, SQL-запрос можно вывести, например, в окно отладчика среды Visual Studio:

// ...

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

// ...
       

Класс System.Diagnostics.Debug как раз является средством взаимодействия между кодом и отладчиком Visual Studio. Сгенерированный код отображается на панели Output:

Просмотр автоматически генерируемого SQL-кода

Обратите внимание, если вы прокрутите это окно выше, то увидите что Entity Framework направил еще один запрос базе данных, для извлечения данных таблицы __MigrationHistory. Как объяснялось в статье “Миграции модели в Code-First”, эта таблица служит для отслеживания изменений в модели данных. Если вас мучают вопросы производительности приложений, то вы можете не волноваться на счет этого момента, так как запрос к таблице с миграциями выполняется один раз, при запуске приложения.

Отложенная компиляция LINQ-запросов

Как описывалось только что, LINQ компилирует запросы из управляемого кода в SQL-инструкции. При этом, если мы удалим вызов метода FirstOrDefault() в примере выше, сам запрос к базе данных будет выполняется не при инициализации переменной name, а при ее вызове в методе Console.WriteLine (в этом случае будет возвращаться коллекция имен пользователей из таблицы). Такой подход называется отложенным выполнением LINQ-запросов, т.е. запрос компилируется не при его объявлении, а при непосредственном вызове переменной, содержащей этот запрос, в коде.

Это обеспечивается благодаря тому, что класс DbSet, экземпляр которого мы получаем через свойство context.Customers, реализует интерфейс IQueryable. Этот интерфейс специфичен для LINQ и находится в пространстве имен System.Linq. Он является производным от интерфейса коллекций IEnumerable и обеспечивает отложенное выполнение запросов. В разделе LINQ to Objects вы можете увидеть, какие методы LINQ выполняют отложенные запросы, а какие методы вызывают запрос сразу при его объявлении. Метод FirstOrDefault() относится к неотложенным запросам, поэтому, в предыдущем примере запрос будет выполняться при инициализации переменной name.

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

Отложенные запросы LINQ

В первом примере запрос выполняется при первой итерации цикла foreach, что является стандартным поведением отложенных запросов LINQ. Во втором примере мы использовали не отложенный метод ToList(), для выполнения запроса при объявлении переменной names. Понимание отложенной природы запросов в LINQ является важным при работе с Entity Framework, т.к. выполнение запроса в коде в Entity Framework означает, что мы выполняем запрос к базе данных.

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