IEntityTypeConfiguration в EF Core, или отделим отделимое

от автора

Почти каждый разработчик программного обеспечения работал с СУБД, по крайней мере каждый слышал о них. В мире существует множество способов для работы с базами данных и один из них — это ORM (англ. Oblect-Relational Mapping). Для разработчиков приложений, особенно бизнес-приложений, различного рода реализации данного способа стали в прямом смысле «спасательным кругом» в грубом мире работы с базами данных. Ещё начиная с .Net Framework компания Microsoft кидала такой круг разработчикам, который носил название Entity Framework (EF). И теперь, в .NET есть кроссплатформенная реализация старенького EF — Entity Framework Core (EF Core).

В EF Core существует несколько способов конфигурирования сущностей, все они рассмотрены в моей предыдущей статье — https://habr.com/ru/post/649647/. Лучший из них на мой взгляд — это реализация IEntityTypeConfiguration<>. Он позволяет отделить модель предметной области от хранилища, сделать структуру проекта более упорядоченной, а само решение более гибким. Далее по статье мы рассмотрим все преимущества данного способа. Итак, пришло время на не реальном реальном проекте разобраться наконец с этим способом описания отношений полей и сущностей.

Сделаем конфигурацию для магазина, который занимается продажей автомобилей. База данных у нас уже существует, нужно только «подружить» с ней наше решение на .NET. Будем считать, что структуру нашей будущей БД разрабатывал вменяемый человек, который соблюдал элементарные правила именования полей и т.п.

Имеем несколько таблиц Cars, EquipmentOptions, Makes, Models. Все они имеют поля, которые являются системными, имеют один и тот же тип и называются одинаково. Это поля Id, CreatedDateTime, UpdatedDateTime. Для описания этих полей, мы создадим базовую модель для будущих сущностей BaseEntity:

public class BaseEntity {     public virtual Guid Id { get; set; }     public virtual DateTime CreatedDateTime { get; set; }     public virtual DateTime? UpdatedDateTime { get; set; } }

После этого мы уже можем не описывать эти три поля в будущих моделях сущностей. Далее приводится листинг сущностей вышеприведенных таблиц.

CarEntity
public class CarEntity : BaseEntity {     public virtual string Vin { get; set; }     public virtual string EngineNum { get; set; }     public virtual string ChassisNum { get; set; }     public virtual string BodyNum { get; set; }     public virtual Guid EquipmentVariantId { get; set; }     public virtual EquipmentVariantEntity EquipmentVariant { get; set; } }

MakeEntity
public class MakeEntity : BaseEntity {     public string Name { get; set; }     public virtual EquipmentVariantEntity EquipmentVariant { get; set; }     public virtual ICollection<ModelEntity> Models { get; set; } }

ModelEntity
public class ModelEntity : BaseEntity {     public virtual string Name { get; set; }     public virtual Guid MakeId { get; set; }     public virtual MakeEntity Make { get; set; }     public virtual EquipmentVariantEntity EquipmentVariant { get; set; } }

EquipmentVariantEntity
public class EquipmentVariantEntity : BaseEntity {     public virtual string Engine { get; set; }     public virtual Guid ModelId { get; set; }     public virtual ModelEntity Model { get; set; }     public virtual ICollection<CarEntity> Cars { get; set; } }

Как видим, модели наши чисты и намерения наши светлы. Приступим к конфигурации сущностей. Подобно выделению базовой сущности — выделим базовую конфигурацию BaseEntityConfiguration<TEntity>:

internal class BaseEntityConfiguration<TEntity> : IEntityTypeConfiguration<TEntity> where TEntity : BaseEntity {     public virtual void Configure(EntityTypeBuilder<TEntity> builder)     {         builder.HasKey(k => k.Id);         builder.Property(p => p.Id).HasColumnName("Id");         builder.Property(p => p.CreatedDateTime).HasColumnName("CreatedDateTime").IsRequired();         builder.Property(p => p.UpdatedDateTime).HasColumnName("UpdatedDateTime");     } }

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

Ниже листинг данных конфигураций.

CarEntityConfiguration
internal class CarEntityConfiguration : BaseEntityConfiguration<CarEntity> {     public override void Configure(EntityTypeBuilder<CarEntity> builder)     {         base.Configure(builder);          builder.ToTable("Cars");          builder.Property(p => p.Vin)             .HasMaxLength(64)             .IsRequired();          builder.Property(p => p.BodyNum)             .HasMaxLength(64);          builder.Property(p => p.ChassisNum)             .HasMaxLength(64);          builder.Property(p => p.EngineNum)             .HasMaxLength(64)             .IsRequired();          builder.Property(p => p.EquipmentVariantId)             .HasColumnName("EquipmentVariantId")             .IsRequired();          builder.HasOne(o => o.EquipmentVariant)             .WithMany(m => m.Cars)             .HasForeignKey(fk => fk.EquipmentVariantId)             .IsRequired();     } }

MakeEntityConfiguration
internal class MakeEntityConfiguration : BaseEntityConfiguration<MakeEntity> {     public override void Configure(EntityTypeBuilder<MakeEntity> builder)     {         base.Configure(builder);          builder.ToTable("Makes");          builder.Property(p => p.Name)             .HasColumnName("Name")             .HasMaxLength(256)             .IsRequired();          builder.HasMany(m => m.Models)             .WithOne(o => o.Make)             .HasForeignKey(fk => fk.MakeId)             .IsRequired()             .OnDelete(DeleteBehavior.Cascade);     } }

ModelEntityConfiguration
internal class ModelEntityConfiguration : BaseEntityConfiguration<ModelEntity> {     public override void Configure(EntityTypeBuilder<ModelEntity> builder)     {         base.Configure(builder);          builder.ToTable("Models");          builder.Property(p => p.Name)             .HasColumnName("Name")             .HasMaxLength(256)             .IsRequired();          builder.Property(p => p.MakeId)             .HasColumnName("MakeId")             .IsRequired();     } }

EquipmentVariantEntityConfiguration
internal class EquipmentVariantEntityConfiguration : BaseEntityConfiguration<EquipmentVariantEntity> {     public override void Configure(EntityTypeBuilder<EquipmentVariantEntity> builder)     {         base.Configure(builder);          builder.ToTable("EquipmentOptions");          builder.Property(p => p.Engine)             .HasColumnName("Engine")             .HasMaxLength(20)             .IsRequired();          builder.Property(p => p.ModelId)             .HasColumnName("ModelId")             .IsRequired();          builder.HasOne(o => o.Model)             .WithOne(o => o.EquipmentVariant)             .HasForeignKey<EquipmentVariantEntity>(fk => fk.ModelId)             .IsRequired();     } }

Вот и всё, конфигурация сущностей произведена. На первый взгляд кажется, что такой подход это оверхед — куча классов, лишняя работа, мы же не индусы?. Действительно, если весь проект — это монолит, то такой подход избыточен, хоть и лаконичен. Там вполне подойдут атрибуты аннотаций данных. Но если например мы разрабатываем сервисы в микросервисной архитектуре — то такой подход кажется единственно верным для поддержания чистой архитектуры и гибкости, к которым многие так сильно стремятся. Модели сущностей мы можем положить в Shared-проект, в то время как конфигурацию вынести в проект, отвечающий за работу с конкретной БД. Таких проектов может быть несколько и все они будут переиспользовать наши модели сущностей и иметь свои конфигурации. Тем самым мы больше не зависим от конкретного хранилища и особенностей работы с ним, мы имеем чистые модели сущностей.

Выводы каждый делает сам, всё конечно зависит от целевой архитектуры. Если монолит — то можно не заморачиваться написанием конфигураций, а сделать всё атрибутами аннотаций данных (хоть на мой взгляд это и сильно загрязняет код и лучше прибегнуть к FluentApi прямо в OnModelCreating контекста). Но если речь идёт о чём-то более гибком — то речи и не может идти об атрибутах данных. Реализация IEntityTypeConfiguration<> кажется единственно верной.


ссылка на оригинал статьи https://habr.com/ru/post/710740/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *