Почти каждый (ну или почти каждый) разработчик слышал про такую технологию программирования, как 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)


  1. debagger
    04.02.2022 10:24
    +1

    По мне, так как раз первый способ более красивый и читабельный. Насчет "недостаточно гибкий" возможно.

    Если первый пример я просмотрел краем глаза и вопросов не осталось, то остальные - пришлось вчитываться и вникать.


    1. eugene_naryshkin Автор
      04.02.2022 10:30

      Спасибо большое за фидбек!


    1. KislyFan
      05.02.2022 12:36

      Использую в работе только второй способ IEntityTypeConfiguration<> это решение оказалось максимально гибким. И вот почему.

      • Аннотационное описание, хорошо тем что она вещь "сама в себе". Но мы жестко привязываем описание к модели, и если хотим передать ту же модель в клиентскую другую часть приложения, то передаем и сведения о полях базы их граничениях.

      • Fluent описание в OnModelCreating привязано к конкретному dbContext

      • В IEntityTypeConfiguration<> мы достаточно свободны, чтобы теоретически вынести все компоненты: модель, контекст, и описание в разные библиотеки, и потом свободно их комбинировать, и повышает переиспользование кода.

      • Ну и из очевидного - мы легко можем заменить реализацию интерфейса, а в других случаях это вызовет небольшие сложности.


  1. alexs0ff
    04.02.2022 11:00
    +2

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


  1. granit1986
    04.02.2022 11:16
    +1

    Предпочитаю FluentApi с интерфейсом. Как по мне более гибкий и понятный. Плюс с учётом, что у нас часто это доменные сущности, а не просто POCO, то вообще аттрибуты там нафиг не нужны


    1. elmanaw
      04.02.2022 12:26
      +1

      Безусловно. Всё на своих местах и не размазано по коду. В контексте DDD лучший выбор.


    1. eugene_naryshkin Автор
      04.02.2022 12:29
      +1

      Ну как правило, слои разделяют, и, соответственно - доменные сущности будут "жить" в слое для реализации доменной логики и ничего не знать о деталях реализации работы с БД. А вот наши классы-сущности в слое доступа к данным, так что и атрибуты подойдут, если не много объектов. Но в любом случае - спасибо за столь ёмкую обратную связь!
      Это тема для следующей статьи, кстати)


      1. granit1986
        04.02.2022 12:34
        +1

        Так доменная сущность в любом случае не будет знать о реализации работы с БД. Просто с помощью Fluent можно гибко настроить маппинг сразу из базы, без всяких промежуточных классов.
        А вообще - это чисто вкусовщина, здесь нет какого-то однозначно правильного решения и смысла спорить тоже.


        1. eugene_naryshkin Автор
          04.02.2022 13:11

          Полностью согласен с Вами.


  1. IosifLvovich
    04.02.2022 12:26

    Выглядит не очень красиво, согласитесь?

    Не соглашусь. По сравнению с fluent, гораздо локаничнее, да и нагляднее.


    1. 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; }
          }

      Уже не так читабельно (как по мне), согласитесь?


      1. IosifLvovich
        04.02.2022 13:24

        Если такая беда с маппингом на поля таблицы, то что аттрибуты, что fluent - всё одинаково говнисто будет выглядеть.

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


        1. eugene_naryshkin Автор
          04.02.2022 13:29

          Как сказали несколькими комментариями выше - это всё вкусовщина, особенно на текущих примерах. Разницу по функциональности способов мы рассмотрим в следующих статьях.


      1. mayorovp
        04.02.2022 14:27

        Тут несколько пустых строк добавить и уже лучше станет.


        И нет, на FluentAPI вы более читаемо не сможете сделать, там будет такая же мешанина.


      1. AgentFire
        04.02.2022 18:58

        где sealed -_-


  1. Phrynohyas
    04.02.2022 13:12
    +1

    Выглядит не очень красиво, согласитесь?

    Если это слой доступа к данным, то выглядит это прекрасно. В одном месте и объявление типов данных, и маппинги.

    Возникает вопрос - а должны ли сущности EF протекать в слой бизнес-логики? Или EF POCO объекты должны маппиться в объекты домена, у которых будет и логика, и поведение, но не будет ненужных на этом уровне аттрибутов-аннотаций, связанных с EF.


    1. eugene_naryshkin Автор
      04.02.2022 13:14

      Мне больше по душе, чтобы слои были строго разделены между собой и являлись слабосвязанными. Для меня однозначно - POCO-объекты должны маппиться в объекты домена. Более глубоко данного вопроса мы коснёмся в следующей статье.
      Спасибо за фидбек!


  1. Korobei
    04.02.2022 18:51
    +1

    С помощью данной технологии мы в принципе можем абстрагироваться от особенностей работы конкретной СУБД и в большинстве случаев, не подвергая модификации код нашего продукта перейти на любую другую СУБД.
    Всегда было интересно насколько это востребованно? 0.1%?

    Работал с разработчиком который делал code first, через db first. Т.е. у него была локальная версия базы из которой он генерировал код для fluent api. Создавал в SSMS таблицы, ключи, индексы, связи и потом из этого получался код для fluent api и легко можно было перегенерировать всю схему. Говорил что ему сильно проще получается.


    1. KislyFan
      05.02.2022 12:55

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

      Говорил что ему сильно проще получается.

      Как вы наверно знаете, в ASPMVC есть особые ограничение на тип передаваемых аргументов между экшнами. Мой бывший коллега (привет Сергей) чтобы передать коллекцию целочисленных элементов, делал конкатенацию в строку, передавал ее в целевой экшн, а потом делил по разделителю и переводил обратно в int. Правильно ли это было? Нет, для этого в MVC были свои инструменты.. с моей точки зрения это индусский говнокод, который накладывал ограничения еще и на клиентскую часть, но ему так было удобно и "сильно проще".


      1. Korobei
        05.02.2022 19:51

        индусский говнокод
        Почему? Мне кажется когда ты контролируешь процесс, то качества кода зависит от тебя, а не от самого факта генерации.


        1. KislyFan
          05.02.2022 21:05

          Вы хоть немного за контекстом следите, я ни слова не говорил о генерации.


          1. Korobei
            05.02.2022 21:40

            Вы привели пример что «сильно проще» = «говонокод», это не всегда так.