Dbset entity framework что это

Обновлено: 03.07.2024

После удаления Entity Framework Core dbData.Database.SqlQuery<SomeModel> я не могу найти решение для создания необработанного SQL-запроса для моего полнотекстового поискового запроса, который вернет данные таблиц, а также рейтинг.

Единственный метод, который я видел для создания необработанного SQL-запроса в Entity Framework Core, - это dbData.Product.FromSql("SQL SCRIPT"); , который бесполезен, поскольку у меня нет DbSet, который будет отображать ранг, который я возвращаю в запросе.

Есть идеи .

Это зависит от того, используете ли вы EF Core 2.1 или EF Core 3 и более поздние версии .

Если вы используете EF Core 2.1

Если вы используете EF Core 2.1 Release Candidate 1, доступный с 7 мая 2018 г., вы можете воспользоваться преимуществами предлагаемой новой функции - типа запроса.

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

Когда использовать тип запроса?

Служит в качестве возвращаемого типа для специальных запросов FromSql ().

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

Сопоставление с таблицами, для которых не определен первичный ключ.

Сопоставление с запросами, определенными в модели.

Сначала вы определили новое свойство типа DbQuery<T> , где T - это тип класса, который будет нести значения столбцов вашего SQL-запроса. Итак, в вашем DbContext у вас будет следующее:

Во-вторых, используйте метод FromSql , как в случае с DbSet<T> :

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

Если вы используете EF Core 3.0 и более поздние версии

Тип запроса теперь известен как Keyless тип объекта. Как сказано выше, типы запросов были введены в EF Core 2.1. Если вы используете EF Core 3.0 или более позднюю версию, вам следует теперь использовать типы tntity без ключа, поскольку типы запросов теперь помечены как устаревшие.

Эта функция была добавлена в EF Core 2.1 под названием типов запросов. В EF Core 3.0 концепция была переименована в типы сущностей без ключа. Аннотации данных [без ключа] стали доступны в EFCore 5.0.

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

Поэтому, чтобы использовать его, вам нужно сначала пометить свой класс SomeModel аннотацией данных Dbset entity framework что это или путем быстрой настройки с помощью вызова метода .HasNoKey() , как показано ниже:

После этой настройки вы можете использовать один из методов, описанных здесь для выполнения вашего SQL-запроса. Например, вы можете использовать это:

На самом деле вы можете создать общий репозиторий и сделать что-то вроде этого

В моем случае использовалась хранимая процедура вместо необработанного SQL

Добавлено ниже в моем классе DbContext

Чтобы выполнить хранимую процедуру:

В Core 2.1 вы можете сделать что-то вроде этого:

А затем определите процедуру SQL, например:

Таким образом, модель рангов не будет создана в вашей БД.

Теперь в вашем контроллере / действии вы можете вызвать:

Таким образом, вы можете вызывать процедуры Raw SQL.

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

Попробуйте использовать SqlParameter, чтобы избежать внедрения Sql.

FromSql не работает с полным запросом. Пример: если вы хотите включить предложение WHERE, оно будет проигнорировано.

Вот пример того, как его использовать:

В EF Core вы больше не можете выполнять «бесплатный» необработанный sql. Вам необходимо определить класс POCO и DbSet для этого класса. В вашем случае вам нужно будет определить Rank :

Так как он наверняка будет доступен только для чтения, будет полезно включить вызов .AsNoTracking() .

РЕДАКТИРОВАТЬ - Критическое изменение в EF Core 3.0:

DbQuery () теперь является устаревшим, вместо него следует использовать DbSet () (снова). Если у вас есть сущность без ключа, т.е. для нее не требуется первичный ключ, вы можете использовать метод HasNoKey () :

Дополнительную информацию можно найти на здесь

Добавить пакет Nuget - Microsoft.EntityFrameworkCore.Relational

Это вернет номера строк как int

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

Планирую избавиться от него, как только добавят встроенную поддержку. Согласно заявлению Артура Виккерса из команды EF Core, это высокий приоритет для поста 2.0. Проблема отслеживается здесь.

Попробуйте это: (создать метод расширения)

Моя модель: (нет в DbSet ):

Примечание . У этого решения низкая производительность.

Не нацелен непосредственно на сценарий OP, но поскольку я боролся с этим, я бы хотел отказаться от этих ex. методы, которые упрощают выполнение необработанного SQL с помощью DbContext :

Я использовал Dapper, чтобы обойти это ограничение Entity framework Core.

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

Dapper легко освоить. На написание и запуск хранимой процедуры с параметрами ушло 15 минут. В любом случае вы можете использовать как EF, так и Dapper. Ниже приведен пример:

Вы также можете использовать QueryFirst. Как и Dapper, это полностью вне EF. В отличие от Dapper (или EF), вам не нужно поддерживать POCO, вы редактируете свой sql SQL в реальной среде, и он постоянно проверяется на соответствие базе данных. Отказ от ответственности: я являюсь автором QueryFirst.

Я знаю, что это старый вопрос, но, возможно, он поможет кому-то вызывать хранимые процедуры без добавления DTO в качестве DbSets.

С Entity Framework 6 вы можете выполнить что-то вроде ниже

Выполните команду Raw DQL SQl, как показано ниже:

Это решение во многом опирается на решение @pius. Я хотел добавить возможность поддержки параметров запроса, чтобы помочь смягчить SQL-инъекцию, и я также хотел сделать его расширением DbContext DatabaseFacade для Entity Framework Core, чтобы сделать его немного более интегрированным.

Сначала создайте новый класс с расширением:

Обратите внимание, что «T» - это тип возврата, а «P» - это тип параметров вашего запроса, которые будут различаться в зависимости от того, используете ли вы MySql, Sql и т. Д.

Далее мы покажем пример. Я использую возможность MySql EF Core, поэтому мы увидим, как мы можем использовать общее расширение, указанное выше, с этой более конкретной реализацией MySql:

Запрос вернет такие строки, как:
«Форд», «Эксплорер», «Форд Эксплорер»
«Тесла», «Модель X», «Тесла Модель X»

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

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

Чуть больше года при моём участии состоялся следующий "диалог":

Entity Framework:


Классика! Думаю многим знакома эта ситуация: когда очень хочется “красиво” и быстро сделать поиск в базе, используя JOIN локальной коллекции и DbSet. Обычно этот опыт разочаровывает.

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

Введение

Все знают про Entity Framework, многие используют его каждый день, и существует много хороших статей про то, как готовить его правильно (использовать более простые запросы, использовать параметры в Skip и Take, использовать VIEW, запрашивать только нужные поля, следить за кэшированием запросов и прочее), однако тема JOIN локальной коллекции и DbSet до сих пор является "слабым местом".

Задача

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

Этот код не работает в Entity Framework 6 вообще. В Entity Framework Core — работает, но всё будет выполнено на стороне клиента и в случае, когда в базе миллионы записей — это не выход.

Я также снимал некоторые метрики: затраченное время и потребление памяти. Оговорка: если тест выполнялся более 10 минут — я его прерывал (ограничение сверху). Машина для тестов Intel Core i5, 8 GB RAM, SSD.

image

Только 3 таблицы: prices, securities and price sources. Prices — содержит 10 миллионов записей.

Способ 1. Naive

Начнём с простого и будем использовать следующий код:

Идея проста: в цикле читаем записи из базы по одной и добавляем в результирующую коллекцию. У этого кода только одно преимущество — простота. И один недостаток — низкая скорость: даже при условии наличия индекса в базе, большая часть времени займёт коммуникация с сервером БД. Метрики получились такие:

image

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

Способ 2. Naive parallel

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

image

Для маленьких коллекций этот подход работает даже медленнее, чем первый способ. А для самого большого — в 2 раза быстрее. Интересно, что на моей машине было порождено 4 потока, но это не привело к 4х кратному ускорению. Это говорит о том, что накладные расходы в этом способе существенны: как на стороне клиента, так и на стороне сервера. Потребление памяти выросло, но незначительно.

Способ 3. Multiple Contains

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

  1. Подготовить 3 коллекции уникальных значений Ticker, PriceSourceId и Date
  2. Выполнить запрос и использовать 3 Contains
  3. Перепроверить результаты локально

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

Это можно объяснить, используя следующий пример. Предположим есть следующая таблица с данными:

image

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

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

image

Потребление памяти — хуже всех предыдущих способов. Количество прочитанных строк многократно превышает количество запрошенных. Тесты для больших коллекций были прерваны так как выполнялись больше 10 минут. Этот способ не годится.

Способ 4. Predicate builder

Попробуем теперь с другой стороны: старые добрые Expression. Используя их, можно построить 1 большой запрос в следующей форме:

… (.. AND .. AND ..) OR (.. AND .. AND ..) OR (.. AND .. AND ..) …

Это даёт надежду на то, что удастся построить 1 запрос и получить только нужные данные за 1 заход. Код:

Код получился более сложный, чем в предыдущих способах. Строить Expression вручную не самая простая и не самая быстрая операция.

image

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

Способ 5. Shared query data table

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

Теперь, чтобы получить результат нужно:

  1. Начать транзакцию
  2. Загрузить данные запроса в новую таблицу
  3. Выполнить сам запрос (используя новую таблицу)
  4. Откатить транзакцию (чтобы очистить таблицу данных для запросов)

Код выглядит так:

image

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

  • Необходимо создать таблицу для конкретного типа запросов
  • Необходимо использовать транзакции (и тратить ресурсы СУБД на них)
  • Да и сама идея, что нужно что-то ПИСАТЬ, когда нужно ЧИТАТЬ, выглядит странно. А на Read Replica это просто не будет работать.
    А в остальном — решение более или менее рабочее, которое уже можно использовать.

Способ 6. MemoryJoin extension

Теперь можно попробовать улучшить предыдущий подход. Размышления такие:

  • Вместо использования таблицы, которая специфичная для одного типа запроса, можно использовать некий обобщенный вариант. А именно создать таблицу с именем вроде shared_query_data, и добавить в неё по несколько полей Guid, несколько Long, несколько String и т.д. Имена можно взять простые: Guid1, Guid2, String1, Long1, Date2, и т.д. Тогда эту таблицу можно будет использовать для 95% типов запросов. Имена свойств можно будет "скорректировать" позже при помощи проекции Select.
  • Далее нужно добавить DbSet для shared_query_data.
  • А что если вместо записи данных в базу — передавать значения, используя конструкцию VALUES? То есть необходимо, чтобы в итоговом SQL запросе вместо обращения к shared_query_data было обращение к VALUES. Как это сделать?
    • В Entity Framework Core — просто используя FromSql.
    • В Entity Framework 6 — придётся использовать DbInterception — то есть менять сгенерированный SQL, добавляя конструкцию VALUES прямо перед выполнением. Это приведет к ограничению: в одном запросе — не более одной конструкции VALUES. Но работать будет!
    Пример преобразования IEnumerable в IQueryable
    1. На вход поступила коллекция объектов следующего типа:
    2. У нас в распоряжении есть DbSet с полями String1, String2, Date1, Long1, etc
    3. Пусть Ticker будет храниться в String1, TradedOn в Date1, а PriceSourceId в Long1 (int маппится в long, чтобы не делать отдельно поля для int и long)
    4. Тогда FromSql + VALUES будет таким:
    5. Теперь можно сделать проекцию и вернуть удобный IQueryable, использующий тот же тип, который был на входе:

    Мне удалось реализовать этот подход и даже оформить его как NuGet пакет EntityFrameworkCore.MemoryJoin (код тоже доступен). Несмотря на то, что в имени есть слово Core, Entity Framework 6 тоже поддерживается. Я назвал его MemoryJoin, но по факту он отправляет локальные данные на СУБД в конструкции VALUES и вся работа выполняется на нём.

    Код получается следующим:

    image

    Это лучший результат из всех, которые я пробовал. Код получился очень простым и понятным, и в то же время рабочим для Read Replica.

    Пример сгенерированного запроса для получения 3х элементов

    Здесь также видно, как обобщенная модель (с полями String1, Date1, Long1) при помощи Select превращается в ту, которая используется в коде (с полями Ticker, TradedOn, PriceSourceId).

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

    • Необходимо добавить дополнительный DbSet в свой контекст (хотя саму таблицу можно не добавлять)
    • В обобщенной модели, которая используется по умолчанию, объявлены по 3 поля типов Guid, String, Double, Long, Date и т.д. Этого должно хватить на 95% типов запросов. И если передать в FromLocalList коллекцию объектов с 20 полями, то будет выброшен Exception, говорящий, что объект слишком сложный. Это мягкое ограничение и его можно обойти — можно объявить свой тип и внести туда хоть по 100 полей. Однако, больше полей — медленней работа.
    • Больше технических деталей описано в моей статье.

    Заключение

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

    Кто-то скажет, что использование MemoryJoin слишком "заумное" и его надо дорабатывать, а до тех пор использовать его не нужно. Это именно та причина, почему я очень сомневался и почти год не писал эту статью. Я соглашусь, что хотелось бы, чтобы это работало проще (надеюсь однажды так и будет), но также скажу, что оптимизация никогда не была задачей Junior’ов. Оптимизация всегда требует понимания как инструмент работает. И если есть возможность получить ускорение в

    8 раз (Naive Parallel vs MemoryJoin), то я бы осилил 2 пункта и документации.

    И в заключении, диаграммы:

    Затраченное время. Только 4 способа выполнили задачу за время менее 10 минут, а MemoryJoin — единственный способ, который выполнил задачу за время менее 10 секунд.

    image

    Потребление памяти. Все способы продемонстрировали примерно одинаковое потребление памяти, кроме Multiple Contains. Это связано с количеством возвращенных данных.

    В предыдущей статье мы рассмотрели как создаются LINQ-запросы в Entity Framework. В этой и последующих статьях мы рассмотрим операции CRUD (create, read, update, delete) для работы с данными. Для извлечения данных из таблиц в SQL используется инструкция SELECT. Знать детали SQL-запросов вам не нужно, т.к. Entity Framework заботится о преобразовании LINQ-запросов в SQL.

    Для получения всех данных из таблицы вам даже не нужно использовать LINQ-запрос, вы можете просто использовать свойство класса контекста, ссылающееся на класс модели и имеющее тип DbSet<T>. Entity Framework создаст запрос в базу данных для загрузки всех данных из таблицы, связанной с этим классом модели. Давайте добавим метод GetAllCustomers() в наш консольный проект, который мы создали ранее, чтобы извлечь все данные покупателей (т.к. мы еще не рассмотрели вставку данных с помощью Entity Framework, чтобы протестировать примеры выборки данных, вы можете заполнить таблицы Customers и Orders вручную, используя средства Visual Studio или SQL Server Management Studio):

    Запрос context.Customers в этом примере извлечет все данные из таблицы Customers. Для запуска примера, вы можете вызвать метод GetAllCustomers() в методе Main() консольного приложения. Для этого примера Entity Framework сгенерирует следующий SQL-код (здесь я привожу фрагменты кода SQL для тех читателей, которые хорошо знакомы с T-SQL и хотят знать как EF транслирует запросы LINQ в SQL):

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

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

    Загрузка всех данных из таблицы с помощью Entity Framework

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

    Указание столбцов, условий и сортировки в запросе

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

    Приведенный выше код использует LINQ для создания запроса выборки сортированных данных из таблицы, а затем перебирает результаты запроса и отображает имя и фамилию каждого покупателя. После получения коллекции данных через свойство Customers класса контекста, вы можете использовать вспомогательные LINQ-методы OrderBy(), GroupBy() и Join() для сортировки, группировки данных на основе ключа и соединения таблиц. Эти методы будут транслированы на одноименные инструкции языка SQL.

    Другой часто определяемой задачей при выборке данных, является их фильтрация на основе определенного условия. В SQL для этих целей используется оператор WHERE, LINQ содержит одноименный метод Where(), с помощью которого можно добавить дополнительное условие для загрузки данных:

    В этом запросе мы выбираем всех покупателей старше 25 лет.

    До сих пор мы выбирали из базы данных только коллекцию объектов модели, т.е. коллекцию IQueryable<Customer>. Зачастую извлекать все данные не нужно, а нужно извлечь данные только из определенного столбца таблицы, создавая проекцию таблицы. Такой подход позволяет улучшать производительность, когда запрашиваются данные из таблицы, содержащей много столбцов, а в приложении нужно работать только с одним или несколькими столбцами.

    Например, мы могли бы захотеть выбирать только фамилии покупателей из таблицы базы данных и отбрасывать всю другую информацию. Для этих целей в LINQ используется метод Select(), которому передается делегат, в котором выбираются нужные свойства модели. Этот метод возвращает уже не коллекцию объектов Customer, а коллекцию простых типов, например, для фамилий он вернет тип IQueryable<string>. Ниже показан соответствующий пример:

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

    Здесь видно, что из таблицы будут выбираться данные только фамилии покупателей (столбец LastName указывается после инструкции SELECT). На рисунке ниже наглядно показано, какие данные выбирает этот запрос:

    Загрузка данных из одного столбца таблицы с помощью Entity Framework

    Загрузка данных из нескольких столбцов таблицы с помощью Entity Framework

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

    Если вы запустите этот пример, то в приложении возникнет исключение NotSupportedException, в котором говорится, что Entity Framework не может использовать сложный тип Customer с запросом LINQ to Entities. Для этих целей вы должны сначала загрузить коллекцию анонимных объектов, которая инициализируется из базы данных, а затем создать коллекцию объектов Customer, которая инициализируется из коллекции анонимных объектов. Эту задачу можно выполнить в одном запросе, функционально разделив сложный запрос на две части – первая будет использоваться Entity Framework для извлечения данных из базы, вторая будет работать в памяти приложения и инициализировать коллекцию объектов Customer. В качестве разделителя нужно использовать метод AsEnumerable():

    Метод AsEnumerable() в LINQ просто преобразует коллекцию IQueryable к IEnumerable. В простых приложениях, работающих с коллекциями данный метод практически не используется, т.к. в нем нет смысла – интерфейс IQueryable является производным от интерфейса IEnumerable. Но этот метод оказывает существенное влияние при использовании с Entity Framework, указывая, что цепочку методов в запросе до его вызова нужно выполнить, отправив запрос к базе данных, а последующие методы будут оперировать уже на коллекции в памяти приложения. Если вы запустите этот пример, то можете убедиться в его работоспособности.

    Поиск в запросе

    Пока вы видели запросы, которые возвращают коллекцию объектов из базы данных, но иногда нужно чтобы запрос возвращал один объект. Наиболее распространенным сценарием для запросов, возвращающих один объект, является поиск определенного объекта с заданным ключом. DbContext API позволяет делать это очень просто, используя метод Find() класса DbSet. Этот метод принимает значение, которое нужно найти в таблице и возвращает соответствующий объект, если он найден, или null если не найден.

    Поиск в запросе в Entity Framework использует следующую последовательность:

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

    Если к коллекции добавлялись новые объекты, то поиск выполняется и в них.

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

    Чтобы увидеть реализацию поиска, давайте добавим новый метод FindCustomer() в котором мы будем принимать идентификатор покупателя от пользователя, путем ввода его в консоль и затем будем искать покупателя с соответствующим идентификатором в базе данных:

    Метод Find() осуществляет поиск по первичному ключу таблицы. Как вы знаете, первичные ключи бывают составными, поэтому методу Find() можно передать несколько значений, для поиска в ключах, при этом эти значения должны следовать в том же порядке, в котором свойства первичных ключей определены в классе модели. Напомню, что в Code-First для этого используется атрибут аннотаций данных Column, с переданным ему параметром Order, либо метод HasColumnOrder(), если вы используете для настроек Fluent API.

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

    Локальные данные

    Во всех предыдущих примерах мы использовали запрос к свойству класса контекста типа DbSet (context.Customers). Как говорилось ранее, использование этого свойства приводит к созданию запроса к базе для выборки всех данных из привязанной таблицы. Мы также использовали метод Find(), который ищет в памяти приложения данные до создания запроса к этой базе данных, в уже загруженных ранее данных.

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

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

    Здесь, при перечислении данных покупателей, новый добавленный покупатель не будет отображаться в списке.

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

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

    Этот метод позволяет проверить, сколько объектов Customer на данный момент находятся в памяти приложения. Если вы запустите приложение с вызовом этого метода, вы увидите, что количество объектов равно нулю. Мы получаем нулевой результат, потому что мы не выполняем никаких запросов, чтобы загрузить покупателей из базы данных, и мы не добавляли новые объекты Customer в коде. Давайте немного изменим этот метод, и запросим некоторые данные из базы:

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

    Загрузка объектов из памяти приложения Entity Framework

    Перебор содержимого DbSet в цикле по каждому элементу является одним из способов, чтобы получить все данные из памяти приложения, но это немного неэффективно, чтобы каждый раз выполнять цикл просто ради загрузки данных. К счастью, DbContext API включает специальный метод Load() в классе DbSet, который можно вызвать для ручного запуска процесса загрузки данных из базы. Ниже показано использование этого метода (не забудьте указать пространство имен System.Data.Entity):

    Этот код гораздо лаконичнее, чем тот, что мы использовали ранее. В сборке System.Data.Entity определен также расширяющий IQueryable<T> метод Load(), поэтому мы можем использовать его не только для загрузки всех данных из таблицы, но также использовать LINQ-методы для сужения выборки. Например, следующий запрос посчитает, сколько находится в памяти объектов Customer, чей возраст превышает 25 лет:

    Использование особенностей коллекции ObservableCollection

    Если вы смотрели примеры использования локальных данных довольно внимательно, то должны были увидеть, что свойство Local возвращает специальный тип обобщенной коллекции ObservableCollection. Этот тип коллекции позволяет получать уведомления, когда добавляются или удаляются элементы из коллекции. Коллекция ObservableCollection полезна в ряде сценариев, работающих с привязкой данных. Например, мы ее использовали при рассмотрении WPF в статье “Привязка к коллекции объектов”, чтобы изменять графический интерфейс приложения, когда добавлялись данные.

    Эта коллекция имеет событие CollectionChanged, в обработчике которого мы можем вносить полезные действия. В контексте локальных данных Entity Framework это событие возникает когда мы загружаем или удаляем данные из базы, работаем с данными в памяти приложения или добавляем новые объекты в DbContext. Ниже показан пример:

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

    Обработка событий изменения локальных данных в Entity Framework

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


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

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