Инициализация базы данных с тестовыми данными. Создание веб-приложения MVC

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

  • Объектно-реляционный конструктор . Этот конструктор предоставляет многофункциональный пользовательский интерфейс для создания объектной модели из существующей базы данных. Данное средство, являющееся частью интегрированной среды разработки Visual Studio, лучше всего подходит для баз данных небольшого или среднего размера.
  • Средство создания кода SQLMetal . По своему набору параметров эта консольная программа несколько отличается от Объектно-реляционного конструктора. Данное средство лучше всего подходит для моделирования больших баз данных.
  • Редактор кода . Можно написать собственный код с помощью редактора кода Visual Studio или другого редактора кода. Этот подход может привести к большому числу ошибок, поэтому при наличии существующей базы данных, которую можно использовать для создания модели с помощью Объектно-реляционного конструктора или программы SQLMetal, использовать его не рекомендуется. Однако редактор кода становится ценным инструментом, когда требуется уточнить или изменить код, уже созданный с помощью других средств.

Используем последний способ, задав атрибуты для существующих объектов :

using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Data.Linq; using System.Data.Linq.Mapping; using System.Data.SqlClient; namespace LinqtoSQL { public class Customer { public string CustomerID { get; set; } public string City { get; set; } public override string ToString() { return CustomerID + "\t" + City; } } class Program { static void Main(string args) { DataContext db = new DataContext (@"Data Source=.\SQLEXPRESS; AttachDbFilename=|DataDirectory|\NORTHWND.MDF; Integrated Security=True; User Instance=True"); var results = from c in db.GetTable() where c.City == "Москва" select c; foreach (var c in results) Console.WriteLine("{0}\t{1}", c.CustomerID, c.City); Console.ReadKey(); } } }

В этом приложении есть класс Customer , который отображает таблицу Customers , и имеет поля CustomerID и City , отображающие поля этой таблицы. Объект класса DataContext задает входную точку в базу данных и имеет метод GetTable , который возвращает коллекцию определенного типа, в данном случае типа Customer .

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

Хранимая процедура извлекает из таблицы Products 10 самых дорогих продуктов и их цены:

USE GO SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO ALTER procedure . AS SET ROWCOUNT 10 SELECT Products.ProductName AS TenMostExpensiveProducts, Products.UnitPrice FROM Products ORDER BY Products.UnitPrice DESC

Для того чтобы вызвать эту процедуру из программы на языке C# и вывести результаты, достаточно написать всего 3 строки кода:

using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Data.Linq; using System.Data.Linq.Mapping; namespace LinqtoSQL { class Program { static void Main(string args) { var db = new northwindDataContext(); foreach (var r in db.Ten_Most_Expensive_Products()) Console.WriteLine(r.TenMostExpensiveProducts + "\t" + r.UnitPrice); Console.ReadKey(); } } }

Обратите внимание, что входная точка в базу данных теперь создается конструктором класса northwindDataContext (в общем случае класс будет называться так: {имя файла отображения} DataContext ), которому больше не нужно передавать параметром строку соединения в явном виде.

10.1.3. ADO.NET Entity Framework

ADO.NET Entity Framework (EF) – объектно-ориентированная технология доступа к данным, является object-relational mapping (ORM) решением для.NET Framework от Microsoft . Предоставляет возможность взаимодействия с объектами как посредством LINQ в виде LINQ to Entities, так и с использованием Entity SQL. Для облегчения построения Веб-решений используется как ADO.NET Data Services, так и связка из Windows Communication Foundation и Windows Presentation Foundation, позволяющая строить многоуровневые приложения, реализуя один из шаблонов проектирования MVC, MVP или MVVM.

Платформа ADO.NET Entity Framework позволяет разработчикам создавать приложения для доступа к данным, работающие с концептуальной моделью приложения, а не напрямую с реляционной схемой хранения. Ее целью является уменьшение объема кода и усилий по обслуживанию приложений, ориентированных на обработку данных. Приложения Entity Framework дают следующие преимущества .

  • приложения могут работать концептуальной моделью в терминах предметной области – в том числе с наследуемыми типами, сложными элементами и связями;
  • приложения освобождаются от жестких зависимостей от конкретного ядра СУБД или схемы хранения;
  • сопоставления между концептуальной моделью и схемой, специфичной для конкретного хранилища, могут меняться без изменения кода приложения;
  • разработчики имеют возможность работать с согласованной моделью объектов приложения, которая может быть сопоставлена с различными схемами хранения, которые, возможно, реализованы в различных системах управления данными;
  • несколько концептуальных моделей могут быть сопоставлены с единой схемой хранения;
  • поддержка интегрированных в язык запросов (LINQ ) обеспечивает во время компиляции проверку синтаксиса запроса относительно концептуальной модели.
10.1.3.1. Компоненты Entity Framework

Платформа Entity Framework представляет собой набор технологий ADO.NET, обеспечивающих разработку приложений, связанных с обработкой данных . Архитекторам и разработчикам приложений, ориентированных на обработку данных, приходится учитывать необходимость достижения двух совершенно различных целей. Они должны моделировать сущности, связи и логику решаемых бизнес-задач, а также работать с ядрами СУБД, используемыми для сохранения и получения данных. Данные могут распределяться по нескольким системам хранения данных, в каждой из которых применяются свои протоколы, но даже в приложениях, работающих с одной системой хранения данных, необходимо поддерживать баланс между требованиями системы хранения данных и требованиями написания эффективного и удобного для обслуживания кода приложения.

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

Платформа Entity Framework является компонентом.NET Framework, поэтому приложения Entity Framework можно запускать на любом компьютере, на котором установлен.NET Framework 3.5 с пакетом обновления 1 (SP1).

10.1.3.1.1. Применение концептуальных моделей на практике

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

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

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

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

  1. Добавление записи
  2. Чтение записей
  3. Редактирование записи
  4. Удаление записи

Создадим новый проект. В этот раз тип проекта будет "Приложение Windows Forms":

Добавим элемент DataGridView на нашу форму. Также добавим 3 кнопки: "Добавление", "Редактирование", "Удаление". Добавим ещё 2 элемента "TextBox", в которые будем выводить информацию о записи, которая в данный момент выделена (эти 2 "TextBox" будут использованы ещё для добавления новой записи). Добавим ещё один "TextBox", в который будет выводиться информация об "ID" записи (это нужно будет для редактирования записи). Также добавим 2 элемента "label". В конечном итоге наша форма будет выглядеть следующим образом:

Будем использовать подход "Code First". Создадим следующий класс:

Public class Countries { public int Id { get; set; } public string Country { get; set; } public string Capital { get; set; } }

Основной код будет в файле "Form1.cs". Сразу приведу весь код, в комментариях он будет подробно рассмотрен:

Using System; using System.Windows.Forms; using System.Data.Entity; using System.Data.Entity.Migrations; namespace WorkWithDataInEF { public partial class Form1: Form { private MyModel db; public Form1() { // Создаём объект нашего контекста db = new MyModel(); InitializeComponent(); // Загружаем данные из таблицы в кэш db.Countries.Load(); // Привязываем данные к dataGridView dataGridView1.DataSource = db.Countries.Local.ToBindingList(); } // Событие: клик по ячейке таблицы private void dataGridView1_CellClick(object sender, DataGridViewCellEventArgs e) { // Проверка выборки строк // Если строка не выбрана, то дальше ничего не происходит if (dataGridView1.CurrentRow == null) return; // Получаем выделенную строку и приводим её у типу Countries Countries country = dataGridView1.CurrentRow.DataBoundItem as Countries; // Если мы щёлкаем по пустой строке, то ничего дальше не делаем if (country == null) return; // Выводим данные о стране и её столице в TextBox tb_Country.Text = country.Country; tb_Capital.Text = country.Capital; tB_ID.Text = country.Id.ToString(); } // Добавление записи private void btn_Add_Click(object sender, EventArgs e) { // Проверяем, что в текстовых полях есть данные if (tb_Country.Text == String.Empty || tb_Capital.Text == String.Empty) { MessageBox.Show("Заполните данные о стране!"); return; } // Создаём экземпляр класса Countries, // т.е получаем данные о нашей стране из текстовых полей Countries country = new Countries { Country = tb_Country.Text, Capital = tb_Capital.Text }; // Заносим данные в нашу таблицу db.Countries.Add(country); // Обязательно сохраняем изменения db.SaveChanges(); // Обновляем наш dataGridView, чтобы в нём отобразилась новая страна dataGridView1.Refresh(); // Обнуляем текстовые поля tb_Country.Text = String.Empty; tb_Capital.Text = String.Empty; tB_ID.Text = String.Empty; } // Редактирование записи private void btn_Edit_Click(object sender, EventArgs e) { // Проверяем, что выбрана запись if (tB_ID.Text == String.Empty) return; // Получаем id из текстового поля int id = Convert.ToInt32(tB_ID.Text); // Находим страну по этому id с помощью метода Find() Countries country = db.Countries.Find(id); if (country == null) return; country.Country = tb_Country.Text; country.Capital = tb_Capital.Text; // Добавляем или обновляем запись db.Countries.AddOrUpdate(country); db.SaveChanges(); dataGridView1.Refresh(); } // Удаление записи // Всё аналогично редактирования записи, только используется метод Remove() private void btn_delete_Click(object sender, EventArgs e) { if (tB_ID.Text == String.Empty) return; int id = Convert.ToInt32(tB_ID.Text); Countries country = db.Countries.Find(id); if (country == null) return; country.Country = tb_Country.Text; country.Capital = tb_Capital.Text; db.Countries.Remove(country); db.SaveChanges(); dataGridView1.Refresh(); } } }

Как мы видим — всё работает.

Самым известным, функциональным и широко используемым ORM в мире .NET является Entity Framework . Для работы с .NET Core была создана версия EF Core . Принцип работы EF Core остался тем же что у его предшественников, но это уже другая технология, так что сейчас ожидать полного набора функционала из Entity Framework 6 не приходится, но разработка проекта продолжается весьма активно. Из данной статьи вы узнаете как быстро приступить к использованию Entity Framework Core в ASP.NET Core проектах.

Для начала работы с EF Core необходимо установить необходимые библиотеки. Добавим пакеты Microsoft.EntityFrameworkCore.SqlServer и Microsoft.EntityFrameworkCore.Tools .

Для начала необходимо определиться с данными, которые будут храниться в базе данных. Я добавлю 2 класса User и Log , которые будут отвечать за данные пользователя и какие-то данные лога.

Public class User { public Guid Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string Email { get; set; } } public class Log { public long Id { get; set; } public DateTime UtcTime { get; set; } public string Data { get; set; } }

После этого можно создать контекст для работы с базой данных:

Class TestDbContext: DbContext { public TestDbContext(DbContextOptions options) : base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity() .HasIndex(b => b.Email); } public DbSet Users { get; set; } public DbSet Logs { get; set; } }

DbContext — данный класс определяет контекст данных, используемый для работы с базой данных.

DbSet — представляет коллекцию всех сущностей указанного типа, которые содержатся в контексте или могут быть запрошены из базы данных.

Сейчас необходимо настроить подключение к базе данных. Для этого откроем файл Startup.cs и в метод ConfigureServices добавим строку:

Services.AddDbContext(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

Строку подключения зададим в файле конфигурации, по умолчанию это appsettings.json

"ConnectionStrings": { "DefaultConnection": "СТРОКА_ПОДКЛЮЧЕНИЯ_ЗДЕСЬ" }

Создание базы данных

Откроем Package Manager Console

И выполним команду Add-Migration InitialCreate

Это создаст файлы необходимые для создания структуры базы данных. Созданные файлы можно увидеть в созданной директории Migrations .

После этого выполним команду Update-Database и база будет создана.

Добавление логирования запросов

Сейчас давайте добавим логирование для всех API запросов. Для этого добавим класс LoggingMiddleware со следующим содержанием:

Public class LoggingMiddleware { private readonly RequestDelegate _next; private readonly ILogger _logger; public LoggingMiddleware(RequestDelegate next, ILogger logger) { _next = next; _logger = logger; } public async Task Invoke(HttpContext context, TestDbContext dbcontext) { if (context != null && context.Request.Path.ToString().ToLower().StartsWith("/api")) { using (var loggableResponseStream = new MemoryStream()) { var originalResponseStream = context.Response.Body; context.Response.Body = loggableResponseStream; try { var request = await FormatRequest(context.Request); await _next(context); var response = await FormatResponse(loggableResponseStream); loggableResponseStream.Seek(0, SeekOrigin.Begin); var newLog = new Log { Path = HttpUtility.UrlDecode(context.Request.Path + context.Request.QueryString), UtcTime = DateTime.UtcNow, Data = request, Response = response, StatusCode = context.Response.StatusCode, }; await loggableResponseStream.CopyToAsync(originalResponseStream); await dbcontext.Logs.AddAsync(newLog); dbcontext.SaveChanges(); } catch (Exception ex) { //Здесь можно добавить логирование ошибок throw; } finally { context.Response.Body = originalResponseStream; } } } } private static async Task FormatRequest(HttpRequest request) { request.EnableRewind(); string responseBody = new StreamReader(request.Body).ReadToEnd(); request.Body.Position = 0; return responseBody; } private static async Task FormatResponse(Stream loggableResponseStream) { loggableResponseStream.Position = 0; var buffer = new byte; await loggableResponseStream.ReadAsync(buffer, 0, buffer.Length); return JsonConvert.SerializeObject(Encoding.UTF8.GetString(buffer)); } }

А в файле Startup.cs в методе Configure добавим:

App.UseMiddleware();

После этого все запросы, которые начинаются с /api будут логироваться в базу данных. Здесь стоит заметить, что вариантов логирования может быть множество. Здесь представлен лишь один из вариантов, который легко реализуем и не требует использования сторонних библиотек либо сервисов.

Еще добавим контроллер для работы с пользователями, добавим там метод для получения информации обо всех пользователях.

")] public class UsersController: Controller { private readonly TestDbContext _context; public UsersController(TestDbContext context) { _context = context; } public async Task GetAll() { var users = await _context.Users.ToListAsync(); return Ok(users); } }

Это базовая информация, которая позволит вам быстро начать работать с Entity Framework Core .

Приятного программирования.

Последнее обновление: 31.10.2015

Entity Framework представляет специальную объектно-ориентированную технологию на базе фреймворка.NET для работы с данными. Если традиционные средства ADO.NET позволяют создавать подключения, команды и прочие объекты для взаимодействия с базами данных, то Entity Framework представляет собой более высокий уровень абстракции, который позволяет абстрагироваться от самой базы данных и работать с данными независимо от типа хранилища. Если на физическом уровне мы оперируем таблицами, индексами, первичными и внешними ключами, но на концептуальном уровне, который нам предлагает Entity Framework, мы уже работает с объектами.

Первая версия Entity Framework - 1.0 вышла еще в 2008 году и представляла очень ограниченную функциональность, базовую поддержку ORM (object-relational mapping - отображения данных на реальные объекты) и один единственный подход к взаимодействию с бд - Database First. С выходом версии 4.0 в 2010 году многое изменилось - с этого времени Entity Framework стал рекомендуемой технологией для доступа к данным, а в сам фреймворк были введены новые возможности взаимодействия с бд - подходы Model First и Code First.

Дополнительные улучшения функционала последовали с выходом версии 5.0 в 2012 году. И наконец, в 2013 году был выпущен Entity Framework 6.0, обладающий возможностью асинхронного доступа к данным.

Центральной концепцией Entity Framework является понятие сущности или entity. Сущность представляет набор данных, ассоциированных с определенным объектом. Поэтому данная технология предполагает работу не с таблицами, а с объектами и их наборами.

Любая сущность, как и любой объект из реального мира, обладает рядом свойств. Например, если сущность описывает человека, то мы можем выделить такие свойства, как имя, фамилия, рост, возраст, вес. Свойства необязательно представляют простые данные типа int, но и могут представлять более комплексные структуры данных. И у каждой сущности может быть одно или несколько свойств, которые будут отличать эту сущность от других и будут уникально определять эту сущность. Подобные свойства называют ключами .

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

Отличительной чертой Entity Framework является использование запросов LINQ для выборки данных из БД. С помощью LINQ мы можем не только извлекать определенные строки, хранящие объекты, из бд, но и получать объекты, связанные различными ассоциативными связями.

Другим ключевым понятием является Entity Data Model . Эта модель сопоставляет классы сущностей с реальными таблицами в БД.

Entity Data Model состоит из трех уровней: концептуального, уровень хранилища и уровень сопоставления (маппинга).

На концептуальном уровне происходит определение классов сущностей, используемых в приложении.

Уровень хранилища определяет таблицы, столбцы, отношения между таблицами и типы данных, с которыми сопоставляется используемая база данных.

Уровень сопоставления (маппинга) служит посредником между предыдущими двумя, определяя сопоставление между свойствами класса сущности и столбцами таблиц.

Таким образом, мы можем через классы, определенные в приложении, взаимодействовать с таблицами из базы данных.

Способы взаимодействия с БД

Entity Framework предполагает три возможных способа взаимодействия с базой данных:

    Database first : Entity Framework создает набор классов, которые отражают модель конкретной базы данных

    Model first : сначала разработчик создает модель базы данных, по которой затем Entity Framework создает реальную базу данных на сервере.

    Code first : разработчик создает класс модели данных, которые будут храниться в бд, а затем Entity Framework по этой модели генерирует базу данных и ее таблицы

Предположим достаточно типовой сценарий — добавление множества объектов в базу:

Бездумное выключение свойства AutoDetectChangesEnabled может привести к нежелательным последствиям (потеря изменений, исключения из-за нарушения целостности данных), поэтому наиболее простое правило я бы сформулировал так — если ваш код не предполагает дальнейшего изменения добавленных в контекст объектов в пределах той же сессии, то это свойство можно смело отключать. Такая ситуация встречается довольно часто — типовой CRUD API обычно получает объект извне и либо просто его добавляет, либо еще определяет, какие были сделаны изменения с момента вычитки, и соответствующим образом обновляет информацию о состоянии объекта в контексте (например, с помощью GraphDiff , или с использованием self-tracked entities, или любых других похожих решений). Сам объект при этом не изменяется.

Постоянная перекомпиляция некоторых запросов

Начиная с Entity Framework 5, запросы автоматически кешируются после компиляции, что позволяет значительно ускорить их последующие выполнения — текст SQL запроса будет взят из кеша, остается только подставить требуемые значения параметров. Но есть несколько ситуаций, в которых компиляция будет выполняться при каждом выполнении.

Использование Contains по коллекции в памяти

На практике нередко возникает необходимость добавить в запрос условие, аналогичное SQL-оператору IN — проверить, совпадает ли значение свойства с каким-нибудь из элементов коллекции. Например, вот так:

List channels = new List { 1, 5, 9 }; dataContext.Entities .AsNoTracking() .Where(e => channels.Contains(e.Channel)) .ToList();
Это выражение в итоге преобразуется в SQL следующего вида:

SELECT . AS , . AS , . AS FROM . AS WHERE . IN (1, 5, 9)
Получается, что для оператора IN параметры не используются, а вместо этого подставляются сами значения. Такой запрос закешировать не получится, т.к. при использовании коллекции с другим содержимым текст запроса нужно будет перегенерировать. Это, кстати, бьет не только по производительности самого Entity Framework, но и по серверу базы данных, так как для любого нового списка значений в операторе IN сервер должен будет заново построить и закешировать план выполнения.

Если в коллекции, по которой делается Contains не ожидается большого числа элементов (скажем, не больше ста), проблему можно решить динамической генерацией условий, соединенных оператором OR. Это легко сделать, например, с помощью библиотеки LinqKit :

List channels = new List { 1, 5, 9 }; var channelsCondition = PredicateBuilder.False(); channelsCondition = channels.Aggregate(channelsCondition, (current, value) => current.Or(e => e.Channel == value).Expand()); var query = dataContext.Entities .AsNoTracking() .Where(channelsCondition);
В итоге получаем уже параметризированный запрос:

SELECT . AS , . AS , . AS FROM . AS WHERE . IN (@p__linq__0,@p__linq__1,@p__linq__2)
Несмотря на то, что динамическое построение запроса выглядит дополнительной затратной работой, на практике на него уходит сравнительно немного процессорного времени. В одной из реальных задач построение запроса при каждом вызове занимало больше секунды. А замена Contains на подобное динамическое выражение уменьшило время обработки запросов (кроме первого) до десятков миллисекунд.

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

Во многих проектах возникает необходимость реализовать пейджинг для результатов поиска. Очевидным решением для выборки нужной порции записей тут являются функции Take и Skip:

Int pageSize = 10; int startFrom = 10; var query = dataContext.Entities .AsNoTracking() .OrderBy(e => e.Name) .Skip(startFrom) .Take(pageSize);
Посмотрим, какой в этом случае будет SQL:

SELECT . AS , . AS , . AS FROM . AS ORDER BY . ASC OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY
И размер страницы, и величина смещения указаны в запросе константами, а не параметрами. Это, опять же, говорит о том, что текст запроса кешироваться не будет. К счастью, начиная с Entity Framework 6 есть простая возможность обойти эту проблему — использовать лямбда-выражения в функциях Take и Skip:

Var query = dataContext.Entities .AsNoTracking() .OrderBy(e => e.Name) .Skip(() => startFrom) .Take(() => pageSize);
И результирующий запрос будет содержать параметры вместо констант:

SELECT . AS , . AS , . AS FROM . AS ORDER BY . ASC OFFSET @p__linq__0 ROWS FETCH NEXT @p__linq__1 ROWS ONLY

Большое количество Include в одном запросе

Очевидно, самый простой способ прочитать данные из базы вместе с дочерними коллекциями и другими навигационными свойствами — это использовать метод Include(). Независимо от количества Include() в LINQ запросе, по итогу будет сформирован один SQL запрос, который возвращает все указанные данные. Может сложиться впечатление, что в рамках Entity Framework такой подход для вычитки сложных объектов будет наиболее оптимальным в любой ситуации. Но это не совсем так.

Для начала рассмотрим структуру итогового SQL запроса. Например, у нас есть LINQ запрос с двумя Include для коллекций.

Var query = c.GuidKeySequentialParentEntities .AsNoTracking() .Include(e => e.Children1) .Include(e => e.Children2) .Where(e => e.Id == sequentialGuidKey);
Соответствующий SQL будет содержать UNION ALL:

SELECT . AS , . AS , . AS , . AS , . AS , . AS , . AS , . AS , . AS , . AS FROM (SELECT CASE WHEN (. IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS , 1 AS , . AS , . AS , . AS , . AS , . AS , CAST(NULL AS uniqueidentifier) AS , CAST(NULL AS varchar(1)) AS , CAST(NULL AS uniqueidentifier) AS FROM . AS LEFT OUTER JOIN . AS ON . = . WHERE . = @p__linq__0 UNION ALL SELECT 2 AS , 2 AS , . AS , . AS , CAST(NULL AS uniqueidentifier) AS , CAST(NULL AS varchar(1)) AS , CAST(NULL AS uniqueidentifier) AS , . AS , . AS , . AS FROM . AS INNER JOIN . AS ON . = . WHERE . = @p__linq__0) AS ORDER BY . ASC, . ASC
Логично было бы предположить, что Include() просто добавляет еще один JOIN в запрос. Но Entity Framework ведет себя сложнее. Если включаемое навигационное свойство — единичный объект, а не коллекция, то будет просто еще один JOIN. Если коллекция — то под каждую будет сформирован отдельный подзапрос, где родительская таблица соединяется с дочерней, а все такие подзапросы будут объединены в общий UNION ALL. Очевидно, что если нужна только одна дочерняя коллекция, то UNION ALL не будет. Схематически это можно изобразить так:

SELECT /* список полей */ FROM (SELECT /* список полей */ FROM /* родительская таблица */ LEFT OUTER JOIN /* дочерняя таблица 1 */ WHERE /* общее условие */ UNION ALL SELECT /* список полей */ FROM /* родительская таблица */ INNER JOIN /* дочерняя таблица 2 */ WHERE /* общее условие */ UNION ALL SELECT /* список полей */ FROM /* родительская таблица */ INNER JOIN /* дочерняя таблица 3 */ WHERE /* общее условие */ /* ... */ ORDER BY /* список полей */
Сделано это для борьбы с проблемой перемножения результатов. Предположим, у объекта есть три дочерних коллекции по 10 элементов в каждой. Если все три добавить через OUTER JOIN напрямую в «главный» запрос, то в результате будет 10 * 10 * 10 = 1000 записей. Если же пойти путем Entity Framework, и эти три коллекции собирать в один запрос через UNION, то получим 30 записей. Чем больше коллекций и элементов в них, тем выигрыш подхода с UNION очевиднее.

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

Основная идея альтернативных решений — это вычитка каждой коллекции отдельным запросом. Наиболее простой вариант возможен, если объекты при выборке добавляются в контекст, т.е. без использования AsNoTracking():

Var children1 = c.ChildEntities1 .Where(e => e.Parent.CreatedAt >= DbFunctions.AddDays(DateTime.UtcNow, -1)) var children2 = c.ChildEntities2 .Where(e => e.Parent.CreatedAt >= DbFunctions.AddDays(DateTime.UtcNow, -1)) children1.Load(); children2.Load(); var query = c.ParentEntities .Where(e => e.CreatedAt >= DbFunctions.AddDays(DateTime.UtcNow, -1)) .ToList();
Получается, что для каждой дочерней коллекции мы вычитываем все объекты, которые имеют отношение к родительским сущностям, попадающим под критерий запроса. После вызова Load() объекты добавляются в контекст. Во время вычитки родительских сущностей Entity Framework найдет все дочерние, находящиеся в контексте, и соответствующим образом добавит на них ссылки.

Основной недостаток здесь — то, что на каждый запрос идет отдельное обращение к серверу базы данных. К счастью, есть способ решить и эту проблему. В библиотеке EntityFramework.Extended есть возможность создавать «будущие» запросы. Основная идея в том, что все запросы, у которых был вызван extension method Future(), будут посланы в одном обращении к серверу, когда у какого-либо из них будет вызван терминальный метод:

Var children1 = c.ChildEntities1 .Where(e => e.Parent.CreatedAt >= DbFunctions.AddDays(DateTime.UtcNow, -1)) .Future(); var children2 = c.ChildEntities2 .Where(e => e.Parent.CreatedAt >= DbFunctions.AddDays(DateTime.UtcNow, -1)) .Future(); var results = c.ParentEntities .Where(e => e.CreatedAt >= DbFunctions.AddDays(DateTime.UtcNow, -1)) .Future() .ToList();
По итогу, как и в первом примере, объекты из коллекции results будут содержать корректно заполненные коллекции Children1 и Children2, причем все данные будут получены за одно обращение к серверу.

Использование «будущих» запросов будет полезно в любых ситуациях, где есть необходимость выполнять несколько отдельных запросов.

Вычитка полей только из базовой сущности при использовании Table Per Type маппинга

Представим себе систему, в которой ряд сущностей имеет базовый класс, содержащий их общие характеристики (название, дата создания, владелец, статус и т.д.). Также есть требование реализовать поиск по этим характеристикам и отображение списка результатов. Отображение подразумевает, опять же, использование только базовых характеристик.

С точки зрения гибкости модели под эту задачу хорошо подходит Table Per Type маппинг, где под каждый тип создается отдельная таблица. Например, у нас есть базовый класс Vehicle и наследники — PassengerCar, Truck, Motorcycle. В этом случае в базе будет создано четыре таблицы.

Напишем запрос, который вычитывает результаты поиска по какому-либо критерию. Например, дата добавления не ранее 10 дней назад:

Var vehicles = context.Vehicles .AsNoTracking() .Where(v => v.CreatedAt >= DbFunctions.AddDays(DateTime.UtcNow, -10)) .ToList();
И посмотрим, во что его преобразует Entity Framework:

SELECT CASE WHEN ((NOT ((. = 1) AND (. IS NOT NULL))) AND (NOT ((. = 1) AND (. IS NOT NULL))) AND (NOT ((. = 1) AND (. IS NOT NULL)))) THEN "0X" WHEN ((. = 1) AND (. IS NOT NULL)) THEN "0X0X" WHEN ((. = 1) AND (. IS NOT NULL)) THEN "0X1X" ELSE "0X2X" END AS , . AS , . AS , . AS , CASE WHEN ((NOT ((. = 1) AND (. IS NOT NULL))) AND (NOT ((. = 1) AND (. IS NOT NULL))) AND (NOT ((. = 1) AND (. IS NOT NULL)))) THEN CAST(NULL AS bit) WHEN ((. = 1) AND (. IS NOT NULL)) THEN . WHEN ((. = 1) AND (. IS NOT NULL)) THEN CAST(NULL AS bit) END AS , CASE WHEN ((NOT ((. = 1) AND (. IS NOT NULL))) AND (NOT ((. = 1) AND (. IS NOT NULL))) AND (NOT ((. = 1) AND (. IS NOT NULL)))) THEN CAST(NULL AS int) WHEN ((. = 1) AND (. IS NOT NULL)) THEN CAST(NULL AS int) WHEN ((. = 1) AND (. IS NOT NULL)) THEN . END AS , CASE WHEN ((NOT ((. = 1) AND (. IS NOT NULL))) AND (NOT ((. = 1) AND (. IS NOT NULL))) AND (NOT ((. = 1) AND (. IS NOT NULL)))) THEN CAST(NULL AS int) WHEN ((. = 1) AND (. IS NOT NULL)) THEN CAST(NULL AS int) WHEN ((. = 1) AND (. IS NOT NULL)) THEN CAST(NULL AS int) ELSE . END AS FROM . AS LEFT OUTER JOIN (SELECT . AS , . AS , cast(1 as bit) AS FROM . AS ) AS ON . = . LEFT OUTER JOIN (SELECT . AS , . AS , cast(1 as bit) AS FROM . AS ) AS ON . = . LEFT OUTER JOIN (SELECT . AS , . AS , cast(1 as bit) AS FROM . AS ) AS ON . = . WHERE . >
Получается, что нам нужна только базовая информация, а Entity Framework вычитывает всю, причем достаточно громоздким запросом. На самом деле в данной конкретной ситуации ничего плохого нет — несмотря на то, что мы выбираем объекты из коллекции базовых классов, фреймворк должен соблюдать полиморфное поведение и возвращать объект того типа, которым он был создан.

Основной вопрос здесь — как упростить запрос, чтобы он не читал лишнее? К счастью, начиная с Entity Framework 5 такая возможность есть — это использование проекции. Просто создаем объект другого типа или анонимный, используя для его заполнения только свойств базовой сущности:

Var vehicles = context.Vehicles .AsNoTracking() .Where(v => v.CreatedAt >= DbFunctions.AddDays(DateTime.UtcNow, -10)) .Select(v => new { Id = v.Id, CreatedAt = v.CreatedAt, Name = v.Name }) .ToList();
И все становится намного проще:

SELECT 1 AS , . AS , . AS , . AS FROM . AS WHERE . >= (DATEADD (day, -10, SysUtcDateTime()))
Но есть и неприятные новости – если в базовом классе есть коллекция, и ее нужно вычитывать, проблема остается. Вот пример:

Var vehicles = context.Vehicles .AsNoTracking() .Where(v => v.CreatedAt >= DbFunctions.AddDays(DateTime.UtcNow, -10)) .Select(v => new { Id = v.Id, CreatedAt = v.CreatedAt, Name = v.Name, ServiceTickets = v.ServiceTickets }) .ToList();
И сгенерированный для него SQL:

SELECT . AS , . AS , . AS , . AS , . AS , . AS , . AS , . AS , . AS , . AS , . AS FROM (SELECT . AS , . AS , . AS , . AS , . AS , . AS , 1 AS , . AS , . AS , . AS , CASE WHEN (. IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS FROM . AS LEFT OUTER JOIN . AS ON . = . LEFT OUTER JOIN . AS ON . = . LEFT OUTER JOIN . AS ON . = . LEFT OUTER JOIN . AS ON . = . WHERE . >= (DATEADD (day, -10, SysUtcDateTime()))) AS ORDER BY . ASC, . ASC, . ASC, . ASC, . ASC
Я создавал тикет для Entity Framework на эту тему: https://entityframework.codeplex.com/workitem/2814 , но мне вежливо ответили, что в виду большой сложности и опасности все разломать, они это исправлять не будут.

В некоторых случаях, когда размер базы и/или количество объектов-наследников невелики, с этим можно жить. Если подобные запросы начинают ощутимо ухудшать производительность, нужно искать решения. Раз на уровне самого фреймворка проблему предотвратить нельзя, нужен обходной путь. Наиболее простой вариант здесь — дочитывать коллекции отдельными запросами, например:

//Создаем базовый запрос var vehiclesQuery = context.Vehicles .AsNoTracking() .Where(v => v.CreatedAt >= DbFunctions.AddDays(DateTime.UtcNow, -10)); //Вычитываем объекты с помощью проекции на вспомогательный класс, игнорируя коллекции var vehicles = vehiclesQuery .Select(v => new VehicleDto { Id = v.Id, CreatedAt = v.CreatedAt, Name = v.Name }) .ToList(); //Дочитываем элементы коллекции, относящиеся к любому из объектов, возвращаемых исходным запросом var serviceTickets = context.ServiceTickets .AsNoTracking() .Where(s => vehiclesQuery.Any(v => v.Id == s.VehicleId)) .ToList(); //Раскладываем элементы по соответствующим объектам vehicles.ForEach(v => v.ServiceTickets .AddRange(serviceTickets.Where(s => s.VehicleId == v.Id)));
Универсального рецепта здесь нет, и приведенное выше решение может не дать выигрыша во всех случаях. Например, базовый запрос может оказаться достаточно сложным, и выполнять его по новой для каждой коллекции будет накладно. Попытаться обойти эту проблему можно через получение списка идентификаторов из результатов базового запроса, а потом использование его во всех дальнейших подзапросах. Но если результатов много, выигрыша может и не быть. К тому же, в этом случае следует помнить о том, что было сказано ранее о методе Contains, который явно напрашивается для поиска по идентификаторам.

Общий подход к решению проблемы я бы сформулировал так — если есть возможность не использовать Table Per Type маппинг, лучше его не использовать. В тех случаях, когда без него сложно обойтись, нужно попробовать варианты, описанные выше, и посмотреть, дают ли они выигрыш.

Дополнительная информация

Нюансы, связанные с производительностью, на которые следует обратить внимание при работе с Entity Framework (в том числе и описанные в статье) кратко описаны по этой ссылке: https://msdn.microsoft.com/en-us/data/hh949853.aspx . К сожалению, не для всех проблем указаны альтернативные решения, но информация все равно очень полезная. Также следует отметить, что как минимум пункт 4.3 на практике не подтверждается для Entity Framework 6.1.3.

Вы можете помочь и перевести немного средств на развитие сайта