Оптимизация запросов entity framework

Обновлено: 06.07.2024

Я хотел бы закончить начатое в предыдущих нескольких заметках (часть 1, часть 2) и докладе дело и привести набор методов по оптимизации производительности L2S и EF. В какой-то степени эту заметку можно считать текстом к презентации. Наверняка, это не все, что можно сделать с этой точки зрения, но это как минимум основные способы, которые могут стать отправной точкой для ваших собственных действий.

Зачем нам все это нужно?

Первым делом давайте обсудим, а зачем вообще нужно оптимизировать производительность этих инструментов? Неужели она настолько плоха, что ими нельзя пользоваться без этого? Вовсе нет, можно и даже нужно! Просто нужно понимать, что за огромное ускорение времени разработки, исчезновение нудного процесса создания Data Access Layer вручную и снижение затрат на дальнейшие изменения в базе данных и модели нужно чем-то платить. А платить приходится двумя вещами: временем на изучение нового инструмента (однажды) и некоторой потерей гибкости и производительности. В разных ORM потери производительности разные, но все же вы должны отдавать себе отчет в том, где и когда использовать ORM, а где и когда - нет.

За год работы с Entity Framework на достаточно крупном проекте я использовал в лучшем случае пару способов из приведенных ниже. И это несмотря на то, что как оказалось для нашего приложения использование ORM было, возможно, не самым лучшим выбором. У нас было несколько бизнес правил, которые заставляли нас вытаскивать чуть ли не полбазы в память, чтобы подсчитать некоторые данные для всего лишь одной сущности! При этом из-за иерархичности данных считать их в базе было еще большим злом, чем считать в памяти (поверьте, мы пробовали и меряли производительность). Поэтому, как вы сами понимаете, за dotTrace я брался довольно часто. Но, как правило, обычно все заканчивалось тем, что я находил либо ляп в коде, либо добавлял локальный кеш, чтобы не вытаскивать одни и те же данные из EF сотни, а то и тысячи раз. Надо сказать, что почти все проблемы с производительностью мы побороли через кеширование данных и правильное использование контекста EF. Но ближе к релизу приложения я начал замечать, что мы уже оптимизировали почти все, что можно и что bottleneck медленно, но уверенно переместился к границе EF, и что дальше придется оптимизировать уже его. Следствием этого и стало исследование, результаты которого я привожу сейчас.

Основные места потери производительности

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

1. Инициализация инфраструктуры. При "холодном старте" и L2S и EF теряют здесь какое-то время. Это происходит всего лишь один раз для апп-домена, но тем не менее может стать неприятной неожиданностью. L2S справляется с этим быстрее, но в EF можно пре-генерировать его внутренние views еще до запуска приложения и таким образом сократить время инициализации в разы. То есть для EF эту потерю (иногда очень значительную при первом запросе) оптимизировать можно.

2. Накладные расходы, связанные с маппингом. И L2S, и EF имеют эти расходы, но в EF они больше, т.к. в нем намного больше вариантов маппинга: наследование, тип на таблицу, несколько типов на таблицу, несколько таблиц на тип, сложные типы. К сожалению, тут особо не разбежишься, т.к. все происходит внутри ORM. Возможно, есть какие-то хаки, но я о них на данный момент не знаю. Поэтому будем считать, что оптимизация этой задачи нам недоступна.

3. Анализ запросов. Это могут быть linq-запросы (Linq to SQL, Linq to Entities) или EntitySQL для EF. Здесь ситуация выглядит получше. Разобранные EntitySQL запросы кешируются на уровне апп-домена, что позволяет не заниматься этим несколько раз. Ключом выступает как раз само текстовое представление запроса, так что будьте внимательнее и используйте параметры. С linq-запросами сложнее. Так как linq - это дерево объектов, которое представляет собой запрос, использовать его в качестве ключа довольно сложно. Но в таком случае запрос можно "откомпилировать" один раз (то есть, грубо говоря, закешировать его разобранный вид и команду, которая будет построена на нем) и потом выполнять его многократно. Тоже помогает.

4. Генерация SQL-запросов. Как и маппинг, происходит глубоко внутри, поэтому добраться туда и как-то повлиять сложно. Но нужно сказать, что почти все провайдеры реализуют внутри себя различные способы оптимизации, такие как кеширование планов запросов. В то же время во всех ORM есть одна и та же проблема: генерируемый SQL-результат не всегда оптимален (хотя как правило это так). Причем, что интересно, EF генерирует запросы в большинстве своем более оптимально, чем L2S. Один из основных советов здесь - в случае получения на выходе неоптимального запроса просто перестройте запрос в коде: поменяйте джойны, разбейте на несколько более мелких. Это помогает.

5. Материализация. Это процесс создания объектов по полученным реляционным данным. Занимает львиную долю времени от времени выполнения самого запроса. Однако оптимизирован за счет того, что контекст как L2S, так и EF (да и сессия NHibernate тоже) сохраняет у себя внутри материализованные объекты и при повторном обращении к этому же объекту уже не создает его повторно (если вы сами не скажете). Конечно, это сделано не для оптимизации, а для того, чтобы в рамках одного контекста у вас был только один объект Заказ с в независимости от количества запросов. Ну, и чтобы трекать изменения в этих самых объектах. Но подобное "кеширование" еще и увеличивает производительность, что тоже является хорошей новостью. В защиту процесса материализации нужно добавить еще и то, что даже если вы пишете свой Data Access Layer, то вы все равно в том или ином виде занимаетесь материализацией. Вам же тоже нужно превратить записи из DataReader в какие-то объекты доменной модели. Да, ручной код более оптимален, т.к. заточен для решения лишь одной задачи, но, поверьте, ORM тоже умные люди пишут :)

Методы оптимизации производительности

Итак, какие же методы оптимизации производительности можно предложить. За конкретными цифрами и сравнениями вы можете обратиться к презентации.

1. Пре-генерация views в Entity Framework. По шагам расписана в MSDN и частично здесь , поэтому подробно останавливаться не буду.

2. Отключение трекинга объектов (tracking). Дает возможность немного улучшить производительность за счет того, что материализованные объекты не отслеживаются контекстом. Обратная сторона - сделанные в объектах изменения не могут быть сохранены в базу. Плюс (здесь я могу ошибиться, но вряд ли) отключается кеширование материализованных объектов в контексте, что в большинстве своем намного сильнее ухудшает производительность для большого количества запросов. Так что решайте сами, но мы у себя не использовали.

В L2S трекингом можно управлять через свойство контекста:

В EF - через ObjectQuery<T>, что не так удобно:

Подробнее про трекинг в EF можно почитать в MSDN.

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

4. Компиляция linq-запросов. Как я уже говорил, linq-запросы, в отличие от EntitySQL, не кешируются автоматически. Для того, чтобы скомпилировать запрос, нужно выполнить очень простой вызов:

При этом не имеет особого значения, используете вы Linq to SQL или Linq to Entities. Вы просто передаете туда либо контекст L2S, либо контекст EF.

Однако, хотел бы предостеречь тех людей, которые уже побежали переписывать свой код. Дело в том, что компиляция запроса - процесс довольно длительный, поэтому он имеет смысл лишь в том случае, если вы делаете несколько вызовов одного и того же запроса (пусть и с разными параметрами). И еще лишь в том случае, если у вас действительно сложный linq-запрос. Для случая, приведенного выше, компиляция запросов не дает существенной выгоды. Для случая с одним запросом откомпилированный запрос может работать дольше неоткомпилированного. Так что it depends.

Подробнее про компиляцию запросов можно прочитать все в том же MSDN. Также советую глянуть статью о потоке выполнения запроса в EF.

Выглядит это где-то так:

6. Использование жадной загрузки (eager loading). Во-первых, нужно сказать, что и L2S, и EF по умолчанию реализуют ленивую загрузку (lazy loading). Только L2S делает это неявно, то есть сразу при обращении к навигационному свойству или коллекции, а в EF нужно явно вызвать метод Load(). Что лучше, что хуже - можно спорить долго. Жаль, что в EF не сделали включение/выключение явного/неявного режима через какое-нибудь свойство. Поэтому нам вот пришлось самим сделать неявную ленивую загрузку через кодогенерацию (я как-нибудь расскажу об этом подробнее). Но разговор сейчас не об этом. По тестам жадная загрузка, конечно же, выигрывает у ленивой, причем иногда весьма существенно. Однако если вы загрузите кучу данных, а потом не будете ее использовать - какой вам от нее прок? В этом случае, ленивая загрузка оказывается на высоте. Как же написать запрос в "жадном" стиле? В Entity Framework это делается через ObjectQuery.Include():

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

Хоть это и не eager loading в чистом виде, но в то же время этот способ обходит вариант прохождения от Customer до его Products через навигационные свойства, которые бы подняли данные через lazy loading.

7. Оптимизация обновления данных. Здесь в первую очередь хочется отметить, что по тестам EF значительно опережает L2S в этом аспекте. Причем дело не в SQL-запросах, так как их производительность, за некоторыми исключениями, почти одинакова. Проблема кроется где-то внутри L2S, поэтому учитывайте и этот аспект при выборе способа доступа к базе данных. Для оптимизации выполнения сгенерированных SQL-запросов в L2S советую глянуть еще на атрибут UpdateCheck, который определяет будет ли сгенерирован where по всем полям сущности или только по ее Id (это делается для реализации оптимистической конкуренции). В EF такая проверка делается только по измененным колонкам, а в L2S - по всем. Также стоит подумать об реализации колонки Version для этой самой конкуренции.

8. Оптимизация SQL-запросов. Как я уже говорил, единственный способ - следить за ними и менять входные linq или EntitySQL запросы. Также по результатам осмотра можно наставить индексов в базе данных. Ну, и никто не отменял хранимые процедуры, куда можно поместить наиболее серьезные запросы. Так что основной совет здесь - не бояться браться за SQL профайлер в случае необходимости :)

Еще интересен тот факт, что EF в целом генерирует более оптимальные запросы, чем L2S. Так что имейте и это в виду.

Выводы

Если сравнивать Entity Framework с другими полноценными ORM, такими как NHibernate, LLBLGen Pro и др, то на данный момент я не могу ничего толком сказать. Надо попробовать, благо тестовое приложение, которое я написал, с легкостью расширяется другими тестовыми провайдерами. Могу лишь кое-что сказать об NHibernate, т.к. я успел его попробовать и почитал дополнительные материалы о нем. NH по производительности занимает место где-то в промежутке между L2S и EF. Плюс этот прекрасный ORM уже давно на рынке и обладает множеством других интересных способностей, например, поддержкой Persistence ignorance и кешом второго уровня, которых пока нет в Entity Framework. Так что при выборе ORM я бы однозначно смотрел и в его сторону.

На этой замечательной ноте я бы хотел завершить цикл постов про производительность L2S и EF :) Вполне возможно, в ближайшем будущем у нас появятся дополнительные данные по производительности NHibernate и еще нескольких интересных ORM. Если у вас есть желание помочь в этом - можете скачать тестовое приложение, написать свой тестовый провайдер (это делается абсолютно несложно, поверьте мне), и расшарить результаты.

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

Правильно использовать индексы

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

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

Как правило, не существует специальных знаний EF для использования индексов или диагностики проблем с производительностью. Общие базы данных, связанные с индексами, точно так же важны для приложений EF, как и приложения, не использующие EF. Ниже приведены некоторые общие рекомендации, которые следует учитывать при использовании индексов.

  • Хотя индексы ускоряют запросы, они также замедляют работу обновлений, так как они нуждаются в актуальном состоянии. Избегайте определения ненужных индексов и рассмотрите возможность использования фильтров индексов для ограничения индекса подмножеством строк, тем самым уменьшая издержки.
  • Составные индексы могут ускорить запросы, которые фильтруются по нескольким столбцам, но они также могут ускорить запросы, которые не фильтруют по всем столбцам индекса, в зависимости от порядка сортировки. Например, индекс в столбцах A и B ускоряет фильтрацию запросов по A и B, а также к запросам, которые фильтруются только по, но не ускоряют запросы только с фильтрацией по каналу B.
  • Если запрос фильтрует по выражению для столбца (например price / 2 ,), то простой индекс использовать нельзя. Однако можно определить сохраненный материализованный столбец для выражения и создать для него индекс. Некоторые базы данных также поддерживают индексы выражений, которые можно напрямую использовать для ускорения фильтрации запросов по любому выражению.
  • различные базы данных позволяют настраивать индексы различными способами, и во многих случаях EF Core поставщики предоставляют их через Fluent API. например, поставщик SQL Server позволяет указать, является ли индекс кластеризованным, или задать его коэффициент заполнения. Дополнительные сведения см. в документации поставщика.

Project только необходимые свойства

EF Core упрощает запрос экземпляров сущностей, а затем использует эти экземпляры в коде. Однако запросы к экземплярам сущностей часто запрашивают больше данных, чем требуется для базы данных. Рассмотрим следующий пример.

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

Это можно оптимизировать с помощью Select , чтобы сообщить EF, какие столбцы следует проецировать:

полученный SQL извлекает только необходимые столбцы:

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

Ограничить размер результирующего набора

По умолчанию запрос возвращает все строки, соответствующие его фильтрам:

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

Как следствие, обычно стоит подумать о том, чтобы ограничить количество результатов:

Избегайте декартового развертывания при загрузке связанных сущностей

В реляционных базах данных все связанные сущности загружаются с помощью запросов JOIN в одиночном запросе.

Если в обычном блоге есть несколько связанных записей, тогда строки для этих записей будут дублировать данные блога. Такое дублирование приводит к возникновению "картезианского взрыва" (нарушение корректности синхронизации). Чем больше связей "один ко многим" будет загружено, тем большим будет объем дублирующихся данных, что негативно повлияет на производительность приложения.

EF позволяет избежать этого воздействия с помощью "разбиения запросов", которые загружают связанные сущности через отдельные запросы. Дополнительные сведения см. в документации по разделению и отдельным запросам.

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

Загружает связанные сущности по возможности.

Перед продолжением работы с этим разделом рекомендуется прочитать выделенную страницу по связанным сущностям .

При работе со связанными сущностями обычно известно, что нам нужно загрузить. типичный пример — загрузка определенного набора блогов вместе со всеми записями. В этих сценариях всегда лучше использовать безотлагательную загрузку, чтобы EF мог извлечь все необходимые данные за один цикл. Функция фильтрации включенных элементов, введенная в EF Core 5,0, также позволяет ограничить связанные сущности, которые вы хотите загрузить, сохранив процесс загрузки и, следовательно, выполнимо в одном цикле.

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

Будьте осторожны с отложенной загрузкой

Отложенная загрузка часто кажется очень полезным способом написания логики базы данных, так как EF Core автоматически загружает связанные сущности из базы данных, так как к ним обращается ваш код. Это позволяет избежать загрузки связанных сущностей, которые не нужны (например, явная загрузка), и, в противном случае, освобождает программиста от необходимости обращаться к связанным сущностям полностью. Однако отложенная загрузка особенно уязвима для создания ненужных дополнительных обращений, которые могут замедлять работу приложения.

Рассмотрим следующий пример.

Это, казалось бы, безобидным часть кода, которая проходит по всем блогам и их записям, распечатяя их. Включение ведения журнала инструкции EF Core открывает следующее:

В чем причина? Почему все эти запросы отправляются для простых циклов выше? При отложенной загрузке записи блога загружаются только при обращении к свойству posts (отложенно). в результате каждая итерация во внутреннем операторе foreach активирует дополнительный запрос к базе данных в отдельном цикле. В результате после первоначального запроса на загрузку всех блогов у нас будет другой запрос к блогу, где загружаются все записи. Это иногда называется проблемой N + 1 и может вызвать очень значительные проблемы с производительностью.

Предполагая, что нам нужны все записи блогов, имеет смысл использовать вместо этого безотлагательную загрузку. Для выполнения загрузки можно использовать оператор include , но так как нам нужны только URL-адреса блогов (и следует загружать только те, которые необходимы). Вместо этого мы будем использовать проекцию:

Это сделает EF Core извлекать все блоги вместе с их записями в одном запросе. В некоторых случаях он также может быть полезен, чтобы избежать появления декартного развертывания с помощью запросов Split.

Поскольку отложенная загрузка упрощает непреднамеренное срабатывание задачи N + 1, рекомендуется избегать ее. Упреждающая или явная загрузка делает неясной в исходном коде, когда происходит цикл обработки базы данных.

Буферизация и потоковая передача

Буферизация означает загрузку всех результатов запроса в память, в то время как потоковая передача означает, что EF передает приложению один результат каждый раз, не содержащий весь набор результатов в памяти. В принципе, требования к памяти для запроса потоковой передачи фиксированы — они одинаковы, если запрос возвращает 1 строку или 1000; для запроса буферизации, с другой стороны, требуется больше памяти, чем больше строк. Для запросов, которые запрашивают большие результирующие наборы, это может быть важным фактором производительности.

Зависит ли буферы или потоки запросов от того, как они оцениваются:

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

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

Внутренняя буферизация EF

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

  • При наличии стратегии повторного выполнения. Это делается для того, чтобы убедиться, что те же результаты возвращаются, если запрос повторяется позже.
  • Когда используется запрос Split , результирующие наборы всех, кроме последнего запроса, буферизуются, если режим MARS не включен на SQL Server. Это связано с тем, что обычно невозможно одновременно использовать несколько результирующих наборов запросов.

Обратите внимание, что эта внутренняя буферизация выполняется в дополнение к любой буферизации, вызванной с помощью операторов LINQ. Например, если для запроса используется параметр ToList и используется стратегия повторного выполнения, то результирующий набор загружается в память дважды: один внутренне по EF и один раз по ToList.

Отслеживание, отсутствие отслеживания и разрешение идентификаторов

Перед продолжением работы с этим разделом рекомендуется прочитать выделенную страницу с отслеживанием и без отслеживания .

EF отслеживает экземпляры сущностей по умолчанию, поэтому изменения в них обнаруживаются и сохраняются при вызове метода SaveChanges . Еще одним результатом отслеживания запросов является то, что EF обнаруживает, что экземпляр уже загружен для ваших данных, и автоматически возвращает этот отслеживаемый экземпляр, а не возвращает новый. Это называется разрешением идентификации. С точки зрения производительности отслеживание изменений означает следующее:

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

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

Ниже приведены результаты отслеживания теста производительности и поведения без отслеживания для запроса, в котором загружаются 10 блогов с 20 записями. Исходный код доступен здесь, поэтому вы можете использовать его в качестве основания для собственных измерений.

Метод нумблогс нумпостсперблог Среднее значение Ошибка StdDev Median Коэффициент ратиосд Gen 0 Поколение 1 Поколение 2 Allocated
астраккинг 10 20 1 414,7 США 27,20 США 45,44 США 1 405,5 США 1.00 0,00 60,5469 13,6719 - 380,11 КБ
Asnotrackin 10 20 993,3 США 24,04 США 65,40 США 966,2 США 0,71 0,05 37,1094 6,8359 - 232,89 КБ

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

Использование необработанных SQL

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

  • используйте необработанные SQL прямо в запросе, например через фромсклрав. EF даже позволяет составлять необработанные SQL с обычными запросами LINQ, позволяя выразить только часть запроса в необработанных SQL. это хороший способ, когда необработанные SQL необходимо использовать только в одном запросе в базе кода.
  • Определите определяемую пользователем функцию (UDF), а затем вызовите ее из запросов. Обратите внимание, что, начиная с 5,0, EF позволяет функциям UDF возвращать полные результирующие наборы — они известны как функции с табличным значением (возвращающие табличное), а также допускают сопоставление с DbSet функцией, делая ее так же, как и с другой таблицей.
  • Определите представление базы данных и запрос из него в запросах. Обратите внимание, что в отличие от функций, представления не могут принимать параметры.

в качестве последнего средства, как правило, следует использовать необработанные SQL, убедившись, что EF не может создать нужные SQL, и если производительность достаточно важна для того, чтобы заданный запрос выровнять его. использование необработанных SQL приносит значительные недостатки в обслуживании.

Асинхронное программирование

Как правило, для масштабирования приложения важно всегда использовать асинхронные API, а не синхронные (например, SaveChangesAsync , а не SaveChanges). Синхронные API блокируют поток на время ввода-вывода базы данных, увеличивая потребность в потоках и число переключений контекста потоков, которые должны быть выполнены.

Дополнительные сведения см. на странице с асинхронным программированием.

Старайтесь не смешивать синхронный и асинхронный код в одном приложении — очень просто случайно запустить нехватку недостаточной работы пула потоков.


Теперь мы можем начать запрашивать данные из базы данных с помощью EF Core. Каждый запрос состоит из трех основных частей:

  • Подключение к базе данных через свойство ApplicationContext DbSet
  • Серия команд LINQ и / или EF Core
  • Выполнение запроса

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

Итак, чтобы объяснить основы запросов, мы собираемся использовать контроллер Values , как мы это делали в первой части серии и только действие Get для простоты. Мы собираемся сосредоточиться на логике EF Core, а не на веб-API в целом.

. Поэтому давайте добавим наш объект контекста в конструктор Values и напишем первый запрос в действии Get:

Из этого запроса мы можем увидеть все упомянутые части. _context.Students - это первая часть, где мы получаем доступ к таблице Student в базе данных через свойство DbSet<Student> Students .

Where(s => s.Age> 25) - вторая часть запроса, в которой мы используем команду LINQ для выбора только необходимых строк. Далее, у нас есть метод ToList() , который выполняет этот запрос.

СОВЕТ: когда мы пишем запросы только для чтения в Entity Framework Core (результат запроса не будет использоваться для каких-либо дополнительных изменений базы данных), мы всегда должны добавлять AsNoTracking способ ускорить выполнение.

В следующей статье мы поговорим о том, как EF Core изменяет данные в базе данных и отслеживает изменения в загруженной сущности. На данный момент просто знайте, что EF Core не будет отслеживать изменения (когда мы применяем AsNoTracking) в загруженном объекте, что ускорит выполнение запроса:

Различные способы построения реляционных запросов

Существуют разные подходы к получению наших данных:

  • Eager loading - жадная загрузка
  • Explicit Loading - явная загрузка
  • Select (Projection) loading - выборка (проекция)
  • Lazy loading - ленивая загрузка

В результате нашего запроса значения свойств навигации равны нулю:


Запросы реляционной базы данных с жадной загрузкой в ​​EF Core

При использовании подхода "Активная загрузка" EF Core включает взаимосвязи в результат запроса. Для этого используются два разных метода: Include() и ThenInclude() . В следующем примере мы собираемся вернуть только одного учащегося со всеми соответствующими оценками, чтобы показать, как работает метод Include() :

Перед отправкой запроса на выполнение этого запроса мы должны установить библиотеку Microsoft.AspNetCore.Mvc.NewtonsoftJson и изменить класс Startup.cs :

Это защита от ошибки «Self-referencing loop» при возврате результата из нашего API (что действительно происходит в реальных проектах). Вы можете использовать объекты DTO, чтобы избежать этой ошибки.


Мы можем взглянуть в окно консоли, чтобы увидеть, как EF Core преобразует этот запрос в команду SQL:


Мы видим, что ядро ​​EF выбирает первого студента из таблицы Student , а затем выбирает все относительные оценки.

Важно знать, что мы можем включать все сущности в наши запросы через сущность Student , потому что она имеет отношения с другими сущностями. Вот почему у нас есть только одно свойство DbSet типа DbSet в классе ApplicationContext .

Но если мы хотим написать отдельный запрос для других сущностей, например, Evaluation, мы должны добавить дополнительное свойство DbSet<Evaluation>

ThenInclude

Чтобы дополнительно изменить наш запрос для включения свойств отношения второго уровня, мы можем присоединить метод ThenInclude сразу после метода Include . Итак, с помощью метода Include мы загружаем свойства отношения первого уровня, и как только мы присоединяем ThenInclude , мы можем еще глубже погрузиться в граф отношений.

Имея это в виду, давайте дополнительно включим все предметы для выбранного ученика:

Сущность Student не имеет прямого свойства навигации для сущности Subject , поэтому мы включаем свойство навигации первого уровня StudentSubjects , а затем включите свойство навигации второго уровня Subject :


Мы можем пойти на любую глубину с помощью метода ThenInclude , потому что, если связь не существует, запрос не завершится ошибкой, он просто ничего не возвращает. Это также относится к методу Include .

Преимущества и недостатки быстрой загрузки и предупреждения консоли

Преимущество этого подхода заключается в том, что EF Core включает реляционные данные с помощью Include или ThenInclude эффективным способом, используя минимум доступа к базе данных (обходы базы данных).

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

Как мы видели, когда мы выполняем наш запрос, EF Core записывает переведенный запрос в окно консоли. Это отличная функция отладки, предоставляемая EF Core, потому что мы всегда можем решить, создали ли мы оптимальный запрос в нашем приложении, просто взглянув на переведенный результат.

Явная загрузка в Entity Framework Core

При таком подходе Entity Framework Core явно загружает отношения в уже загруженную сущность. Итак, давайте рассмотрим различные способы явной загрузки отношений:

В этом примере мы сначала загружаем объект Student . Затем мы включаем все оценки, связанные с выбранным учеником. Кроме того, мы включаем все связанные темы через свойство навигации StudentSubjects .

Важно отметить, что когда мы хотим включить коллекцию в основную сущность, мы должны использовать метод Collection , но когда мы включаем отдельную сущность в качестве свойства навигации, мы получаем использовать метод Reference .

Запросы в Entity Framework Core с явной загрузкой

При работе с явной загрузкой в ​​Entity Framework Core у нас есть дополнительная команда. Это позволяет применить запрос к отношению. Итак, вместо использования метода Load , как мы делали в предыдущем примере, мы собираемся использовать метод Query :

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

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

Select загрузка (проекция)

Этот подход использует метод Select для выбора только тех свойств, которые нам нужны в нашем результате. Давайте посмотрим на следующий пример:

Таким образом мы проецируем только те данные, которые хотим вернуть в ответ. Конечно, нам не нужно возвращать анонимный объект, как здесь. Мы можем создать наш собственный объект DTO и заполнить его в запросе проекции.

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

Ленивая загрузка в Entity Framework Core

Ленивая загрузка была введена в EF Core 2.1, и мы можем использовать ее, чтобы отложить извлечение данных из базы данных до тех пор, пока они действительно не понадобятся. Эта функция может помочь в некоторых ситуациях, но она также может снизить производительность нашего приложения, и это основная причина, по которой она стала дополнительной функцией в EF Core 2.1.

Оценка клиента и сервера

Все написанные нами запросы - это те запросы, которые EF Core может преобразовывать в команды SQL (как мы видели из окна консоли). Но в EF Core есть функция под названием Client vs Server Evaluation, которая позволяет нам включать в наш запрос методы, которые нельзя преобразовать в команды SQL. Эти команды будут выполнены, как только данные будут извлечены из базы данных.

Например, представим, что мы хотим показать одного учащегося с оценочными пояснениями в виде единой строки:

Начиная с EF Core 3.0 оценка клиента ограничивается только проекцией верхнего уровня (по сути, последним вызовом Select() ).

И вот результат запроса:


Несмотря на то, что оценка клиента и сервера позволяет нам писать сложные запросы, мы должны обращать внимание на количество строк, которые мы возвращаем из базы данных. Если мы вернем 20 000 строк, наш метод будет выполняться для каждой строки на клиенте. В некоторых случаях это может занять много времени.

Необработанные команды SQL

В EF Core есть методы, которые можно использовать для написания необработанных команд SQL для извлечения данных из базы данных. Эти методы очень полезны, когда:

  • мы не можем создавать наши запросы стандартными методами LINQ.
  • если мы хотим вызвать хранимую процедуру
  • если переведенный запрос LINQ не так эффективен, как хотелось бы.

Метод FromSqlRaw

Мы также можем вызывать хранимые процедуры из базы данных:

Метод FromSqlRaw - очень полезный метод, но он имеет некоторые ограничения:

  • Имена столбцов в нашем результате должны совпадать с именами столбцов, которым сопоставлены свойства.
  • Наш запрос должен возвращать данные для всех свойств объекта или типа запроса.
  • SQL-запрос не может содержать отношения, но мы всегда можем комбинировать FromSqlRaw с методом Include

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

Метод ExecuteSqlRaw

Метод ExecuteSqlRaw позволяет нам выполнять команды SQL, такие как Update, Insert, Delete. Давайте посмотрим, как мы можем это использовать:

Эта команда выполняет требуемую команду и возвращает количество затронутых строк. Это работает одинаково при обновлении, вставке или удалении строк из базы данных. В этом примере ExecuteSqlRaw вернет 1 в качестве результата, потому что обновлена только одна строка:


Очень важно отметить, что мы используем свойство Database для вызова этого метода, тогда как в предыдущем примере нам приходилось использовать свойство Student для FromSqlRaw метод.

Еще одна важная вещь, на которую следует обратить внимание: мы используем функцию интерполяции строк для запросов в методах FromSqlRaw и ExecuteSqlRaw , поскольку она позволяет нам чтобы поместить имя переменной в строку запроса, которую EF Core затем проверяет и преобразует в параметры. Эти параметры будут проверены, чтобы предотвратить атаки SQL-инъекций. Мы не должны использовать интерполяцию строк вне методов необработанных запросов EF Core, потому что в этом случае мы потеряем обнаружение атак Sql-инъекций.

Метод перезагрузки

Если у нас есть уже загруженная сущность, а затем мы используем метод ExecuteSqlRaw для внесения некоторых изменений в эту сущность в базе данных, наша загруженная сущность наверняка будет устаревшей. Давайте изменим наш предыдущий пример:

Как только мы выполним этот запрос, столбец Age изменится на 28, но давайте посмотрим, что произойдет с загруженным объектом studentForUpdate :


Вот оно, свойство Возраст не изменилось, хотя оно было изменено в базе данных. Конечно, это ожидаемое поведение.

Итак, теперь возникает вопрос: «Что, если мы хотим, чтобы это изменилось после выполнения метода ExecuteSqlRaw?».

Что ж, для этого нам нужно использовать метод Reload :

Теперь, когда мы снова выполняем код:


Свойство age загруженного объекта изменено.

Заключение

Мы отлично поработали. Мы рассмотрели множество тем и много узнали о запросах в Entity Framework Core.

Итак, подводя итог, мы узнали:

  • Как работают запросы в EF Core
  • О разных типах запросов и о том, как использовать каждый из них.
  • Способ использования команд Raw SQL с различными методами EF Core

В следующей статье мы узнаем о EF Core методах, которые изменяют данные в БД.

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

Создание нового консольного приложения для демонстрации работы с 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, содержащий модель данных. Модель сущностных классов должна выглядеть следующим образом:

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

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

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

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

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

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

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

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

уменьшается время компиляции, т.к. не нужно создавать кучу дополнительных объектов, как при использовании Entity SQL;

снижается риск допустить ошибку в запросе, который довольно высок при использовании запросов SQL;

удобные подсказки IntelliSense IDE-среды Visual Studio позволяют довольно быстро набирать нужные запросы;

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

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

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

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

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

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

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

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

Класс 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 означает, что мы выполняем запрос к базе данных.

Читайте также: