Entity framework вложенные запросы

Обновлено: 05.07.2024

Запрос Linq-to-Entities сначала преобразуется в дерево выражений Linq , которое затем преобразуется Object Services в дерево команд . И если запрос Linq-to-Entities гнездится для запроса Linq-to-Objects, то этот вложенный запрос также преобразуется в дерево выражений .

a) Я полагаю, что ни один из операторов вложенного запроса Linq-to-Objects фактически не выполняется, но вместо поставщик данных для конкретного БД (или, возможно, службы объектов) знает, как преобразовать логику операторов Linq-to-Objects в соответствующие операторы SQL?

b) Поставщик данных знает, как создавать эквивалентные инструкции SQL только для некоторых операторов Linq-to-Objects?

c) Точно так же поставщик данных знает, как создавать эквивалентные операторы SQL только для некоторых методов, отличных от Linq, в библиотеке классов Net Framework?

EDIT:

Я знаю только некоторые Sql, поэтому я не могу быть полностью уверен, но чтение запроса Sql, сгенерированного для вышеуказанного кода, кажется, что поставщик данных фактически не выполнял numbers.Max , но вместо этого просто как-то понял, что numbers.Max должен вернуть максимальное значение, а затем перейти к включению в сгенерированный Sql запросит вызов функции TSQL в MAX . Он также помещает все значения, хранящиеся в массиве numbers , в запрос Sql.

Исходя из этого, возможно ли, что поставщик Linq2Entities действительно не выполняет методы не Linq и Linq-to-Object, а вместо этого создает эквивалентные SQL-инструкции для некоторых из них (а для других он генерирует исключение)?

1 ответ

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

Это потому, что numbers в вашем примере есть int[] .

Интересно, что это оценивается только один раз до создания запроса, поэтому он вычисляется только один раз. Если вы сделали что-то вроде этого:

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

Также помните, что он ленив, поэтому, если вы никогда не выполняете запрос, подвыражение никогда не компилируется и не выполняется. Кроме того, если вы измените numbers после вашего оператора, но перед выполнением foreach , тогда запрос будет создан с использованием измененного значения.

Edit:

Вот статья под названием Пошаговое руководство. Создание IQueryable LINQ Provider . Проверьте раздел «Добавление оценщика выражений». В частности, класс Nominator :

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

Вот функция, которая определяет, можно ли оценивать выражение:

Итак, до тех пор, пока никакое подвыражение не содержит параметр выражения, тогда оно может быть оценено.

Изменить 2:

На основе вашего редактирования он делает похоже, что он помещает значения из массива в таблицу в SQL и выбирает MAX . Я, честно говоря, понятия не имею, почему это было бы неприятно, если ни один из элементов подвыражения numbers.Max() не является параметром. Я, конечно, знаю, что вы можете написать выражение типа numbers.Contains(c.ID) , и это фактически создаст IN , например c.ID IN (@p0, @p1, @p2, @p3, @p4) где @p0 = 1 , @p1 = 2 и т. д.

Итак, чтобы ответить на ваш вопрос: «Возможно ли, что поставщик Linq2Entities действительно не выполняет методы не Linq и Linq-to-Object, а вместо этого создает эквивалентные SQL-инструкции для некоторых из них»

ORM Entity Framework Core с каждой версией становится все более и более богатой на фичи. Команда разработчиков тратит много времени на перфоманс и вероятно простое обновление Nuget-пакета уже приведет к некоторому бусту, который почувствуют пользователи. Но сегодня я хочу рассказать о совершенно конкретной фиче: это новый режим запросов — "разделённые запросы" или "split queries" в оригинале.

На моем текущем проекте в качестве хранилища используется Postgres, доступ к которой осуществляется через драйвер Npgsql и EF Core. Одной из центральных сущностей всего бизнес-процесса является "методика выполнения ПЦР-исследования", которая по сути представляет рецепт как выполнить исследование и включает в себя довольно много информации:

  • список используемых реагентов с указанием "рецепта смешивания"
  • список результатов, которые будут определены
  • список оборудования на котором возможно выполнение исследования
  • и так далее, всего 8 вложенных коллекций

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

В самом начале мы не парили себе голову и стали использовать lazy-loading, что хорошо работало в сценариях "частичного применения": когда извлекалась методика только со списком реагентов, или методика только со списком результатов.
Но в определенных сценариях требовалось получить методику "целиком", что привело к классической проблеме N+1, когда для извлечения сущности и связанных коллекций требуется 1 запрос на извлечение сущности и еще по одному для каждого элемента коллекции.
Такое отношение к данным сильно просаживало производительность и следующим витком было включить коллекции в родительский объект с использованием LINQ -конструкций .Include().ThenInclude() .

Всё работало более менее хорошо до появления царь-методики. Это комплексный и сложный тест, в который включено много реагентов, много результатов, он использует много каналов детекции. К ней производится много форм комплектации наборов реагентов.
Так или иначе, мы получили развесистую структуру, в которой каждая вложенная коллекция имела не как обычно 3-5 записей, а по 20-30. Вот тут наш софт и сказал "кря".
Если раньше на извлечение полной сущности уходило от нескольких десятков миллисекунд до сотен, то у царь-методики это занимало до полутора десятков секунд, что иногда приводило к краху запроса в БД. Это было уже недопустимо и требовало каких-то решений.

Как пишут Майкрософт в своей статье про разделённые запросы обычно каждый LINQ-запрос преобразуется в один SQL запрос с использованием JOIN для извлечения связанных коллекций.
Я позволю себе немного перевернуть пример из документации и извлекать не блоги и посты, а посты и комменты. Пусть модель данных выглядит следующим образом:

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

Что будет транслировано примерно в такой SQL-запрос усреднённый, потому что вариаций масса:

А что если статья большая? Например автор был в ударе и выдал 100 КБ текста (это около 50 печатных листов). Комментаторы читали, наслаждались и комментировали, так что оставили 1000 комментариев. Сколько примерно будет весить результирующий набор, который надо вычитать ORM?
На вскидку — p.content = 100 КБ, повторим 1000 раз и на выходе

100 МБ текста, дублирующегося 1000 раз. И это без учета размера int и текста комментариев.

Lazy-load в этом случае будет побыстрее, хотя бомбить базу 1000 и одним запросом — тоже сомнительное развлечение. Можно ли что-то с этим сделать не прибегая вручную к оптимизации запросов?

AsSplitQuery()

Да, и вот каким образом. В EF Core 5.0 появилась новая директива .AsSplitQuery() , которая заставит query provider транслировать загрузку связанной коллекции отдельным запросом.

Транслируется в следующие SQL-запросы:

Что уже приведет к тому, что текст записи не будет читаться 1000 раз, а только 1.

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

На скорую руку я сваял бенчмарк. Пусть есть коллекция записей типа MainEntity , в которой нет ничего, кроме вложенных коллекций. 5 "маленьких" записей и 1 большая.

Записи заполняются при инициализации БД строчками случайной длины по 10 символов для "маленькой" записи и по 1000 для "большой".
В каждую вложенную коллекцию добавляется по ItemsInCollection записей. Тестовый метод извлекает по 2 записи MainEntity , присоединяя к ней от 0 до 6 коллекций (параметр LoadRefs ) в двух режимах — одним запросом и разделёнными запросами (параметр SplitQueries ).

Полный код бенчмарка доступен на гитхабе.

Я запускал бенчмарк на домашней машине, СУБД в дефолтной конфигурации, подключение локальное через localhost.

ItemsInCollection SplitQueries LoadRefs Mean Error StdDev Median
2 False 0 694.6 μs 18.42 μs 52.57 μs 686.9 μs
2 False 1 1,004.3 μs 25.43 μs 69.60 μs 983.4 μs
2 False 2 1,255.3 μs 32.02 μs 89.25 μs 1,237.0 μs
2 False 3 1,578.9 μs 45.46 μs 126.73 μs 1,545.1 μs
2 False 4 2,013.3 μs 56.55 μs 162.26 μs 1,976.8 μs
2 False 5 2,685.2 μs 69.00 μs 196.85 μs 2,651.1 μs
2 False 6 4,646.8 μs 134.52 μs 392.41 μs 4,515.2 μs
2 True 0 726.5 μs 17.60 μs 48.76 μs 725.0 μs
2 True 1 1,403.1 μs 34.46 μs 96.06 μs 1,394.3 μs
2 True 2 1,928.7 μs 57.68 μs 165.51 μs 1,923.3 μs
2 True 3 2,639.6 μs 96.20 μs 277.56 μs 2,584.5 μs
2 True 4 3,128.8 μs 117.46 μs 340.77 μs 3,180.4 μs
2 True 5 3,725.9 μs 121.37 μs 357.87 μs 3,713.8 μs
2 True 6 4,299.9 μs 166.28 μs 485.04 μs 4,233.4 μs
5 False 0 706.6 μs 18.03 μs 50.25 μs 698.9 μs
5 False 1 1,071.6 μs 20.91 μs 51.69 μs 1,068.6 μs
5 False 2 1,512.7 μs 30.13 μs 54.33 μs 1,513.6 μs
5 False 3 2,809.9 μs 148.44 μs 435.35 μs 2,619.9 μs
5 False 4 7,803.3 μs 435.35 μs 1,242.08 μs 7,243.8 μs
5 False 5 37,752.4 μs 439.33 μs 366.86 μs 37,791.4 μs
5 False 6 321,948.5 μs 3,336.86 μs 2,605.20 μs 321,361.0 μs
5 True 0 714.0 μs 12.87 μs 11.41 μs 715.7 μs
5 True 1 1,436.5 μs 33.54 μs 92.37 μs 1,418.8 μs
5 True 2 2,233.7 μs 79.47 μs 230.55 μs 2,232.8 μs
5 True 3 3,056.3 μs 166.89 μs 476.15 μs 3,051.3 μs
5 True 4 3,339.3 μs 105.32 μs 303.88 μs 3,340.5 μs
5 True 5 3,962.7 μs 179.15 μs 508.21 μs 3,862.4 μs
5 True 6 4,496.6 μs 133.87 μs 394.71 μs 4,484.2 μs
10 False 0 747.7 μs 30.51 μs 88.51 μs 719.0 μs
10 False 1 1,211.5 μs 49.81 μs 142.92 μs 1,162.0 μs
10 False 2 2,161.1 μs 88.84 μs 259.14 μs 2,123.4 μs
10 False 3 9,423.3 μs 702.14 μs 2,014.57 μs 9,313.8 μs
10 False 4 90,392.5 μs 821.13 μs 727.91 μs 90,467.2 μs
10 False 5 1,202,652.5 μs 23,336.09 μs 24,969.36 μs 1,205,782.6 μs
10 False 6 34,625,732.4 μs 691,082.68 μs 1,055,356.24 μs 34,718,363.9 μs
10 True 0 747.0 μs 24.88 μs 68.93 μs 738.7 μs
10 True 1 1,712.9 μs 53.74 μs 154.20 μs 1,697.2 μs
10 True 2 2,519.9 μs 107.27 μs 316.28 μs 2,491.5 μs
10 True 3 3,349.0 μs 149.58 μs 436.33 μs 3,295.7 μs
10 True 4 4,268.4 μs 165.83 μs 483.72 μs 4,274.0 μs
10 True 5 4,882.6 μs 188.59 μs 547.13 μs 4,832.2 μs
10 True 6 5,560.8 μs 249.02 μs 726.40 μs 5,478.1 μs

Обратите внимание на выделенные значения. Добавление одной дополнительной коллекции всего с 10 записями (пусть и с относительно большими данными) приводит к деградации в почти 30 раз.

А при использовании разделенных запросов разница уже не так драматична. Да и сами цифры значительно меньше в абсолютных величинах (5,5 мс против 34625 мс).

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

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


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

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