Entity framework рекурсивный запрос

Обновлено: 02.07.2024

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

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

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

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

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

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

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

Category и ID , Name , Parent и Children . Parent и Children из Category тоже.

когда я делаю запрос LINQ to Entities для конкретного Item , Он не возвращает связанный Category , Если я использую Include("Category") метод. Но это не приносит полную категорию, с ее родителем и детьми. Я мог бы сделать Include("Category.Parent") , но этот объект что-то вроде дерево, у меня есть рекурсивная иерархия и я не знаю, где она заканчивается.

как я могу сделать EF полностью загрузить Category , с родителем и детьми, и родитель со своим родителем и детьми, и так далее?

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

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

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

т. е. что-то вроде:

Если вы правильно написали хранимую процедуру, материализуя все элементы в иерархии (т. е. ToList() ) должен сделать EF отношения fixup пинает.

и затем нужный пункт (первый()) должны иметь все его дети загружены, и у них должны быть свои дети загружены и т. д. Все они будут заполнены из этого одного вызова хранимой процедуры, поэтому проблем с MARS тоже нет.

надеюсь, что это помогает

Алекс

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

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

что вы должны сделать, это что-то вроде следующего:

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

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

помните,что ваши контексты должны быть как можно короче.

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

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

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

вместо этого переверните свой запрос: Get Catalog и Include предметы в нем. Это даст вам все элементы как иерархически (свойства навигации) и сглажены, поэтому теперь вам просто нужно исключить некорневые элементы, присутствующие в корне, что должно быть довольно тривиальным.

у меня была эта проблема и предоставил подробный пример этого решения другому,здесь

При выборке данных выбирать нужно ровно столько сколько нужно за один раз. Никогда не извлекайте все данные из таблицы!


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


Вау, вау, вау, разогнался.

Самое время немного освежить знания по методам LINQ.

Давайте рассмотрим отличия между ToList AsEnumerable AsQueryable

Итак, ToList

  • Выполняет запрос немедленно.
  • Используйте .ToList() для форсирования получения данных и выхода из режима поздней загрузки (lazy loading), так что этот метод полезен перед тем как вы пройдетесь по данным.
  • Выполнение с задержкой (lazy loading)
  • Принимает параметр: Func <TSource, bool>
  • Загружает каждую запись в память приложения и управляет фильтрует его (в том числе Where/Take/Skip приведут к тому, что, например запрос select * from Table1,
  • загрузит результирующий набор в память, затем выберет первые N элементов)
  • В этом случает отрабатывает схема: Linq-to-SQL + Linq-to-Object.
  • Используйте IEnumerable для получения списка из базы данных в режиме поздней загрузки (lazy loading).
  • Выполнение с задержкой (lazy loading)
  • Преобразует Expression в T-SQL (с учетом специфики провайдера), удаленное исполняет запрос и возвращает результат в память приложения.
  • Вот почему DbSet (в Entity Framework) также наследуется от AsQueryable чтобы получать эффективные запросы.
  • Не загружает каждую запись, например если Take(5) это сгенерирует запрос вида «select top 5 * SQL» в фоновом режиме. Это означает, что этот подход более дружественный для SQL базы данных, и дает более скоростной результат.Так что AsQueryable() обычно работает быстрее, чем AsEnumerable() так как сначала генерирует T-SQL включающий в себя все условия Linq определённые вами.
  • Используйте AsQueryable если хотите запрос к базе данных который может быть улучшен перед запуском на стороне сервера.

Пример использования AsQueryable в простейшеем случае:

Волшебство простого чтения

Если вам не нужно менять данные, только отобразить используйте .AsNoTracking() метод.

Медленная выборка


Быстрая выборка (только на чтение)


Чувствую, вы немного уже размялись?

Типы загрузки связанных данных

Для тех, кто забыл, что такое lazy loading.

Ленивая загрузка (Lazy loading) означает, что связанные данные прозрачно загружаются из базы данных при обращении к свойству навигации. Подробнее читаем тут .

И заодно, напомню о других типах загрузки связанных данных.

Активная загрузка (Eager loading) означает, что связанные данные загружаются из базы данных как часть первоначального запроса.


Внимание! Начиная с версии EF Core 3.0.0, каждое Include будет вызывать добавление дополнительного JOIN к запросам SQL, создаваемым реляционными поставщиками, тогда как предыдущие версии генерировали дополнительные запросы SQL. Это может значительно изменить производительность ваших запросов, в лучшую или в худшую сторону. В частности, запросы LINQ с чрезвычайно большим числом операторов включения могут быть разбиты на несколько отдельных запросов LINQ.

Явная загрузка (Explicit loading) означает, что связанные данные явно загружаются из базы данных позднее.


Рывок и прорыв! Двигаемся дальше?

Готовы ускориться еще больше?

Чтобы резко ускориться при выборке сложно структурированных и даже ненормализованных данных из реляционной базы данных есть два способа сделать это: используйте индексированные представления (1) или что еще лучше – предварительно подготовленные(вычисленные) данные в простой плоской форме для отображения (2).

(1) Индексированное представление в контексте MS SQL Server

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

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

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

(2) Если нужно сделать запрос, требующий отображения более трех уровней связанных таблиц в количестве три и более c повышенной CRUD нагрузкой, лучшим способом будет задуматься о том, чтобы периодически вычислять результирующий набор, сохранять его в таблице и использовать для отображения. Результирующая таблица, в которой будут сохраняться данные должна иметь Primary Key и индексы по полям поиска в LINQ.

Что насчет асинхронности?

Да! Используем ее где только можно! Вот пример:


И да, ничего не забыли для повышения производительности? Бууум!

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

Напомню, когда выполняются запросы в Entity Framework Core.

При вызове операторов LINQ вы просто создаете представление запроса в памяти. Запрос отправляется в базу данных только после обработки результатов.

Ниже приведены наиболее распространенные операции, которые приводят к отправке запроса в базу данных.

  • Итерация результатов в цикле for.
  • Использование оператора, например ToList, ToArray, Single, Count.
  • Привязка данных результатов запроса к пользовательскому интерфейсу.

Как же организовать код EF Core с точки зрения архитектуры приложения?

(1) C точки зрения архитектуры приложения, нужно обеспечить чтобы код доступа к вашей базе данных был изолирован / отделен в четко определенном месте (в изоляции). Это позволяет найти код базы данных, который влияет на производительность.

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

Как правильно и быстро сохранять данные с помощью SaveChanges?

Если вставляемые записи одинаковые имеет смысл использовать одну операцию сохранения на все записи.

Неправильно


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

Триггеры, вычисляемые поля, пользовательские функции и EF Core

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

Параллелизм в EF Core

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

EF Core использует асинхронные запросы, которые позволяют избежать блокирования потока при выполнении запроса в базе данных. Асинхронные запросы важны для обеспечения быстрого отклика пользовательского интерфейса в толстых клиентах. Они могут также увеличить пропускную способность в веб-приложении, где можно высвободить поток для обработки других запросов. Вот пример:

А что вы знаете про компилированные запросы LINQ?

Если у вас есть приложение, которое многократно выполняет структурно похожие запросы в Entity Framework, вы часто можете повысить производительность, компилируя запрос один раз и выполняя его несколько раз с различными параметрами. Например, приложению может потребоваться получить всех клиентов в определенном городе; город указывается во время выполнения пользователем в форме. LINQ to Entities поддерживает использование для этой цели скомпилированных запросов.

Много примеров можно посмотреть тут.

Не делайте больших контекстов DbContext!

В общем так, я знаю многие из вас, если не почти все — lazy f_u__c_k__e_r__s и всю базу данных вы размещаете в один контекст, особенно это свойственно для подхода Database-First. И зря вы это делаете! Ниже приведен пример как можно разделить контекст. Конечно, таблицы соединения между контекстами придется дублировать, это минус. Так или иначе если у вас в контексте более 50 таблиц лучше подумать о его разделении.

Использование группировки контекста (pooling DdContext)

Как избежать лишних ошибок при CRUD в EF Core?

Никогда не делайте вычисления в вставку в одном коде. Всегда разделяйте формирование/подготовку объекта и его вставку/обновление. Просто разнесите по функциям: проверку введенных данным пользователем, вычисления необходимые предварительных данных, картирование или создание объекта, и собственно CRUD операцию.

Что делать, когда совсем дела плохо с производительностью приложения?

Пиво тут точно не поможет. А вот что поможет, так это разделение чтение и записи в архитектуре приложения с последующего разнесением по сокетам этих операций. Задумайтесь об использовании Command and Query Responsibility Segregation (CQRS) pattern, а также попробуйте, разделить таблицы на вставку и чтение между двумя базами данных.


Теперь мы можем начать запрашивать данные из базы данных с помощью 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 означает, что мы выполняем запрос к базе данных.

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