Entity framework временные таблицы
I'm a huge fan of entity based and result set based relational mapping (classic ORMs). I'm also huge fan of DML based relational mapping (micro ORMs). In general I'm a huge fan of every technology that allows me to get the job done in the best possible way. I believe that one should never limit himself to single approach within a project - it should always be about choosing the best tool for the job. But sometimes there are real life constraints (licensing, business, politics etc.) which are limiting the choices. This was one of those situations.
Entity Framework is not the perfect tool for complex reporting but still can be used to do it. Recently I was tasked with optimizing such a scenario. The code was building a quite complex report across several tables all filtered by the same sub query. In general it looked similar to the snippet below.
The top level method generates union from number of independent item queries. Each item query receives identifiers of students for whom the report is being generated.
In the end Entity Framework was generating SQL query with pattern visible below.
The redundancy in the query is immediately visible. Both the students and items queries were not a trivial ones, which resulted in pressure on the database. Quick look at SQL Server profiler provided CPU time ~750ms and overall duration ~1700ms. Taking into consideration that those data had to be rendered and returned to user this is "bad experience" territory. How to optimize this? All the sub queries by itself looked quite good (despite not being trivial) so the initial idea became removing the redundancy.
Adjusting POC to sp_executesql usage
I've read all I could find regarding temporary tables but I wasn't able to find a way to create one ahead without specifying full definition, so the POC had to include it.
This worked and was even faster than previous one (probably because SQL Server didn't had to invest processing into figuring out the definition) - CPU time ~420ms, overall duration ~520ms. The challenge was how to do that from code in as generic as possible way.
Методы, реализуемые в репозитории (обновления или транзакции по отношению к запросам)
В каждом классе репозитория следует поместить методы сохраняемости, которые обновляют состояние сущностей, содержащихся в связанном с ним агрегате. Помните, что имеется однозначное соответствие между агрегатом и связанным с ним репозиторием. Также учитывайте, что корневой объект сущности агрегата может иметь внедренные дочерние сущности в своем графе EF. Например, покупатель может иметь несколько способов оплаты в виде связанных дочерних сущностей.
Поскольку этот подход для микрослужбы заказов в eShopOnContainers также основан на CQS/CQRS, большинство запросов не реализуется в пользовательских репозиториях. Разработчики имеют право создавать нужные им запросы и соединения для уровня представления данных без ограничений, установленных агрегатами, пользовательскими репозиториями в агрегатах и DDD в целом. Большинство пользовательских репозиториев, предлагаемых в данном руководстве, имеют несколько методов обновления или транзакций, но только те методы запросов, которые необходимы для получения обновляемых данных. Например, репозиторий BuyerRepository реализует метод FindAsync, так как приложению нужно узнать, существует ли конкретный покупатель, прежде чем создавать нового покупателя, связанного с заказом.
Однако реальные методы запросов для получения данных для отправки на уровень представления данных или в клиентские приложения реализуются, как было указано, в запросах CQRS на основе гибких запросов с помощью Dapper.
Adjusting the implementation
Putting this in simple words all I had to do was generating a "create table" script based on entity. This should be doable, Entity Framework should be storing the information I needed somewhere. After some research I've found what I was looking for. I've adopted the information provided into a from of extension method.
Armed with this extension method I've started refactoring my temporary table generation code.
I've added all the HasColumnType, HasMaxLength etc. to the configuration class and fired away. It worked.
Ссылочные типы, допускающие значение null
Отслеживается по проблеме № 14150
Размер футболки: большой
Мы добавим в код EF Core заметки по использованию ссылочных типов, допускающих значения NULL.
Инфраструктура производительности и новые тесты
Состояние: ограничено или выполнено
Размер футболки: Средний
Обновление. Мы улучшили инфраструктуру тестирования и добавили новые тесты для поддержки работы, выполненной для EF Core 6. Дополнительные улучшения в этой области были исключены из выпуска EF Core 6.0.
Предложения
Нам важно ваше мнение о планировании. Лучший способ указать важность проблемы — проголосовать за нее на GitHub (👍). Эти данные будут учитываться в процессе планирования для следующего выпуска.
При использовании реляционных баз данных, таких как SQL Server, Oracle или PostgreSQL, рекомендуемый подход заключается в реализации уровня сохраняемости на основе Entity Framework (EF). EF поддерживает LINQ и предоставляет строго типизированные объекты для вашей модели, а также упрощенную сохраняемость в базу данных.
Соответствие запросов в EF6
Состояние: ограничено или выполнено
Размер футболки: большой
EF Core 5.0 поддерживает большинство шаблонов запросов, поддерживаемых в EF6, а также шаблоны, которые не поддерживаются в EF6. В выпуске EF Core 6.0 мы планируем устранить это несоответствие и включить в число поддерживаемых EF Core запросов все запросы EF6. Для этого будет проведен анализ несоответствий. Тем не менее уже сейчас определены проблемы с запросом GroupBy, например с преобразованием запроса GroupBy с последующим запросом FirstOrDefault, и необработанными запросами SQL для примитивных и несопоставленных типов.
Обновление. Необработанные SQL-запросы для примитивных и несопоставленных типов были удалены из версии 6.0 из-за ограничений ресурсов и корректировки приоритетов.
Реализация пользовательских репозиториев с помощью Entity Framework Core
На уровне реализации репозиторий является просто классом с кодом сохраняемости данных, координируемым единицей работы (DBContext в EF Core) при выполнении обновлений, как показано в следующем классе:
Интерфейс IBuyerRepository поступает из уровня модели предметной области как контракт. Однако реализация репозитория выполняется на уровне сохраняемости и инфраструктуры.
ColumnAttribute.Order
Отслеживается по проблеме № 10059
Состояние: ведутся работы
Размер футболки: маленький
Эта возможность позволяет использовать произвольный порядок столбцов при создании таблицы с помощью миграций или EnsureCreated . Обратите внимание, что для изменения порядка столбцов в существующих таблицах требуется перестроение таблицы. Поддержка такой возможности в каких-либо выпусках EF Core не планируется.
Улучшение существующих возможностей и исправление ошибок
На данный момент в этом выпуске планируется решение всех проблем или ошибок, назначенных для вехи 6.0.0. К ним относится множество небольших улучшений и исправлений ошибок.
Сопоставление полей вместо свойств
Эта функция, появившаяся в EF Core 1.1, позволяет сопоставлять столбцы с полями напрямую. Теперь существует возможность не использовать свойства в классе сущностей, а просто сопоставлять столбцы таблицы с полями. Эта возможность часто используется в закрытых полях для хранения внутреннего состояния, доступ к которому не должен осуществляться извне сущности.
Это можно делать как с отдельными полями, так и с коллекциями, например с List<> . Этот момент упоминался ранее, когда мы обсуждали моделирование классов модели предметной области, но здесь можно видеть, как это сопоставление выполняется для конфигурации PropertyAccessMode.Field , выделенной в предыдущем коде.
Дополнительные ресурсы
Время существования экземпляра репозитория в контейнере IoC
Аналогичным образом время существования репозитория обычно должно быть задано как scoped — ограниченное областью (InstancePerLifetimeScope в Autofac). Оно также может быть задано как transient — временное (InstancePerDependency в Autofac), но служба будет более эффективной в отношении памяти при использовании времени существования, ограниченного областью.
Использование для репозитория времени существования singleton может вызвать серьезные проблемы с параллелизмом, если для DbContext установлено время существования scoped (InstancePerLifetimeScope) (время существования по умолчанию для DBContext).
Использование теневых свойств в EF Core, скрытых на уровне инфраструктуры
Теневые свойства в EF Core — это свойства, которые не существуют в вашей модели классов сущностей. Значения и состояния этих свойств сохраняются только в классе ChangeTracker на уровне инфраструктуры.
Инфраструктура в Entity Framework Core с точки зрения DDD
С точки зрения DDD важной особенностью EF является возможность использования сущностей предметной области POCO, которые в терминологии EF также называются сущностями code-first. При использовании сущностей предметной области POCO ваши классы модели предметной области являются неустойчивыми, следуя принципам Persistence Ignorance (независимости сохраняемости) и Infrastructure Ignorance (независимости инфраструктуры).
В шаблонах DDD вы должны инкапсулировать правила и поведение домена в самом классе сущностей, чтобы он мог управлять инвариантами, проверками и правилами при доступе к любой коллекции. Таким образом, в DDD не рекомендуется разрешать общий доступ к коллекциям дочерних сущностей или объектов значений. Вместо этого следует предоставить методы, определяющие, как и когда могут обновляться поля и коллекции свойств, и какое поведение и действия должны осуществляться, когда это происходит.
Начиная с EF Core 1.1, для удовлетворения этих требований DDD можно создавать в своих сущностях простые поля вместо общедоступных свойств. Если вы не хотите, чтобы поле сущности было доступно извне, можно просто создать атрибут или поле вместо свойства. Можно также использовать методы задания закрытых свойств.
Таким же образом теперь можно иметь доступ только для чтения к коллекциям с помощью открытого свойства, типизированного как IReadOnlyCollection , который поддерживается членом частного поля для коллекции (например, a List ) в сущности, которая использует EF для сохраняемости. В предыдущих версиях Entity Framework требовалось наличие свойств коллекции для поддержки ICollection , что означало, что любой разработчик, использующий родительский класс сущностей, может добавлять или удалять элементы с помощью его коллекций свойств. Эта возможность противоречила бы рекомендуемым шаблонам в DDD.
Вы можете использовать закрытую коллекцию при предоставлении объекта IReadOnlyCollection только для чтения, как показано в следующем примере кода.
Свойство OrderItems может быть доступно только для чтения с помощью IReadOnlyCollection . Этот тип доступен только для чтения, поэтому он защищен от обычных внешних обновлений.
При использовании полей вместо свойств сущность OrderItem сохраняется так же, как если бы она имела свойство List . Однако она предоставляет единственный метод доступа AddOrderItem для добавления в заказ новых элементов. В результате поведение и данные оказываются связаны друг с другом и будут согласованными во всем коде приложения, использующем модель предметной области.
Временные таблицы SQL Server
Отслеживается по проблеме № 4693
Размер футболки: большой
Временные таблицы поддерживают запросы к данным, которые хранились в них любой момент времени, а не только к самым последним сохраненным данным, как в случае с обычными таблицами. EF Core 6.0 поддерживает создание временных таблиц с помощью миграций, а также доступ к данным посредством запросов LINQ.
Начальный объем работы в этом направлении приводится в описании этой проблемы. Возможно, с учетом полученных в процессе работы над выпуском отзывов будет реализована дополнительная поддержка.
4. Продвинутая вставка с использованием MERGE
SqlBulkCopy умеет только добавлять записи в таблицу, и никакого функционала для изменения уже существующих записей не предоставляет. И тем не менее, мы можем ускорить выполнение Update операций! Как? Вставляем данные во временную пустую таблицу, а затем синхронизируем таблицы с помощью инструкции MERGE, дебютировавшей в SQL Server 2008:
MERGE (Transact-SQL)
Выполняет операции вставки, обновления или удаления для целевой таблицы на основе результатов соединения с исходной таблицей. Например, можно синхронизировать две таблицы путем вставки, обновления или удаления строк в одной таблице на основании отличий, найденных в другой таблице.
- cоздать/очистить временную таблицу, полностью идентичную целевой таблице;
- вставить данные с помощью SqlBulkCopy во временную таблицу;
- используя MERGE, добавить записи из временной таблицы в целевую.
Временная таблица
Необходимо создать таблицу в БД, полностью повторяющую схему таблицы для вставки данных. Создавать копии вручную — худший вариант из возможных, так как вся дальнейшая работа по сравнению и синхронизации схем таблиц также ляжет на ваши плечи. Надежнее копировать схему программно и непосредственно перед вставкой. Например, с использованием SQL Server Management Objects (SMO):
Стоит обратить внимание на класс ScriptingOptions, содержащий несколько десятков параметров для тонкой настройки генерируемого SQL. Полученный StringCollection развернем в String. К сожалению, лучшего решения, чем заменить в скрипте имя исходной таблицы на имя временной а-ля String.Replace(«Order», «Order_TEMP»), я не нашел. Буду благодарен за подсказку красивого решения по созданию копии таблицы в пределах одной БД. Выполним готовый скрипт любым удобным способом. Копия таблицы создана!
Mixed mode assembly is built against version 'v2.0.50727' of the runtime and cannot be loaded in the 4.0 runtime without additional configuration information.
Другой вариант — использовать Database.ExecuteWithResults.
Копирование данных из временной таблицы в целевую
Осталось выполнить на стороне SQL Server инструкцию MERGE, сравнивающую содержимое временной и целевой таблиц и выполняющую апдейт или вставку (если необходимо). К примеру, для таблицы [Order] код может выглядеть следующим образом:
Данный SQL-запрос сравнивает записи из временной таблицы [Order_TEMP] с записями из целевой таблицы [Order], и выполняет Update, если найдена запись с аналогичным значением в поле Id, либо Insert, если такой записи не найдено. Выполним код любым удобным способом, и готово! Не забываем очистить/удалить временную таблицу по вкусу.
Применение пользовательского репозитория по сравнению с непосредственным применением DbContext EF
Однако реализация пользовательских репозиториев обеспечивает определенные преимущества при реализации более сложных микрослужб или приложений. Шаблоны Unit of Work и Repository предназначены для инкапсуляции уровня сохраняемости инфраструктуры, поэтому он отделен от уровней приложения и модели предметной области. Реализация этих шаблонов позволяет упростить использование макетов репозиториев, моделирующих доступ к базе данных.
На рис. 7-18 можно увидеть различия между отсутствием использования репозиториев (с применением класса DbContext EF напрямую) и использованием репозиториев, что упрощает макетирование таких репозиториев.
Рис. 7-18. Применение пользовательских репозиториев по сравнению с простым DbContext
На рис. 7-18 показано, что применение пользовательского репозитория добавляет уровень абстракции, который может использоваться для упрощения тестирования путем макетирования репозитория. Существует несколько вариантов макетирования. Можно макетировать только репозитории или макетировать всю единицу работы. Как правило, достаточно макетирования только репозиториев, и сложность абстрагирования и макетирования всей единицы работы обычно не требуется.
Короче говоря, пользовательские репозитории позволяют упростить тестирование кода с помощью модульных тестов, на которые не влияет состояние уровня данных. Если выполняются тесты, которые также обращаются к реальной базе данных через Entity Framework, то это не модульные тесты, а интеграционные тесты, которые выполняются гораздо медленнее.
Если вы использовали DbContext напрямую, вам потребовалось бы выполнить его макетирование или запуск модульных тестов с помощью SQL Server в памяти с прогнозируемыми данными для модульных тестов. Однако макетирование DbContext или управление фиктивными данными требует больше усилий, чем макетирование на уровне репозитория. Конечно, вы всегда можете тестировать контроллеры MVC.
Пакеты миграций
Отслеживается по проблеме № 19693
Размер футболки: Средний
Пакет миграций — это автономный исполняемый файл, который применяет миграции к рабочей базе данных. Он действует аналогично команде dotnet ef database update , но при этом должен значительно упростить развертывание SSH, DOCKER или PowerShell за счет включения всех необходимых компонентов в один исполняемый файл.
Общие сведения
Тесты TechEmpower Fortunes
Отслеживается по проблеме № 23611
Размер футболки: очень большой
В выпуске EF Core 6.0 мы постараемся добиться для EF Core показателей производительности, соответствующих реализации с Dapper, на основе результатов теста TechEmpower Fortunes. (Это сложная задача, но мы приложим все силы, чтобы достичь лучшего возможного результата.)
Улучшения в Microsoft.Data.Sqlite
Состояние: ограничено или выполнено
Размер футболки: Средний
Мы планируем внести ряд небольших улучшений для Microsoft.Data.Sqlite, включая организацию пула соединений и использование подготовленных инструкций для повышения производительности.
Обновление. Из версии 6.0 исключены подготовленные операторы.
Поставщик базы данных Cosmos
Состояние: "Развернуто" или "Завершено"
Размер футболки: большой
Мы активно собираем отзывы о том, какие улучшения необходимо внести в поставщик Cosmos в EF Core 6.0. По мере получения новой информации мы будем обновлять этот документ. На данный момент мы ждем ваших голосов (👍) за необходимые функции Cosmos.
Обновление. Мы осуществляем обширную разработку для клиентов в отношении поставщика Cosmos. В результате в EF Core 6.0 были включены приведенные ниже улучшения.
Обновление. В выпуске 6.0 были устранены приведенные ниже проблемы.
Прочие функции
Размер футболки: большой
В EF 6.0 планируется включить ряд прочих функций, в том числе следующие.
Обновление. В выпуске 6.0 были устранены приведенные ниже проблемы.
Команда разработчиков EF Core также работает с несколькими сопутствующими независимыми технологиями. В частности, планируется работа в следующих направлениях.
DataVerse (ранее — Common Data Services)
Состояние: ведутся работы
Размер футболки: продолжается
Реализация шаблона спецификации запроса
Как говорилось ранее в разделе, посвященном проектированию, шаблон спецификации запроса — это шаблон предметно-ориентированного проектирования, в котором можно поместить определение запроса с дополнительной логикой сортировки и разбиения на страницы.
Шаблон спецификации запроса определяет запрос в объекте. Например, чтобы инкапсулировать постраничный запрос, который выполняет поиск некоторых продуктов, вы можете создать спецификацию PagedProduct, которая принимает необходимые входные параметры (номер страницы, размер страницы, фильтр и т. д.). Затем в любом методе репозитория (обычно в перегрузке List()) вы можете принять IQuerySpecification и запустить ожидаемый запрос на основе этой спецификации.
Примером универсального интерфейса спецификации является следующий код, который похож на код, используемый в эталонном приложении eShopOnWeb.
Реализация базового класса универсальной спецификации выглядит следующим образом.
Следующая спецификация загружает одну сущность корзины, заданную идентификатором корзины или идентификатором покупателя, которому принадлежит корзина. Она будет безотлагательно загружать коллекцию Items корзины.
И, наконец, ниже вы видите, как универсальный репозиторий EF может использовать такую спецификацию для фильтрации и безотложной загрузки данных, связанных с типом T заданной сущности.
Помимо инкапсуляции логики фильтрации, эта спецификация может указывать форму возвращаемых данных, включая свойства, которые следует заполнить.
Хотя не рекомендуется возвращать IQueryable из репозитория, совершенно нормально использовать их в репозитории для создания набора результатов. Вы видели применение этого подхода в методе List выше, где промежуточные выражения IQueryable использовались для построения списка Includes запроса перед выполнением запроса с условиями спецификации в последней строке.
При добавлении/изменении большого количества записей (10³ и выше), производительность Entity Framework оставляет желать лучшего. Причиной этому являются как архитектурные особенности самого фреймворка, так и неоптимальный генерируемый SQL. Забегая вперед — сохранение данных в обход контекста сокращает время выполнения на порядки.
Содержание статьи:
1. Insert/Update стандартными средствами Entity Framework
2. Поиск решения проблемы
3. Интеграция Entity Framework и SqlBulkCopy
4. Продвинутая вставка с использованием MERGE
5. Сравнение производительности
6. Выводы
SqlServer.Core
Состояние: ведутся работы
Размер футболки: продолжается
Работа над Microsoft.Data.SqlClient при этом продолжится в прежнем объеме. Это по-прежнему будет рекомендованный способ подключения к SQL Server и SQL Azure как при использовании EF Core, так и в других случаях. Также этот поставщик будет поддерживать появляющиеся новые возможности SQL Server.
Миграция и развертывание
По результатам исследований, проведенных при работе над выпуском EF Core 5.0, мы планируем реализовать улучшенную поддержку управления миграцией и развертыванием баз данных. Предполагаются улучшения по двум основным направлениям.
Заметки к данным и текучий API
Существует множество дополнительных соглашений EF Core, и большинство из них можно изменить с помощью заметок к данным или текучего API, реализованного в методе OnModelCreating.
Заметки к данным необходимо использовать в самих классах модели сущностей, что является более навязчивым способом с точки зрения DDD. Это связано с тем, что вы засоряете модель заметками к данным, относящимися к базе данных инфраструктуры. С другой стороны, текучий API представляет удобный способ изменения большинства соглашений и сопоставлений на уровне инфраструктуры сохраняемости данных; таким образом, модель сущностей будет чистой и отделенной от инфраструктуры сохраняемости.
Выводы
В случае работы с контекстом, содержащим большое количество объектов (10³ и выше), отказ от инфраструктуры Entity Framework (добавление в контекст + сохранение контекста) и переход на SqlBulkCopy для записи в БД может обеспечить прирост производительности в десятки, а то и сотни раз. Однако, по моему мнению, использовать связку EF+SqlBulkCopy повсеместно — явный признак того, что с архитектурой вашего приложения что-то не так. Рассмотренный в статье подход следует рассматривать как простое средство для ускорения производительности в узких местах уже написанной системы, если менять архитектуру/технологию по каким-либо причинам затруднительно. Любой разработчик, использующий Entity Framework, должен знать сильные и слабые стороны этого инструмента. Успехов!
В первой своей попытке закрыть дыру в производительности Entity Framework'а я рассматривал только материализацию. Но дальше в процессе работы, как того и следовало ожидать, я наткнулся и на другое, более весомое ограничение. Операции вставки, модификации и удаления записей происходят тоже медленно. На 100 вставок EF посылает в базу 100 запросов на вставку, никак не пытаясь их сгруппировать.
Кроме этого, в одном из проектов была обнаружена одна неприятная ошибка: EF версии 5.0.0, при работе с Oracle, в Clob/Xml поля не позволяет вставлять строки более 2000 символов.
Для решения был создан компонент, который я назвал Context Items, со следующими возможностями:
1) Bulk Insert (MS Sql): в таблицы, не имеющие Identity в качестве первичного ключа возможно осуществить вставку методом Bulk Insert, который поддерживается базой данных Ms Sql Server. В случае с Identity нет способа надежно получить назад ключи, сгенерированные базой при вставке с помощью Bulk Insert, поэтому для таблиц имеющих такие ключи используется группировка нескольких обычных Insert-запросов в один запрос. Это работает существенно медленнее, чем Bulk Insert, но все же быстрее, чем через EF.
2) Sequenced Bulk Insert (MS Sql): альтернативой Identity обычно служит Guid, это решает проблему вставки, но создает другую проблему – в силу большей длины ключа операции Join начинают работать медленнее, кроме этого Guid непоследователен, и поэтому Clustered индексы не приносят своих преимуществ. Как решение данной проблемы начиная с MS Sql Server 2012 есть возможность использовать Sequence для создания первичных ключей. Это позволяет использовать целочисленные последовательные ключи, что позволяет использовать Clustered индексы, аналогично Identity, и одновременно позволяет использовать Bulk Insert для вставки. Компонент поддерживает только ацикличные Sequence с инкрементом 1.
3) Bulk Update (MS Sql): самой по себе в базе данных такой операции не существует, компонент воплощает ее последовательно выполняя следующие 4 операции:
По причине того, что операция не атомарная, ее желательно исполнять в транзакции.
4) Bulk Delete (MS Sql): также как и Bulk Update, эта операция происходит в 4 шага:
5) Материализация: Функция перекочевала из предыдущего варианта, дополнительно я добавил в репозиторий тестовый проект, включающий в себя сравнение производительности материализации с micro-ORM Dapper. Context Items выполняет эту операцию примерно на 3-5% быстрее, чем Dapper, и на 40% быстрее, чем EF, при использовании AsNoTracking, без этого EF работает еще в несколько раз медленнее.
7) Equality members: Для всех сущностей генерируются методы GetHashCode и Equals, работающие с первичным ключом.
Компонент Context Items поддерживает EF версии 5.0.0 – 6.1.3, а также базы данных Oracle и MS Sql Server.
Поддерживается только database-first подход, имеется ограничение — имена сущностей должны совпадать с именами таблиц. Руки не дошли, чтобы это ограничение починить.
Limitations
This solution has one serious limitation - the names of columns in temporary table must match the names of corresponding columns in query generated by Entity Framework. This won't always be true. If the query contains only columns coming directly from tables this should be safe assumption, but when calculated columns appears they will have names chosen by Entity Framework. In above approach this would require nasty hard coding like HasColumnName("C2") which should rather be avoided.
There is a potential way of working around this limitation - I will explore it in part two.
Copyright © 2009 - 2021 Tomasz Pęczek | Blog content licensed under the Creative Commons CC BY 4.0 | Code samples and snippets licensed under the MIT license
Версия EF Core 6.0 уже выпущена. На этой странице будет храниться архивная версия плана.
Как было описано в процессе планирования, мы объединили полученные от заинтересованных лиц сведения в план для выпуска Entity Framework Core (EF Core) 6.0. Этот план периодически обновляется, чтобы отобразить корректировки графика и области.
В отличие от предыдущих выпусков в этом плане мы не будем пытаться охватить весь объем работы, намеченный для выпуска 6.0. В нем определяются направления и способы улучшения, предполагаемые в этом выпуске, а также предусмотрены возможности для внесения корректировок и добавления новых задач по мере получения отзывов и новых знаний в процессе работы над выпуском.
Этот план не устанавливает никаких обязательств. Он является отправной точкой и будет расширяться по мере получения новых знаний. Могут быть включены некоторые вещи, пока не запланированные для версии 6.0, и исключены некоторые вещи, уже запланированные для этой версии.
Changing the shared sub query to in-memory collection
First thing which came into mind was grabbing the students identifiers once and passing them as in-memory collection to all the sub queries. The code change was in fact easy and cheap.
Generated SQL query also looked promising.
Verification with SQL Server profiler showed that times have been nicely reduced (CPU time ~345ms and overall duration ~565ms) so the problem should be solved. Unfortunately the end-to-end stress performance test results didn't improve. The overall latency remained at similar level and application CPU usage went up. Why?
The only change that has been done was introduction of IN clause representing Enumerable.Contains. This scenario was indeed problematic with Entity Framework 4 as usage of Enumerable.Contains was translated into a tree of OR expressions, but Entity Framework 6 has introduced native support for DbInExpression so it should be ok. Still the Visual Studio profiler was pointing at query generation as the reason. Quick look at Entity Framework performance considerations regarding Autocompiled Queries brought the answer - the IN clause prevents query caching. Every time the code was being executed Entity Framework had to regenerate all the items queries (and the top query as well) which resulted in high CPU usage. This meant that different solution was needed.
Поддерживаемые платформы
Время существования экземпляра IUnitOfWork и DbContext EF в контейнере IoC
Режим создания экземпляра DbContext не должен быть настроен как ServiceLifetime.Transient или ServiceLifetime.Singleton.
Улучшения в System.Data
Состояние: ограничено или выполнено
Размер футболки: большой
В этом направлении будут вестись следующие работы.
Обновление. В выпуске 6.0 были устранены приведенные ниже проблемы.
Компоновщик или компиляция AOT
Отслеживается по проблеме № 10963
Состояние: ограничено или выполнено
Размер футболки: Средний
EF Core создает большой объем кода во время выполнения. Это может стать проблемой для моделей приложений, которые зависят от встряхивания дерева компоновщика, таких как Xamarin и Blazor, а также для платформ, не допускающих динамическую компиляцию, таких как iOS. В процессе работы над выпуском EF Core 6.0 мы продолжим исследования в этом направлении и будем по мере возможности вносить точечные улучшения. Тем не менее мы не рассчитываем полностью решить эту проблему в рамках выпуска 6.0.
Производительность
В целом EF Core работает быстрее, чем EF6, но при этом все же остаются направления, в которых возможны значительные улучшения производительности. В выпуске EF Core 6.0 мы планируем решить ряд соответствующих задач, а также добиться улучшения нашей инфраструктуры и производительности и тестов.
Изучение этой темы требует большого объема циклических исследований, по результатам которых мы будем определять направления для концентрации основных усилий. Мы планируем начать с решения следующих задач.
Номер версии и дата выпуска
Алгоритм Hi/Lo в EF Core
Интересным аспектом кода в предыдущем примере является применение алгоритма Hi/Lo в качестве стратегии создания ключей.
Алгоритм Hi/Lo удобно использовать в тех случаях, когда требуются уникальные ключи перед фиксацией изменений. Вкратце, алгоритм Hi-Lo присваивает строкам таблицы уникальные идентификаторы, которые не зависят от немедленного сохранения строк в базе данных. Это позволяет начинать использовать идентификаторы сразу, как это происходит с обычными последовательными идентификаторами базы данных.
Алгоритм Hi/Lo описывает механизм для получения пакета уникальных идентификаторов из последовательности связанной базы данных. Эти идентификаторы являются безопасными для использования, так как база данных гарантирует уникальность, исключая конфликты между пользователями. Этот алгоритм представляет интерес по следующим причинам.
Он не нарушает шаблон единицы работы.
Он получает идентификаторы последовательности в пакетах, чтобы свести к минимуму количество обращений к базе данных.
Он создает удобный для восприятия идентификатор, в отличие от методов, в которых используются идентификаторы GUID.
EF Core поддерживает алгоритм HiLo с помощью метода UseHiLo , как показано в предыдущем примере.
3. Интеграция Entity Framework и SqlBulkCopy
Попробуем сделать всё сами. В простейшем случае, вставка данных из коллекции объектов с помощью SqlBulkCopy выглядит следующим образом:
The given value of type String from the data source cannot be converted to type nvarchar of the specified target column.
К сожалению, SqlBulkCopy зачастую не предоставляет информацию, позволяющую однозначно определить строку/сущность, вызвавшие ошибку. Еще одна неприятная особенность — при попытке вставить дубликат записи по первичному ключу, SqlBulkCopy выбросит исключение и завершит работу, не предоставляя возможности обработать ситуацию и продолжить выполнение.
Маппинг
В случае корректно сгенерированных сущностей и БД становятся неактуальными проверки на соответствие типов, или длину поля в таблице, как тут. Полезней разобраться с маппингом колонок, выполняемым через свойство SqlBulkCopy.ColumnMappings:
Если источник данных и таблица назначения имеют одинаковое количество столбцов и исходная позиция каждого исходного столбца в источнике данных соответствует исходной позиции соответствующего столбца назначения, коллекция ColumnMappings не требуется. Однако если количество столбцов или их порядок различны, необходимо использовать ColumnMappings для обеспечения правильного копирования данных между столбцами.
Для EF В 99% случаев потребуется задать ColumnMappings явно (из-за navigation properties и любых дополнительных свойств). Navigation properties можно отсеять при помощи Reflection:
Такой код сгодится для POCO класса без дополнительных свойств, в противном случае придется переходить на «ручное управление». Получить схему таблицы тоже достаточно просто:
Что дает возможность вручную провести маппинг между классом-источником и целевой таблицей.
Использование свойства SqlBulkCopy.BatchSize и класса SqlBulkCopyOptions
SqlBulkCopy.BatchSize:
BatchSize | Количество строк в каждом пакете. В конце каждого пакета серверу отправляется количество содержащихся в нем строк. |
SqlBulkCopyOptions — перечисление:
Имя члена | Описание |
---|---|
CheckConstraints | Проверять ограничения при вставке данных. По умолчанию ограничения не проверяются. |
Default | Использовать значения по умолчанию для всех параметров. |
FireTriggers | Когда задана эта установка, сервер вызывает триггеры вставки для строк, вставляемых в базу данных. |
KeepIdentity | Сохранять идентификационные значения источника. Когда эта установка не задана, идентификационные значения присваиваются таблицей назначения. |
KeepNulls | Сохранять значения NULL в таблице назначения независимо от параметров значений по умолчанию. Когда эта установка не задана, значения null, где возможно, заменяются значениями по умолчанию. |
TableLock | Получать блокировку массового обновления на все время выполнения операции массового копирования данных. Когда эта установка не задана, используется блокировка строк. |
UseInternalTransaction | Когда эта установка задана, каждая операция массового копирования данных выполняется в транзакции. Если задать эту установку и предоставить конструктору объект SqlTransaction, будет выброшено исключение ArgumentException. |
Мы можем опционально включить проверку триггеров и ограничений на стороне БД (по умолчанию выключена). При указании BatchSize и UseInternalTransaction, данные будут отправляться на сервер блоками в отдельных транзакциях. Таким образом, все успешные блоки до первого ошибочного, будут сохранены в БД.
Самые востребованные возможности
Традиционно основным источником данных для процесса планирования являются ваши голоса (👍), отдаваемые за новые возможности на сайте GitHub. В рамках выпуска EF Core 6.0 планируется работа над следующими самыми востребованными возможностями.
Текучий API и метод OnModelCreating
Как уже упоминалось, для изменения соглашений и сопоставлений можно использовать метод OnModelCreating класса DbContext.
Микрослужба заказов в eShopOnContainers реализует явное сопоставление и конфигурацию, когда это необходимо, как показано в следующем коде.
Можно задать все сопоставления текучего API в одном и том же методе OnModelCreating , но рекомендуется разбить этот код на несколько классов конфигурации, по одному на сущность, как показано в примере. В первую очередь отдельные классы конфигурации для настройки разных типов сущностей рекомендуется использовать для больших моделей.
В примере кода показано несколько явных объявлений и сопоставлений. Однако соглашения EF Core выполняют многие из этих сопоставлений автоматически, так что фактический код, который понадобится в вашем случае, может иметь меньший размер.
Критические изменения
Ниже перечислены основные направления, по которым планируется работа над выпуском EF Core 6.0.
Столбцы JSON
Отслеживается по проблеме № 4021
Размер футболки: Средний
Эта возможность позволит определить общие механизмы и шаблоны для поддержки JSON, которые могут быть реализованы любым поставщиком базы данных. Мы планируем совместно с участниками сообщества согласовать существующие реализации для Npgsql и Pomelo MySQL, а также добавить поддержку для SQL Server и SQLite.
Сопутствующие вопросы
Это исправления ошибок и улучшения, которые пока не запланированы для выпуска 6.0, но мы будем рассматривать их в зависимости от того, как будет продвигаться описанная выше работа. Эти проблемы могут быть включены в процесс работы над выпуском EF Core 6.0 и автоматически становятся кандидатами на решение в следующем выпуске.
Кроме того, при планировании мы всегда учитываем вопросы, набравшие больше всего голосов. Исключение любой из подобных проблем из выпуска всегда дается нелегко, но нам нужен реалистичный план с учетом ресурсов, которыми мы располагаем. Обязательно проголосуйте (👍) за интересующие вас возможности.
Сопоставление таблиц
Сопоставление таблиц определяет таблицы, данные которых должны запрашиваться и сохраняться в базе данных. Ранее было показано, как сущности предметной области (например, предметная область продуктов или заказов) могут использоваться для создания схемы связанной базы данных. Технология EF строго построена на концепции соглашений. Соглашения рассматривают такие вопросы, как "Что будет называться таблица?" или "Какое свойство является первичным ключом?" Соглашения обычно основаны на обычных именах. Например, первичный ключ обычно является свойством, которое заканчивается на Id .
По соглашению каждая сущность будет настроена для сопоставления с таблицей с именем, указанным в свойстве DbSet , которое предоставляет сущность в производном контексте. Если для конкретной сущности значение свойства DbSet не задано, то используется имя класса.
Управление миграциями
Отслеживается по проблеме № 22945
Размер футболки: большой
Увеличение количества миграций, создаваемых для приложения, может стать проблемой. Кроме того, такие миграции часто развертываются вместе с приложением, даже если это не требуется. В связи с этим в EF Core 6.0 мы планируем реализовать улучшенные средства, а также функции управления проектами и сборками. В частности, мы планируем решить эти две проблемы: сжатие нескольких миграций в одну и повторное создание чистого моментального снимка модели.
Обновление. Большая часть работы в этой области была исключена из версии 6.0 из-за ограничения ресурсов.
GraphQL
Состояние: ведутся работы
Размер футболки: продолжается
5. Сравнение производительности
Среда выполнения: Visual Studio 2013, Entity Framework 6.1.1 (Database First), SQL Server 2012. Для тестирования использовалась таблица [Order] (схема таблицы приведена выше). Были проведены измерения времени выполнения для рассматриваемых в статье подходов к сохранению данных в БД, результаты представлены в ниже (время указано в секундах):
Insert
Способ фиксации изменений в базе данных | Количество записей | ||
---|---|---|---|
1000 | 10000 | 100000 | |
Add + SaveChanges | 7,3 | 101 | 6344 |
Add + (AutoDetectChangesEnabled=false) + SaveChanges | 6,5 | 64 | 801 |
Add + отдельный контекст + SaveChanges | 8,4 | 77 | 953 |
AddRange + SaveChanges | 7,2 | 64 | 711 |
SqlBulkCopy | 0,01 | 0,07 | 0,42 |
Ого! Если использовать метод Add для добавления в контекст и SaveChanges для сохранения, сохранение 100000 записей в БД займет почти 2 часа! В то время, как SqlBulkCopy на эту же задачу тратит менее секунды!
Update
Способ фиксации изменений в базе данных | Количество записей | ||
---|---|---|---|
1000 | 10000 | 100000 | |
SaveChanges | 6,2 | 60 | 590 |
SqlBulkCopy + MERGE | 0,04 | 0,2 | 1,5 |
Вновь SqlBulkCopy вне конкуренции. Исходный код тестового приложения доступен на GitHub.
Предоставление приложениям доступа к соглашениям о построении модели
Отслеживается по проблеме № 214
Размер футболки: Средний
Отсутствие ошибок
Размер футболки: большой
Мы планируем устранить все нерешенные ошибки во время работы над выпуском EF Core 6.0. Важные аспекты
- Это относится к проблемам с меткой type-bug.
- Будут определены исключения, например ошибки, для устранения которых требуется изменение проекта или реализация новой функции. Такие проблемы получат метку blocked .
- При необходимости по мере приближения выхода общедоступной версии или RTM-выпуска мы будем прекращать работу над некоторыми ошибками с учетом связанной с ними степени риска.
Знакомство с Entity Framework Core
Поскольку общие сведения о EF Core уже доступны в документации Майкрософт, здесь будут просто приведены ссылки на эту информацию.
Moving the optimization to SQL level
The repetition had to be removed, but it couldn't be done by IN clause with static identifiers list. The identifiers sub query results had to be gathered once and reused at SQL level. A potential solution I could think of was temporary table. The creation and access in case of temporary table shouldn't be expensive, it will also be automatically removed when the session ends. This seemed to be a reasonable approach so a POC was born.
Testing of POC has shown that this approach is more expensive than IN clause (CPU time ~550ms and overall duration ~660ms) but way better then original one (and can be considered acceptable). Now the same thing needed to be done using Entity Framework.
1. Insert/Update стандартными средствами Entity Framework
Начнем с Insert. Стандартным способом добавления новых записей в БД является добавление в контекст с последующим сохранением:
Каждый вызов метода Add приводит к дорогостоящему в плане выполнения вызову внутреннего алгоритма DetectChanges. Данный алгоритм сканирует все сущности в контексте и сравнивает текущее значение каждого свойства с исходным значением, хранимым в контексте, обновляет связи между сущностями и т.п. Известным способом поднятия производительности, актуальным до выхода EF 6, является отключение DetectChanges на время добавления сущностей в контекст:
Также рекомендуется не держать в контексте десятки тысяч объектов и сохранять данные блоками, сохраняя контекст и создавая новый каждые N объектов, например, как тут. Наконец, в EF 6 появился оптимизированный метод AddRange, поднимающий производительность до уровня связки Add+AutoDetectChangesEnabled:
К сожалению, перечисленные подходы не решают основной проблемы, а именно: при сохранении данных в БД, на каждую новую запись генерируется отдельный INSERT запрос!
С Update ситуация аналогичная. Следующий код:
приведет к выполнению отдельного SQL-запроса на каждый измененный объект:
В простейших случаях, может помочь EntityFramework.Extended:
Данный код выполнится в обход контекста и сгенерирует 1 SQL-запрос. Более подробно о скорости EF и работе с этой библиотекой в статье за авторством tp7. Очевидно, что решение не универсальное и годится только для записи во все целевые строки одного и того же значения.
Объекты значений
Отслеживается по проблеме № 9906
Размер футболки: Средний
Ранее наша команда разработчиков полагала, что принадлежащие сущности, которые предназначены для поддержки статистических вычислений, также целесообразно использовать для аппроксимации объектов значений. Однако опыт показал, что это не так. Соответственно, в EF Core 6.0 мы планируем оптимизировать использование объектов значений в рамках проблемно-ориентированного проектирования. Этот подход будет основываться на преобразователях значений, а не на принадлежащих сущностях.
Изначально объем работы в этом направлении ограничен поддержкой преобразователей значений, которые сопоставляются с несколькими столбцами. Возможно, с учетом полученных в процессе работы над выпуском отзывов будет реализована дополнительная поддержка.
Эксперименты и исследования
Команда разработчиков EF в процессе работы над выпуском EF Core 6.0 планирует уделять время экспериментам и исследованиям в двух областях. Мы рассматриваем это как процесс обучения и поэтому не устанавливаем никаких конкретных конечных показателей в этих областях для выпуска 6.0.
2. Поиск решения проблемы
Испытывая стойкое отвращение к написанию «велосипедов», я в первую очередь поискал best-practices для множественной вставки с помощью EF. Казалось бы, типовая задача — но подходящего решения «из коробки» найти не удалось. В то же время, SQL Server предлагает ряд техник быстрой вставки данных, таких как утилита bcp и класс SqlBulkCopy. О последнем и пойдет речь ниже.
- Отправлять данные на сервер поблочно с поддержкой транзакций;
- Выполнять маппинг колонок из DataTable на таблицу БД;
- Игнорировать constraints, foreign keys при вставке (опционально).
- Атомарность вставки (опционально);
- Невозможность продолжения работы после возникновения исключения;
- Слабые возможности по обработке ошибок.
EntityFramework.BulkInsert
На поверку оказавшийся нерабочим. При изучении Issues я наткнулся на дискуссию с участием… небезызвестной Julie Lerman, описывающую проблему, аналогичную моей и оставшуюся без ответа авторов проекта.
EntityFramework.Utilities
Живой проект, активное сообщество. Нет поддержки Database First, но обещают добавить.
Implementing the temporary table creation
First of all I needed an entity to represent the temporary table. As the project was using Code First approach I've created a POCO with same properties as the projection DTO (having a separated class allowed me to protect constructor and properties setters).
In order to map this entity to a temporary table TableAttribute can be used, but I prefer to keep mappings separated from entities so I have created a configuration class.
Now I needed to get the actual parametrized query out of IQueryable (I didn't want to hardcode everything so the solution would be more generic in future), this post got me going. First I've created a small extension method.
After separating those nasty internals I could write nice method which would get the job done.
That wasn't that hard. Proud of myself I've quickly changed the method I wanted to optimize.
When a parametrized query is being executed by ADO.NET provider for SQL Server it is being wrapped by call to sp_executesql - this is how parameters are being passed. The tricky part of sp_executesql is the fact that the statement is being executed in "inner scope". Temporary table created by parent scope would be visible inside of sp_executesql but one created within sp_executesql is not visible outside. In my case this meant that I had to separate creation of the table from filling it with data (I didn't want to go away from parametrized query for obvious reasons).
Скомпилированные модели
Отслеживается по проблеме № 1906
Размер футболки: очень большой
Благодаря скомпилированным моделям станет возможным создание скомпилированной формы модели EF. Это позволит повысить высокую производительность при запуске, а также общую производительность доступа к модели.
Дополнительные ресурсы
Читайте также: