Продолжим изучение возможностей Entity Framework Code First для указания соотношений классов Модели и создаваемых таблиц. На очереди второй способ – использование Fluent API.
Потоковый интерфейс (Fluent API)
Потоковый интерфейс это способ реализации программного интерфейса библиотеки, который обеспечивает создание более удобного для чтения исходного кода программы. При этом можно выделить следующие признаки:
- Методы возвращают объекты, что позволяет вызывать их цепочкой в рамках одного оператора.
- Имена методов подбираются так, чтобы обеспечить максимально понятный код.
Чтобы лучше понять суть Fluent API, давайте посмотрим как может выглядеть запись оператора с использованием такого подхода:
modelBuilder.Entity<Gamer>()
.HasRequired(e => e.ReserveTeam)
.WithMany(t => t.Reserve)
.HasForeignKey(e => e.ReserveId)
.WillCascadeOnDelete(false);
Правильно подобранные названия методов обеспечивают лёгкое понимание цели исходного кода. Например, в приведенном выше коде можно легко прочитать, что:
в сущности, описанной классом Gamer
.есть обязательное навигационное поле ReserveTeam
.связанное с коллекцией Reserve в другой сущности (таблице)
.при помощи внешнего ключа ReserveId
.без каскадного удаления записей
Кроме того, такой подход очень удобен при использовании совместно с контекстными подсказками, такими как IntelliSense в Visual Syudio. В этом случае они наглядно указывают возможные дальнейшие действия в зависимости от текущего контекста.
В качестве минуса потокового интерфейса необходимо отметить невозможность указания точки останова для отладчика между вызовами методов в цепочке.
В этой серии статей под Fluent API будем понимать, как правило, реализацию интерфейса Entity Framework Code First для настройки параметров генерации таблиц.
Использование Entity Framework Fluent API
Как уже было отмечено, при использовании Entity Framework Fluent API нет необходимости модифицировать Модель. Параметры создания таблиц определяются программно. Необходимый для этого код размещается внутри метода OnModelCreating(DbModelBuilder modelBuilder), переопределенного в соответствующем классе контекста базы данных.
Построение выражений для каждого нового правила начинается с экземпляра DbModelBuilder, полученного в качестве параметра. Его метод Entity<T>() возвращает объект типа EntityTypeConfiguration. Он, в свою очередь, используется для настройки параметров генерации таблицы для сущности указанного типа T. Вот пример кода на основе класса контекста CatalogContext из создаваемого веб-приложения:
namespace BookCatalog.Models.DbContext
{
using System.Data.Entity;
using BookCatalog.Models;
public class CatalogContext : DbContext
{
// You can add custom code to this file. Changes will not be overwritten.
//
// If you want Entity Framework to drop and regenerate your database
// automatically whenever you change your model schema, add the following
// code to the Application_Start method in your Global.asax file.
// Note: this will destroy and re-create your database with every model change.
//
// System.Data.Entity.Database.SetInitializer(new System.Data.Entity.DropCreateDatabaseIfModelChanges<BookCatalog.Models.CatalogContext>());
public DbSet<Tag> Tags { get; set; }
public DbSet<Publisher> Publishers { get; set; }
public DbSet<Language> Languages { get; set; }
public DbSet<BookDetails> Books { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// место для вызовов Entity Framework Fluent API
}
}
}
В приведенном примере отмечено место добавления вызовов Fluent API. Их, в зависимости от цели, можно разделить на три группы:
- Настройка свойств;
- Настройка типов;
- Настройка взаимосвязей между сущностями.
Давайте рассмотрим каждую группу в отдельности подробнее и с небольшими примерами.
Настройка свойств с использованием Fluent API
Для настройки параметров соотношения отдельных свойств и колонок таблицы в базе данных класс EntityTypeConfiguration предоставляет метод Property(). Он возвращает объект, являющийся одним из наследников класса PrimitivePropertyConfiguration, которые и содержит необходимые методы. Посмотрим на некоторые из них на примерах.
Указание первичного ключа
И начнём сразу с исключения. Дело в том, что первичный ключ задается вызовом HasKey() класса EntityTypeConfiguration. Для примера, укажем в этой роли свойство Id сущности BookDetails:
modelBuilder.Entity<BookDetails>().HasKey(b => b.Id);
В качестве параметра передается лямбда-выражение, которое возвращает значение свойства, выбранного для первичного ключа. На основе полученного параметра, ядро Entity Framework определит необходимое свойство и создаст первичный ключ на его основе. Стоит отметить, что именно так во многих методах Fluent API указываются свойства для настройки.
Если необходимо задать составной первичный ключ по двум свойствам, то код будет выглядеть так:
modelBuilder.Entity<Publisher>().HasKey(p => new { p.Title, p.Homepage } );
Атрибут-аналог: [Key].
Ограничение длины строки
Ограничим длину свойства Title класса BookDetails в 128 символов:
modelBuilder.Entity<BookDetails>().Property(p => p.Title)
.HasMaxLength(128);
Атрибут-аналог: [MaxLenght].
Обязательное к заполнению свойство
Также для указанного в прошлом примере свойства установим необходимость всегда содержать значение
modelBuilder.Entity<BookDetails>().Property(p => p.Title)
.IsRequired();
Атрибут-аналог: [Require].
Установка способа генерации значения
Предположим, что для свойства Id класса BookDetails значения будет указывать бизнес-логика самостоятельно. Тогда необходимо отключить их генерирование на стороне базы данных:
modelBuilder.Entity<BookDetails>().Property(p => p.Id)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
Атрибут-аналог: [DatabaseGenerated].
Исключение свойства при создании таблицы
Предположим, что в классе BookDetails есть свойство TotalTagsLenght. Его не требуется включать в базу данных, так как это значение вычисляется бизнес-логикой веб-приложения. Указать это можно следующим способом:
modelBuilder.Entity<BookDetails>().Ignore(t => t.TotalTagsLenght);
Атрибут-аналог: [NotMapped].
Указание имени для колонки в базе данных
Для свойства PublishedAt класса BookDetails установим текст "PublishDate" в качестве имени колонки:
modelBuilder.Entity<BookDetails>().Property(t => t.PublishedAt)
.HasColumnName("PublishDate");
Атрибут-аналог: [Column].
Указание использовать или не использовать уникод для текстового поля
По умолчанию, на основе всех свойств типа string будут созданы колонки типа nvarchar, т.е. с поддержкой уникода. При необходимости можно изменить это поведение:
modelBuilder.Entity<BookDetails>().Property(t => t.Author)
.IsUnicode(false);
Атрибут-аналог: [Column] c явным указанием типа колонки.
Указание порядка колонки в таблице
Указать порядковый номер колонки в таблице можно используя метод HasColumnOrder():
modelBuilder.Entity<BookDetails>().Property(t => t.Author)
.HasColumnOrder(2);
Атрибут-аналог: [Column] c явным указанием порядкового номера.
Указание использовать определенный тип для колонки в базе данных
Fluent API позволяет указать тип, который должна содержать связанная со свойством колонка:
modelBuilder.Entity<BookDetails>().Property(t => t.Author)
.HasColumnType("varchar");
Атрибут-аналог: [Column] c явным указанием типа колонки.
Конфигурация вложенных типов
Рассмотрим ситуацию, когда класс Customer включает в себя экземпляр класса Address. В последнем есть свойство Street, которому необходимо установить максимальную длину строки в 128 символов. Такая задача легко решается с помощью Fluent API:
modelBuilder.Entity<Customer>().Property(t => t.Address.Street)
.HasMaxLength(128);
Стоит отметить, что основное преимущество Fluent API проявляется при необходимости указать несколько параметров соотношения для одного свойства. И несмотря на то, что в примерах выше настраивается по одному свойству, следующая запись корректна:
modelBuilder.Entity<BookDetails>().Property(t => t.Title)
.IsRequired()
.HasMaxLength(128)
.HasColumnName("BookTitle")
.HasColumnOrder(2);
Оставим небольшое число методов для самостоятельного изучения и перейдем к настройке параметров отображения для типов.
Настройка типов с использованием Fluent API
Для типов Fluent API позволят настроить имя таблицы, схему отображения типов и исключения. Давайте сразу перейдем к конкретным примерам.
Указание имени таблицы
Допустим необходимо присвоить имя "Catalog" для таблицы, в которой будут располагаться данные из экземпляров класса BookDetails. Код этой настройки будет следующий:
modelBuilder.Entity<BookDetails>().ToTable("Catalog");
Атрибут-аналог: [Table].
Исключение типа при создании таблицы
Entity Framework Code First автоматически определяет используемые типы и размещает их в таблицах создаваемой базы данных. В некоторых случаях необходимо исключить определенные классы из этого процесса. Например, если данные для них вычисляются. Код, решающий эту задачу, выглядит следующим образом:
modelBuilder.Ignore<Tag>();
В данном случае экземпляры класса Tag не будут сохраняться в базе данных. Равно как и не будет создана соответствующая таблица.
Указание сложных типов
Entity Framework Code First также автоматически распознает сложные типы. Однако в некоторых случаях могут быть ошибки. Например, если в таком классе есть поле Id для каких-либо целей. Тогда потребуется указать его с помощью FluentAPI:
modelBuilder.ComplexType<SomeComplexType>();
Настройка отображения "Таблица для иерархии (Table per hierarchy)"
При использовании данного типа отображения наследования, для всех классов, унаследованных от общего базового, создается единая таблица. В Code First это вариант по умолчанию. При этом в таблицу добавляется колонка под названием Discriminator, в которой указывается имя конкретного типа для каждой строки. Переопределить такое поведение можно следующим образом:
modelBuilder.Entity<BaseType>()
.Map<BaseType>(m => m.Requires("Type").HasValue("BaseType"))
.Map<ChildType>(m => m.Requires("Type").HasValue("ChildType"));
Настройка отображения "Таблица для каждого конкретного типа (Table per concrete type)"
Тип отображения наследования "таблица для каждого конкретного типа" подразумевает, что создаются индивидуальные таблицы для каждого конкретного типа. Определить такое поведение при помощи Fluent API можно следующим образом:
modelBuilder.Entity<BaseType>()
.Property(p => p.Id)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
modelBuilder.Entity<ChildType>().Map(m =>
{
m.MapInheritedProperties();
m.ToTable("ChildType");
});
Настройка отображения "Таблица для каждого типа (Table per type)"
Этот вариант отображения наследования предусматривает, что создается одна таблица на основе базового типа для общих свойств. Все остальные размещаются в вспомогательных таблицах. Задается такая схема следующим образом:
modelBuilder.Entity<BaseType>().ToTable("BaseType");
modelBuilder.Entity<ChildType>().ToTable("ChildType");
Разделение одного типа по нескольким таблицам
В некоторых ситуация необходимо разделить существующий класс на несколько таблиц с целью оптимизации производительности.
Предположим, что свойства Id, Title, Author и Url класса BookDetails запрашиваются наиболее часто. Поэтому расположим их в отдельной таблице:
modelBuilder.Entity<BookDetails>()
.Map(m => {
m.Properties(b => new { b.Id, b.Title, b.Author, b.Url });
m.ToTable("Catalog");
})
.Map(m => {
m.Properties(b => new {
b.Description,
b.IsFree,
b.IsVisible,
b.LanguageId,
b.PublishedAt,
b.PublisherId,
b.Rating });
m.ToTable("BookDetails");
});
Объединение нескольких классов в одну таблицу
Данный сценарий может пригодиться для оптимизации в случае связи "одна строка к одной". Например, пусть каждый судья (класс Arbitter) может быть назначен только на одну игру (класс Game). Тогда код Fluent API будет выглядеть следующим образом:
modelBuilder.Entity<Game>()
.HasKey(g => g.ArbitterID);
modelBuilder.Entity<Arbitter>()
.HasRequired(a => a.Game)
.WithRequiredPrincipal(t => t.Arbitter);
modelBuilder.Entity<Arbitter>().ToTable("Games");
modelBuilder.Entity<Game>().ToTable("Games");
В следующей части тема настройки Модели для Code First будет продолжена. В ней рассмотрим возможности Fluent API для указания связей между таблицами.