Почти каждый (ну или почти каждый) разработчик слышал про такую технологию программирования, как ORM (англ. Oblect-Relational Mapping) или, если по-русски - объектно-реляционное отображение. Данная технология связывает базы данных с концепциями объектно-ориентированного программирования и, благодаря ей, многие разработчики уже стали забывать о работе с базами данных в чистом виде, например, создавая структуру данных непосредственно в СУБД.
Платформа .NET имеет кросплатформенную реализацию технологии ORM - Entity Framework Core (далее EF Core). С помощью данной технологии мы в принципе можем абстрагироваться от особенностей работы конкретной СУБД и в большинстве случаев, не подвергая модификации код нашего продукта перейти на любую другую СУБД.
Существует несколько вариантов конфигурирования сущностей, о которых мы и будем говорить в данной статье.
Дальнейшее рассмотрение способов конфигурирования подразумевает, что Вы уже знакомы с EF Core и можете поднять самый простой проект работающий с СУБД, использующий данную технологию.
Итак, давайте рассмотрим способы, как можно сконфигурировать сущности и отношения между ними.
Способ первый - атрибуты аннотаций данных.
Данный способ является самым простым для понимания, но недостаточно гибким и читабельным на мой субъективный взгляд. Суть его заключается в следующем - в классе сущности, представляющей модель предметной области с помощью атрибутов аннотаций данных из пространства System.ComponentModel.DataAnnotations настраивается маппинг к объекту СУБД:
[Table("SomeTable")]
public class SomeEntity
{
[Key]
public Guid Id { get; set; }
[Column("SomeName")]
[MaxLength(20)]
public string Name { get;set; }
}
Выглядит не очень красиво, согласитесь?
Способ второй - FluentAPI.
Данный способ понравится поклонникам паттерна проектирования Fluent interface (текучий интерфейс), который позволяет множественно вызывать методы объекта, тем самым повышая читабельность кода. Сконфигурировать сущность с помощью FluentAPI можно несколькими способами.
Способ второй с половиной - FluentAPI в переопределенном методе OnModelCreating класса, наследующего поведение DbContext (предположим, что у нас есть модель, описанная в первом способе):
public class SomeDbContext: DbContext
{
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<SomeEntity>().ToTable("SomeTable")
.HasKey(k => k.Id);
modelBuilder.Entity<SomeEntity>()
.Property(p => p.Name)
.HasColumnName("SomeName")
.HasMaxLength(20);
}
}
Таким образом, мы сконфигурировали одну сущность. А теперь представьте, какая "портянка" кода будет, если у нас хотя бы 10 таблиц в базе данных, в среднем имеющих 5 полей? Мда уж, такой поворот событий нас вряд ли устроит. Давайте перейдем к более лаконичному способу.
Способ второй, без половин - FluentAPI с помощью реализации интерфейса IEntityTypeConfiguration пространства Microsoft.EntityFrameworkCore. Данный способ позволяет "разложить по полочкам" конфигурации и отделить их от простейших POCO-классов, представляющих наши сущности. Для реализации данного способа необходимо создать класс, реализующий вышеуказанный интерфейс и его единственный метод Configure:
public class SomeEntityConfiguration : IEntityTypeConfiguration<SomeEntity>
{
public void Configure(EntityTypeBuilder<SomeEntity> builder)
{
builder.ToTable("SomeTable");
.HasKey(k => k.Id);
builder.Property(p => p.Name)
.HasColumnName("SomeName")
.HasMaxLength(20);
}
}
Затем в методе OnModelCreating нашего контекста мы применим нашу конфигурацию:
public class SomeDbContext: DbContext
{
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
}
}
Данный способ позволяет нам разделить ответственности наших классов - контекст не завязан на конкретных конфигурациях, а потому, изменение класса конфигурации никоим образом не повлечет за собой изменение других мест.
Способы конечно между собой различаются, и по-своему хороши для различных ситуаций.
Когда например у нас намечается несколько объектов, без каких либо сложных связей между собой - атрибуты аннотаций помогут нам быстро поднять решение для работы.
Если у нас несколько объектов и имеются между ними сложные связи, а также поля объектов имеют много специфичных свойств - FluentAPI в методе OnModelCreating поможет быстро и наглядно описать сущности и связи между ними.
Ну и наконец, если объектов много, конфигурировать сущности необходимо со связями, описывать специфичные свойства полей - реализация интерфейса IEntityTypeConfiguration - самое то! А если у объектов есть общие поля (например, Время создания, кто создал и т.п.) - можем определить базовый класс для сущностей, базовую конфигурацию. Тогда конфигурации остальных сущностей будут наследовать уже от базовой нашей конфигурации, переопределять метод Configure, вызывать в нем базовую реализацию и после определять специфичную для текущей сущности конфигурацию.
P.S. Буду рад, если в комментариях увижу ещё способы, не рассмотренные в данной статье.
Комментарии (22)
alexs0ff
04.02.2022 11:00+2Главное единообразие, а то посмотришь на проектах - все три стиля вперемешку, ничего кроме раздражения не вызывает.
granit1986
04.02.2022 11:16+1Предпочитаю FluentApi с интерфейсом. Как по мне более гибкий и понятный. Плюс с учётом, что у нас часто это доменные сущности, а не просто POCO, то вообще аттрибуты там нафиг не нужны
elmanaw
04.02.2022 12:26+1Безусловно. Всё на своих местах и не размазано по коду. В контексте DDD лучший выбор.
eugene_naryshkin Автор
04.02.2022 12:29+1Ну как правило, слои разделяют, и, соответственно - доменные сущности будут "жить" в слое для реализации доменной логики и ничего не знать о деталях реализации работы с БД. А вот наши классы-сущности в слое доступа к данным, так что и атрибуты подойдут, если не много объектов. Но в любом случае - спасибо за столь ёмкую обратную связь!
Это тема для следующей статьи, кстати)granit1986
04.02.2022 12:34+1Так доменная сущность в любом случае не будет знать о реализации работы с БД. Просто с помощью Fluent можно гибко настроить маппинг сразу из базы, без всяких промежуточных классов.
А вообще - это чисто вкусовщина, здесь нет какого-то однозначно правильного решения и смысла спорить тоже.
IosifLvovich
04.02.2022 12:26Выглядит не очень красиво, согласитесь?
Не соглашусь. По сравнению с fluent, гораздо локаничнее, да и нагляднее.
eugene_naryshkin Автор
04.02.2022 12:45Ну а если ещё добавить summary к каждому свойству, и свойств будет более 2-3 и настроить маппинг на поля объектов БД надо будет более тонко? Например:
[Table("Cars")] public class CarEntity { /// <summary>That represents unique identifier</summary> [Key] public Guid Id { get; set; } /// <summary>That represents make of vehicle and null input not allowed</summary> [Column("Make", TypeName = "varchar2")] [MaxLength(16)] [Required] public string Make { get; set; } /// <summary>That represents model of vehicle and null input is not allowed</summary> [Column("Model", TypeName = "varchar2")] [MaxLength(26)] [Required] public string Model { get; set; } /// <summary>That represents production year of vehicle</summary> [Column("Year")] public int ModelYear { get; set; } /// <summary>That represents VIN (vehicle identifier) of vehicle and null input is not allowed</summary> [Column("VIN", TypeName = "varchar2")] [MaxLength(14)] [Required] public string Vin { get; set; } /// <summary>That represents color of vehicle</summary> [Column("Color", TypeName = "varchar2")] [MaxLength(16)] public string Color { get; set; } }
Уже не так читабельно (как по мне), согласитесь?
IosifLvovich
04.02.2022 13:24Если такая беда с маппингом на поля таблицы, то что аттрибуты, что fluent - всё одинаково говнисто будет выглядеть.
FluentAPI даёт гораздо больше возможностей в плане настроек, но ограничения лично мне удобней искать в классе.
eugene_naryshkin Автор
04.02.2022 13:29Как сказали несколькими комментариями выше - это всё вкусовщина, особенно на текущих примерах. Разницу по функциональности способов мы рассмотрим в следующих статьях.
mayorovp
04.02.2022 14:27Тут несколько пустых строк добавить и уже лучше станет.
И нет, на FluentAPI вы более читаемо не сможете сделать, там будет такая же мешанина.
Phrynohyas
04.02.2022 13:12+1Выглядит не очень красиво, согласитесь?
Если это слой доступа к данным, то выглядит это прекрасно. В одном месте и объявление типов данных, и маппинги.
Возникает вопрос - а должны ли сущности EF протекать в слой бизнес-логики? Или EF POCO объекты должны маппиться в объекты домена, у которых будет и логика, и поведение, но не будет ненужных на этом уровне аттрибутов-аннотаций, связанных с EF.
eugene_naryshkin Автор
04.02.2022 13:14Мне больше по душе, чтобы слои были строго разделены между собой и являлись слабосвязанными. Для меня однозначно - POCO-объекты должны маппиться в объекты домена. Более глубоко данного вопроса мы коснёмся в следующей статье.
Спасибо за фидбек!
Korobei
04.02.2022 18:51+1С помощью данной технологии мы в принципе можем абстрагироваться от особенностей работы конкретной СУБД и в большинстве случаев, не подвергая модификации код нашего продукта перейти на любую другую СУБД.
Всегда было интересно насколько это востребованно? 0.1%?
Работал с разработчиком который делал code first, через db first. Т.е. у него была локальная версия базы из которой он генерировал код для fluent api. Создавал в SSMS таблицы, ключи, индексы, связи и потом из этого получался код для fluent api и легко можно было перегенерировать всю схему. Говорил что ему сильно проще получается.KislyFan
05.02.2022 12:55Суть не в частоте использования, а потенциальных проблемах которые могут появится при подобной потребности.
Говорил что ему сильно проще получается.
Как вы наверно знаете, в ASPMVC есть особые ограничение на тип передаваемых аргументов между экшнами. Мой бывший коллега (привет Сергей) чтобы передать коллекцию целочисленных элементов, делал конкатенацию в строку, передавал ее в целевой экшн, а потом делил по разделителю и переводил обратно в int. Правильно ли это было? Нет, для этого в MVC были свои инструменты.. с моей точки зрения это индусский говнокод, который накладывал ограничения еще и на клиентскую часть, но ему так было удобно и "сильно проще".
Korobei
05.02.2022 19:51индусский говнокод
Почему? Мне кажется когда ты контролируешь процесс, то качества кода зависит от тебя, а не от самого факта генерации.
debagger
По мне, так как раз первый способ более красивый и читабельный. Насчет "недостаточно гибкий" возможно.
Если первый пример я просмотрел краем глаза и вопросов не осталось, то остальные - пришлось вчитываться и вникать.
eugene_naryshkin Автор
Спасибо большое за фидбек!
KislyFan
Использую в работе только второй способ IEntityTypeConfiguration<> это решение оказалось максимально гибким. И вот почему.
Аннотационное описание, хорошо тем что она вещь "сама в себе". Но мы жестко привязываем описание к модели, и если хотим передать ту же модель в
клиентскуюдругую часть приложения, то передаем и сведения о полях базы их граничениях.Fluent описание в OnModelCreating привязано к конкретному dbContext
В IEntityTypeConfiguration<> мы достаточно свободны, чтобы теоретически вынести все компоненты: модель, контекст, и описание в разные библиотеки, и потом свободно их комбинировать, и повышает переиспользование кода.
Ну и из очевидного - мы легко можем заменить реализацию интерфейса, а в других случаях это вызовет небольшие сложности.