Небольшие заметки

Entity Framework и ограничения DbCommand

September 28, 2020

На работе у нас есть некое подобие самописной ORM, работающей напрямую с ADO.NET и одна из проблем с которой мы сталкивались при её разработке - это то, что у количества параметров используемых в DbCommand есть лимит, этот лимит накладывается базой данных и у разных баз данных он разный.

Например, у PostgreSQL в каждом SQL statement (под SQL statement имеется в виду то, что в разговорной речи называют SQL запросом) может использоваться не больше 65535 параметров (в одну DbCommand можно отправить множество SQL statement и таким образом в общем DbCommand может содержать больше 65535 параметров).

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

INSERT INTO table_name (column_namе(s)) values (value1, ...), (valueN, ...), ...

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

И в запросах с использованием оператора IN:

SELECT column_name(s)
FROM table_name
WHERE column_name IN (value1, value2, ...);

Здесь список, в котором ищутся значения (для каждого значения из списка используется параметр), должен превышать 65535 элементов.

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

Очень многое в поведении может определить конкретный дата-провайдер, я исследовал только связку Entity Framework + Npgsql, поэтому что-то из текста может быть неактуально для других дата-провайдеров

Зачем вообще использовать параметры

Основная причина в использовании параметров - это защита от SQL инъекций:

Command objects use parameters to pass values to SQL statements or stored procedures, providing type checking and validation. Unlike command text, parameter input is treated as a literal value, not as executable code. This helps guard against “SQL injection” attacks, in which an attacker inserts a command that compromises security on the server into an SQL statement.

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

  • Use Parameterized SQL commands for all data access, without exception.
  • Do not use SqlCommand with a string parameter made up of a concatenated SQL String.

Insert множества значений в Entity Framework

В Entity Framework эта проблема не встречается (во всяком случае при работе с PostgreSQL), потому что для каждой вставляемой строки генерируется свой INSERT (SQL statement), а для каждого SQL statement мы можем использовать 65535 параметров, соответственно, чтобы превысить разрешённый лимит нужна таблица с 65536 столбцами:

dbContext.Countries.Add(new Models.Country { Name = "Албания"});
dbContext.Countries.Add(new Models.Country { Name = "Словения"});
dbContext.SaveChanges();
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (7ms) [Parameters=[@p0='?', @p1='?'], CommandType='Text', CommandTimeout='30']
      INSERT INTO "Countries" ("Name")
      VALUES (@p0)
      RETURNING "Id";
      INSERT INTO "Countries" ("Name")
      VALUES (@p1)
      RETURNING "Id";

В общем-то в том числе и поэтому для массовой вставки Entity Framework не очень пригоден.

Кстати, по поводу массового INSERT в дотнете и PostgreSQL недавно был доклад у DotNetRu: Евгений Фирстов - PostgreSQL: Under Pressure

IN оператор в Entity Framework

IN оператор генерируется, например, при вызове LINQ метода Contains():

var countryNames = Enumerable.Range(0, 100).Select(item => item.ToString()).ToList();
dbContext.Countries.Where(c => countryNames.Contains(c.Name)).Select(c => c.Name).ToArray();
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (2ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT c."Name"
      FROM "Countries" AS c
      WHERE c."Name" IN ('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23', '24', '25', '26', '27', '28', '29', '30', '31', '32', '33', '34', '35', '36', '37', '38', '39', '40', '41', '42', '43', '44', '45', '46', '47', '48', '49', '50', '51', '52', '53', '54', '55', '56', '57', '58', '59', '60', '61', '62', '63', '64', '65', '66', '67', '68', '69', '70', '71', '72', '73', '74', '75', '76', '77', '78', '79', '80', '81', '82', '83', '84', '85', '86', '87', '88', '89', '90', '91', '92', '93', '94', '95', '96', '97', '98', '99')

Тут проблемы в Entity Framework тоже нет, просто потому что он вообще не использует параметры при формировании такого запроса, беря на себя риски с возможными SQL инъекциями.

Кстати, если интересно можете изучить в коде, как Entity Framework генерирует запросы (ссылки в конце)

Как мы решили проблему в своей ORM

Мы считаем параметры и при достижении лимита начинаем новый SQL statement.

Таймаут времени выполнения команды

Как мы выяснили, проблема с лимитом параметров не актуальна для Entity Framework, но у команды есть и другое ограничение, вероятность столкнуться с которым больше - это таймаут времени выполнения (часто путают его с Connection Timeout - но это разные вещи, Connection Timeout отвечает за таймаут установления соединения).

У команды есть лимит времени, отведённый на её выполнение, если команда выполняется дольше, то выполнение прерывается выбрасыванием исключения. Выше мы видели, что при вставке множества новых строк, Entity Framework объединят INSERT’ы в одну команду, таким образом потенциально общее время выполнения такой команды может быть большим. Точно также он поступает и в случае операций обновления и удаления и вообще он склонен все возможные операции проводимые на базе данных в один момент времени упаковывать в одну команду (при вызове SaveChanges, например).

В логах Entity Framework выше, можно увидеть что CommandTimeout для запросов он устанавливает в 30 секунд:

info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (7ms) [Parameters=[@p0='?', @p1='?'], CommandType='Text', CommandTimeout='30']

Выбирает такое значение он, вероятно, потому что это значение по умолчанию для Npgsql.

Изменить его можно либо задав прямо в connection string подключения к базе данных, либо в коде

Дополнительные ссылки

  1. Генерация запросов чтения данных: Entity Framework QuerySqlGenerator, Npgsql QuerySqlGenerator
  2. Генерация запросов изменения данных Entity Framework UpdateSqlGenerator, Npgsql UpdateSqlGenerator
  3. Статья в которой исследуется ограничение параметров применительно к SQL Server: Playing with parameters limit on SQL Server with Entity Framework
Комментировать

Про Entity Framework и DDD

September 03, 2019

Обычно так случалось, что если архитектура у нас по DDD, а проект более менее большой и серьёзный, то мы как правило разделяли доменную и дата модели на две отдельные, потому что Entity Framework в своих ранних версиях накладывал много ограничений и специфичных требований на модели, но в процессе взросления EF эти ограничения становились всё меньше и теперь уже Entity Framework Core позволяет в качестве и доменных и дата моделей использовать одну и ту же модель не идя на компромиссы. А как это делать можно прочитать в статьях Джули Лерман:

  1. DDD-Friendlier EF Core 2.0
  2. DDD-Friendlier EF Core 2.0, Part 2

Или посмотреть в её выступлении на NDC Conference: Mapping DDD Domain Models with EF Core 2.1

И ещё есть хорошая статья на хабре: Сущности в DDD-стиле с Entity Framework Core

Подход с объединением моделей проще, потому что при использовании отдельных моделей для домена и дата слоя необходимо решить проблему отслеживания изменений в доменных сущностях и правильного переноса этих изменений в дата модели, так чтобы Entity Framework смог сохранить всё корректно. Готовой статьи на эту тему дать не смогу, но, в принципе, эта проблема релевантна проблеме под названием “Работа с отсоединёнными сущностями” - в случае, когда доменные и дата модели разделены, доменные модели это по сути и есть отсоединённые сущности, так что идеи как решить проблему синхронизации доменных и дата моделей можно почерпнуть, изучая как решают проблему работы с отсоединёнными сущностями. А статья на эту тему, например, вот: Доступ к данным - Обработка состояния отсоединенных сущностей в EF всё той же Джули Лерман.

Ну и если говорить о DDD, то ещё можно затронуть тему Спецификаций и в статье ниже отличный пример того, как можно реализовать спецификации для использования в EF: EntityFramework: (анти)паттерн Repository

Комментировать

Прерывание выполнения потока в .NET Core

August 28, 2019

В .NET Core выпилили метод Thread.Abort() и это значит, что теперь невозможно насильственное прерывание выполнения потока извне этого потока средствами управляемого кода.

Так что все эти знания о нюансах использования Thread.Abort() и чудесных способностях выбираться из try/catch блоков исключения ThreadAbortException, которые мы заучивали в Рихтере, теперь бесполезны - сегодня выкидывание ThreadAbortException невозможно.

Цитата из документации:

Even though this type exists in .NET Core, since Abort is not supported, the common language runtime won’t ever throw ThreadAbortException.

В этом Issue на GitHub подробное обсуждение плюсов и минусов такого решения.

Если кратко о причинах, то это было сделано, потому что очень дорого писать библиотеки, которые гарантировали бы защиту от corrupted state всего процесса, то есть гарантировали бы безопасность дальнейшей работы процесса, в условиях, когда в любой момент выполнение потока может быть прервано - а именно такие библиотеки нужно было написать разработчика .NET Core. Поэтому если вам нужно прерывать выполнение потока, то используйте CancellationToken, если это ваш код или код чужих разработчиков поддерживающий его, или запускайте код, который потребует прерывание выполнения потока в отдельном процессе и прекращайте его целиком.

Вот некоторые цитаты из этого обсуждения:

The key problem with thread abort is that it affects reliability of the whole stack. If you are using thread abort, all code (ie all libraries) running in the process have to be robust against being killed by thread abort at any point. It is extremely expensive to audit and write libraries with this constrain.

Try to review any code that is doing a more complex managed/unmanaged interop (e.g. sockets in CoreFX) and try to find places where inserting a thread abort exception would cause bad things to happen. I am sure that you are going to find many places where inserting thread abort would lead to hangs, crashes or data corruptions. And there will be a lot more that the tests would discover. People just do not naturally write code that is able to recover from being aborted at any point.

It is hard to tell where it is “safe” to insert the thread aborts. Libraries that need to be robust in presence of Thread.Abort need to be annotated for it, coded in a special way and stress tested.

We have tried to do this in .NET Framework: It was a full time job for several people to run a stress harness that inserted thread about at random points in .NET Framework, and file and fix bugs on the crashes, hangs and data corruptions that it hit. This was done only for a subset of .NET Framework that was usable in SQLCLR, and still it was never ending stream of issues.

Even with this effort, we often got a support escalation (from paid support) where people hit problems with Thread.Abort in production. Some of these issues require a very ugly hacks to workaround because of there was just no right fix to them. We had to resort to hacks like decoding assembly instructions and suppress or adjust thread abort behavior for particular instruction pattern that was known to hit the problem.

I am not even talking about larger .NET ecosystem - if you take a random NuGet package from nuget.org, it is almost guaranteed that it has reliability bugs in the presence of thread abort.

Да и Джон Скит, Эрик Липперт и Джо Даффи, задолго до .NET Core тоже не рекомендовали использовать Thread.Abort()

Комментировать

Про производительность Entity Framework

August 15, 2019

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

Performance considerations for EF 4, 5, and 6

Но whitepaper никак не решает проблему того, что Entity Framework в принципе не приспособлен для массовых операций изменения данных - массовых Insert, Update и Delete. Тут проблема лежит в двух плоскостях - тормозит DbContext и тормозит сама база данных, так как EF производит все изменения отдельными sql запросами (хотя и в рамках одного физического запроса к базе данных).

Техники работы с контекстом, чтобы он меньше тормозил при Insert рассмотрены в этой статье Rick Strahl: Entity Framework and slow bulk INSERTs

И техника с пересозданием DbContext и “пакетным” сохранением описанная там, довольно эффективна, а в этом ответе на SO есть сравнение влияния размера пакетов на скорость сохранения, так чтобы вы могли выбрать оптимальный.

Для ускорения контекста при массовом обновлении и удалении, тоже есть “хаки”:

  1. Аттачить к контексту “болванки” (сущности, которые на загружались из БД и которых нет в контексте), а затем явно их помечать в контексте как обновлённые или удалённые (через DbContext.Entry), кое-что по этой теме можно найти здесь.
  2. Также как при Insert пакетировать сохранение.
  3. Также как при Insert отключать автоматический поиск изменений в контексте.

Но самый эффективный путь - это вообще отказаться от использования Entity Framework для массовых операций изменения данных и, например, напрямую использовать SQL. (Если решите пойти этим путём, то загляните ещё в эту статью, в ней автор рассказывают свою идею как сделать такие SQL-запросы более типизированными.)

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

Например, вот в этой статье, рассказано, как использовать специальные возможности MSSQL Server для ускорения Insert и Update на уровне базы данных: Entity Framework: повышаем производительность при сохранении данных в БД. Подобную возможность, кстати, предоставляет и PostgreSQL.

Ну и буквально на днях наткнулся на библиотеку Linq2Db у которой есть интеграция с Entity Framework и с помощью которой, судя по всему, без проблем можно будет делать массовый Insert, Update, Delete. Кстати, умеет использовать специальные высокопроизводительные операции специфичные для базы данных. Но предупреждаю, сам её ещё не использовал.

Комментировать

Несколько интересных статей про Entity Framework Core

August 12, 2019

Не помню уже, как велики были возможности логирования в классическом Entity Framework, но у Entity Framework Core с этим точно всё в порядке, статья на эту тему: Настройка логирования в Entity Frmework Core

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

Статья с разбором продвинутых техник использования проекций в Entity Framework Core.

  1. Как хранить проекции для их переиспользования.
  2. Как хранить и использовать вложенные проекции для проекций.
  3. И наконец самое интересное - способ создания вложенных проекций не для коллекций, а для одиночных сущностей (для вложенной проекции одиночной сущности нельзя вызвать Select, а значит и нельзя сделать проекцию.

EF Core 1.1: Read Only Entities & Extending Metadata with Annotations - статья показывающая пример работы с аннотациями метаданных сущностей в EF Core.

Ну и наконец две статьи в которых рассказывается, как можно изменить генерацию SQL в Entity Framework Core.

  1. Extending SQL Generation in Entity Framework Core
  2. Реализуем свой оператор в Entity Framework Core
Комментировать

Статья про джойны

July 30, 2019

Статья с хорошим объяснением механизма работы джойнов. Если вы забыли, то джойн даёт в результате декартово произведение записей двух таблиц. А пока читал вспомнил, что когда работал с NHibernate при джойне двух таблиц со связью один ко многие, часто сталкивался с проблемой появления дублирующихся записей в результате выборки. Интересно, можно ли такую аномалию получить при использовании Entity Framework.

Комментировать

Выгрузка сборок в dotnet core 3

June 18, 2019

Благая весть идёт: предстоящий .NET Core 3 принесёт нам возможность выгружать сборки из памяти. Сделать это можно будет с помощью класса AssemblyLoadContext. А вот тут инструкция по использованию.

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

Комментировать

Прошлое и будущее командной строки Windows

June 20, 2018

Серия статей про командную строку Windows. Здесь всё что вы хотели знать о ней, но боялись спросить. Ну как минимум стоит прочитать, если хотите узнать чем отличается TTY, PTY, терминал, консоль и командная строка.

Комментировать

Защитное программирование

April 04, 2018

Старый перевод старой статьи Марка Симана, автора книги “Внедрение зависимостей в .NET”. В ней он рассуждает о концепции защитного программирования, также описанной в книге “Совершенный код” (впервые?).

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

Суть такова: Ваш код не должен возвращать и передавать в другие методы null. Если вы находитесь в защищенной местности (область кода в которой вы можете быть уверены, что null вам не будет возвращен из метода или свойства объекта), то вы можете быть уверены, что null вы сами никуда не передадите, соответственно никакие дополнительные проверки для этого не нужны и всё сводится к:

  1. Проверяйте данные приходящие из незащищённой местности (пользовательский ввод, системы не являющиеся вашей зоной контроля или не следующие концепции защитного программирования).
  2. Не возвращайте в защищённой местности из ваших методов и объектов null.
Комментировать

Хорошая статья про DRY

March 22, 2018

Сама статья

Оказалось, что принцип DRY был впервые описан в книге “Программист Прагматик”, которую я обязательно прочитаю в будущем.

Принцип DRY о дублировании знания (обычно к знаниям относят бизнес-логику или алгоритмы), а не кода, поэтому:

  1. Дублирование знания является нарушением принципа DRY.
  2. Дублирование кода не обязательно является нарушением принципа DRY.

Неправильное применение принципа DRY приведёт к:

  1. Бесполезным абстракциям
  2. Преждевременной оптимизации

Которые в свою очередь приведут к увеличению сложности и ненужному объединению кода.

В комментах дали ссылку на “Правило трёх”

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

Комментировать