EntityFrameworkCore потрясающий инструмент который сильно упрощает нам жизнь, но если не контролировать какие SQL запросы уходят в СУБД, то это может доставить боль при обновлениях мажорных версий, т.к. EF может в целом изменить подходы к построению SQL и что-нибудь сломать или сильно замедлить запросы из-за генерации неэффективных запросов.
А такое случается достаточно часто, несмотря даже на сумасшедшие 286 882 тестов, которые к сожалению всё равно не могут покрыть всевозможные сценарии и так или иначе будут просачиваться баги.
К примеру, несколько лет назад на одном проекте при апдейте с версии 2.2 на 3.0, у меня один запрос стал тянуть чуть ли не все данные из СУБД, из-за чего ему стало не хватать work_mem, поэтому он начал активно использовать диск и генерировал временные файлы. Но запрос выполнялся долго, отменялся по таймауту, но почему-то не подчищалась папка temp и таким образом диск быстро заполнялся и пришлось ребутать СУБД чтобы он не ушел в режим readonly.
Решилось всё переписыванием запроса с учетом особенностей уже версии 3.0.
Тогда же занесли еще один досадный баг, когда просто забыли рекурсивно пройтись по деревьям выражений из-за чего сломалось использование PredicateBuilder, а сам фикс был однострочным, но помню много времени ждал пока этот фикс доедет до релиза.
А из свежих, в 9 версии сломали GroupBy в некоторых кейсах.
Подобные проблемы регулярно встречаются и хорошо бы это покрыть тестами и недопустить проблемы до прода.
Самый простой способ тестирования генератора SQL запросов - это snapshot тесты.
В .NET есть крутая библиотека Verify и Verify.EntityFramework.
Используя его, мы можем встроиться в DbContext и с помощью интерцепторов записывать все исходящие SQL запросы и сохранить их как снепшот.
При апдейте версии EFCore или при изменении нашего кода, такие тесты покажут изменения в SQL запросах если они были.
Полноценного примера в доке нет, поэтому держите гайд:
1. В Composition Root (в проекте со Startup.cs или Program.cs) добавить зависимость от Verify.EntityFramework и вызвать метод расширения EnableRecording на DbContextOptionsBuilder. Его надо добавить только при запуске тестов:
builder.Services.AddDbContext((sp, options) =>
{
// тут настройка DbContext: options.UseNpgsql и т.п.
// при запуске интеграционных тестов окружение проставляю в IntegrationTests.
if (_env.EnvironmentName == "IntegrationTests" && options is DbContextOptionsBuilder o)
{
o.EnableRecording();
}
}
2. В сборке с интеграционными тестами подключить библиотеку Verify.NUnit/XUnit и инициализировать Verify.EntityFramework:
public static class ConfigureVerify
{
[ModuleInitializer]
public static void Init()
{
var model = GetDbModel();
VerifyEntityFramework.Initialize(model);
}
private static IModel GetDbModel()
{
var options = new DbContextOptionsBuilder();
options
.UseNpgsql("fake")
.UseSnakeCaseNamingConvention();
using var data = new ApplicationDbContext(options.Options);
return data.Model;
}
}
3. В тестах обернуть в using вызов тестируемого кода (всю секцию act)
using (Recording.Start())
{
// act
await sut.ExecuteAsync();
}
и вызывать Verify в качестве assert:
await Verify().AutoVerify(includeBuildServer: false);
После успешного прохода теста, появится *.verified.txt файл, в котором будут все отправленные SQL запросы в рамках тестируемого сценария:
{
ef: [
{
Type: ReaderExecutedAsync,
HasTransaction: true,
Text:
-- StrictOrderingOutboxRepository:LockAndReturnItemsBatchAsync
SELECT * FROM kafka_ef_outbox_items
ORDER BY id ASC
LIMIT 10
FOR UPDATE NOWAIT;
},
]
}
Если изменить код, добавить еще SQL вызовы или поменять их местами, то изменится и снепшот, который можно будет провалидировать и закоммитить в git репозиторий.
Если изменится SQL запрос, можно всегда его отсюда скопировать и сделать explain, посмотреть на план запроса и либо переписать либо оставить как есть.
А такое случается достаточно часто, несмотря даже на сумасшедшие 286 882 тестов, которые к сожалению всё равно не могут покрыть всевозможные сценарии и так или иначе будут просачиваться баги.
К примеру, несколько лет назад на одном проекте при апдейте с версии 2.2 на 3.0, у меня один запрос стал тянуть чуть ли не все данные из СУБД, из-за чего ему стало не хватать work_mem, поэтому он начал активно использовать диск и генерировал временные файлы. Но запрос выполнялся долго, отменялся по таймауту, но почему-то не подчищалась папка temp и таким образом диск быстро заполнялся и пришлось ребутать СУБД чтобы он не ушел в режим readonly.
Решилось всё переписыванием запроса с учетом особенностей уже версии 3.0.
Тогда же занесли еще один досадный баг, когда просто забыли рекурсивно пройтись по деревьям выражений из-за чего сломалось использование PredicateBuilder, а сам фикс был однострочным, но помню много времени ждал пока этот фикс доедет до релиза.
А из свежих, в 9 версии сломали GroupBy в некоторых кейсах.
Подобные проблемы регулярно встречаются и хорошо бы это покрыть тестами и недопустить проблемы до прода.
Самый простой способ тестирования генератора SQL запросов - это snapshot тесты.
В .NET есть крутая библиотека Verify и Verify.EntityFramework.
Используя его, мы можем встроиться в DbContext и с помощью интерцепторов записывать все исходящие SQL запросы и сохранить их как снепшот.
При апдейте версии EFCore или при изменении нашего кода, такие тесты покажут изменения в SQL запросах если они были.
Полноценного примера в доке нет, поэтому держите гайд:
1. В Composition Root (в проекте со Startup.cs или Program.cs) добавить зависимость от Verify.EntityFramework и вызвать метод расширения EnableRecording на DbContextOptionsBuilder. Его надо добавить только при запуске тестов:
builder.Services.AddDbContext((sp, options) =>
{
// тут настройка DbContext: options.UseNpgsql и т.п.
// при запуске интеграционных тестов окружение проставляю в IntegrationTests.
if (_env.EnvironmentName == "IntegrationTests" && options is DbContextOptionsBuilder o)
{
o.EnableRecording();
}
}
2. В сборке с интеграционными тестами подключить библиотеку Verify.NUnit/XUnit и инициализировать Verify.EntityFramework:
public static class ConfigureVerify
{
[ModuleInitializer]
public static void Init()
{
var model = GetDbModel();
VerifyEntityFramework.Initialize(model);
}
private static IModel GetDbModel()
{
var options = new DbContextOptionsBuilder();
options
.UseNpgsql("fake")
.UseSnakeCaseNamingConvention();
using var data = new ApplicationDbContext(options.Options);
return data.Model;
}
}
3. В тестах обернуть в using вызов тестируемого кода (всю секцию act)
using (Recording.Start())
{
// act
await sut.ExecuteAsync();
}
и вызывать Verify в качестве assert:
await Verify().AutoVerify(includeBuildServer: false);
После успешного прохода теста, появится *.verified.txt файл, в котором будут все отправленные SQL запросы в рамках тестируемого сценария:
{
ef: [
{
Type: ReaderExecutedAsync,
HasTransaction: true,
Text:
-- StrictOrderingOutboxRepository:LockAndReturnItemsBatchAsync
SELECT * FROM kafka_ef_outbox_items
ORDER BY id ASC
LIMIT 10
FOR UPDATE NOWAIT;
},
]
}
Если изменить код, добавить еще SQL вызовы или поменять их местами, то изменится и снепшот, который можно будет провалидировать и закоммитить в git репозиторий.
Если изменится SQL запрос, можно всегда его отсюда скопировать и сделать explain, посмотреть на план запроса и либо переписать либо оставить как есть.