Всем привет.

Я недавно обнаружил, что не все, кто работают с EF, умеют его готовить. Более того, не горят желанием разбираться. Сталкиваются с проблемами на самых ранних этапах — настройке.
Даже после успешной настройки появляются проблемы с запросами данных. Не потому, что люди не знают LINQ, а потому что не все можно смаппить из объектов в реляционные модели. Потому что работая с линком люди думают таблицами. Рисуют SQL запросы и пытаются перевести их в LINQ.

Об этом и, возможно, о чем-то еще я и хочу поговорить в статье.

Настройка


Пришел человек на проект, впервые увидел там EF, подивился такому чуду, научился пользоваться, решил использовать в личных целях. Создал новый проект,… А дальше что делать?

Начинается гуглёшь, пробы, ошибки. Для того, чтобы просто подключить EF к проекту, разработчик сталкивается с непонятного рода проблемами.

1. А как настроить его на работу с нужной СУБД?
2. А как настроить работу миграций?

Уверен, проблем больше, но это, наверное, самые частые. Давайте обо всем по порядку.

1. Ну тут именно гуглить. :) Ваша задача найти провайдера EntityFramework Core для вашей СУБД.

Так же в описании провайдера вы найдете инструкции по настройке.

Для PostgreSQL, например, нужно установить Nuget пакет Npgsql.EntityFrameworkCore.PostgreSQL
Создать собственный контекст, принимающий опции DbContextOptions и создать все это дело таким вот образом

var opts = new DbContextOptionsBuilder<MyDbContext>()
        .UseNpgsql(constring);

var ctx = new MyDbContext(opts.Options);


Если у вас ASP .NET Core приложение, контекст регистрируется в контейнере по другому. Но это уже вы можете в своем рабочем проекте подглядеть. Или в гугле.

2. Если нянек в вашей компании нет, то вы, скорее всего, в курсе, как установить необходимые инструменты для работы с миграциями. Хоть для диспетчера пакетов, хоть NET Core CLI.

Но вот о чем вы, возможно, не в курсе, так это о том, что выбранный стартовый проект (--startup-project) при работе с миграциями запускается. Это значит, что если стартовым проектом для миграций является тот же проект, который запускает ваше приложение и при запуске вы зачем-то накатываете миграции через ctx.Database.Migrate(), то при попытке создать удалить ранее созданную миграцию или создать еще одну, то последняя созданная миграция накатится на базу. СЮРПРИЗ!

Но, возможно, при попытке создать первую миграцию вы словите что-то типа этого

No database provider has been configured for this DbContext. A provider can be configured by overriding the DbContext.OnConfiguring method or by using AddDbContext on the application service provider. If AddDbContext is used, then also ensure that your DbContext type accepts a DbContextOptions<TContext> object in its constructor and passes it to the base constructor for DbContext.

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

Настройка моделей


Вот вы уже создали вашу первую миграцию, но она почему то пустая. Хотя у вас есть модели.

Дело в том, что вы не научили ваш контекст транслировать ваши модели в вашу БД.

Для того, чтобы EF создал миграцию для создания таблицы для вашей модели в БД, необходимо как минимум завести свойство типа DbSet<MyEntity> в вашем контексте.

например, по такому вот свойству
public DbSet<MyEntity> MyEntities { get; set; }

Будет создана таблица MyEntities с полями, соответствующими свойствам сущности MyEntity.

Если вы не хотите заводить DbSet или хотите повлиять на дефолтные правила создания таблицы для сущности, вам нужно переопределить метод
protected override void OnModelCreating(ModelBuilder modelBuilder)
вашего контекста.

Как это сделать, вы скорее всего знаете. Более того, вы наверняка знакомы с атрибутами, позволяющими управлять правилами маппинга. Но вот какая есть штука. Атрибуты не дают полного контроля над маппингом, а значит у вас будут уточнения в OnModelCreating. То есть правила маппинга сущностей у вас будут и в виде аннотаций и в виде fluent api. Ходи потом ищи, почему поле имеет не то имя, которое вы ожидали, или не те ограничения.

— Ну тогда все просто, — скажете вы — Я буду настраивать все через в OnModelCreating

И тут, после настройки 20й сущности из 10 полей, у вас начинает рябить в глазах. Вы пытаетесь найти настройку поля у какой либо сущности, но перед глазами все плывет от единообразной плахи длиной в 200-500 строк.

Да, можно разбить эту плаху на 20 методов и станет чуточку проще. Но полезно знать, что есть интерфейс IEntityTypeConfiguration<TEntity>, реализовав который для конкретной сущности, вы можете описать в этой реализации правила маппинга конкретной сущности. Ну а для того, чтобы контекст это подхватил, в OnModelCreating нужно написать
modelBuilder.ApplyConfigurationsFromAssembly(assemblyWithConfigurations);

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

foreach (var idEntity in modelBuilder.Model.GetEntityTypes()
    .Where(x => typeof(BaseIdEntity).IsAssignableFrom(x.ClrType))
    .Select(x => modelBuilder.Entity(x.ClrType)))
{
    idEntity.HasKey(nameof(BaseIdEntity.Id));
}

Построение запросов


Ну вот, порядок навели, стало чуточку приятнее.

Теперь осталось переделать запросы нашего домашнего проекта из SQL в Linq. Я уже писал запросы на работе, это проще простого
ctx.MyEntities.Where(condition).Select(map).GroupBy(expression).OrderBy(expression)… легкота.

Так, какой там у нас запрос?

SELECT bla bla bla FROM table
RIGHT JOIN....... 

ага

ctx.LeftEntities.RightJoin(.... f@#$

Я вот пользовался Linq и его методами расширения задолго до того, как познакомился с EF. И у меня ни разу не возникало вопроса, а где же в нем RIGHT JOIN, LEFT JOIN…

Что такое LEFT JOIN с точки зрения объектов?

Это


class LeftEntity 
{
    public List<RightEntity> RightEntities { get; set; }
}

Это даже не магия. У нас просто есть некая сущность, которая ссылается на список других сущностей, но при этом может не иметь ни одной.

То есть это

ctx.LeftEntities.Include(x => x.RightEntities)

А что такое RIGHT JOIN? Это перевернутый LEFT JOIN, то есть мы просто начинаем с другой сущности.

Но бывает всякое, иногда нужен контроль над каждой связкой, как отдельной сущностью, даже если левой сущности не сопоставлено ни одной правой (правая NULL). Поэтому явно LEFT JOIN можно выполнить так

ctx.LeftEntities.SelectMany(x => x.RightEntities.DefaultIfEmpty(), (l, r) => new { Left = l, Right = r })

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

Мне тоже эта затея не нравится, но у всех свои причуды и нескончаемая любовь в Excel. Так что прикроем мат кашлем, выругавшись в кулак, и продолжим работать. Что там у нас дальше?

FULL JOIN

Так, ну я уже понял, не думаем SQLем, думаем объектами, вот так, а теперь вот так… вот черт. А это как изобразить?

Без кортежей никуда.

Вообще, когда нам приходится работать с кортежами, мы теряем понимание о типе связи (1-1, 1-n, n-n) и вообще, теоретически, можем скрестить мух и слонов. Это черная магия.
Сделаем это!

За основу возьмем many-to-many.

Таким образом имеем 3 типа сущности

LeftEntity
RightEntity
LeftRight (для организации связи)

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

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

Получается, что LeftRight нам не подходит в качестве вывода, его ограничения не позволят создать связку без одной из сущностей. Если вы практикуете DDD, вы словите неконсистенцию.
Даже если не практикуете, делать так не стоит. Создадим новый тип для вывода
LeftRightFull, который все так же содержит ссылку на левую и на правую сущности.

Итак, у нас есть

Left
L1
L2

Right
R1
R2

LeftRight
L2 R2

Мы хотим на выходе
L1 n
L2 R2
n R1

Начнем с левого соединения

var query = ctx.LeftEntities
    .SelectMany(x => x.RightLinks.DefaultIfEmpty(), (l, lr) => new LeftRightFull
    {
        LeftEntity = l,
        RightEntity = lr.RightEntity
    })


Вот, мы уже имеем
L1 n
L2 R2

Чего дальше то? Прилепить RightEntity через SelectMany? Ну так у нас получится
L1 n R1 n
L1 n R2 L2
L2 R2 R1 n
L2 R2 R2 L2

Отсеять лишнее (L2 R2 R1 n и L1 n R2 L2) по условию мы сможем, однако, остается непонятным, как L1 n R1 n превратить в
L1 n
n R1

Возможно, раскопки увели нас не в ту степь. Давайте просто соединим эти аналогичные запросы с левыми соединениями для левых и правых сущностей через Union.
L1 n
L2 R2
UNION
L2 R2
n R1

Тут могут возникнуть вопросы по быстродействию. На них я не отвечу, потому что все будет зависеть от конкретной СУБД и дотошности ее оптимизатора.

В качестве альтернативы можно создать View с нужным запросом. На EF Core 2 Union не работает, так что мне пришлось создать View. Однако, рекомендую про нее не забывать. Вдруг, вы решите реализовать мягкое удаление сущностей через флаг IsDeleted. Во вьюхе нужно это поддержать. Если забудете, то неизвестно когда получите багрепорт.

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

Всем пока.