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