Пытаясь более подробно разобраться с мануалами по ASP.NET MVC 4 столкнулся с такими понятиями как Fluent API, Code First, аннотации и многими другими. По Fluent API оказалось не так и много информации. Особенно на русском. Смотрим.
Подход Code First в Entity Framework позволяет использовать свои собственные доменные классы для представления модели, которую EF использует для построения запросов, отслеживания изменений и обновления. Code First использует паттерн программирования, который называется соглашение конфигураций. Это означает, что Code First считает, что Ваши классы следуют соглашением схемы, которую EF использует для концептуальной модели. В этом случае EF сможет использовать необходимые детали для выполнения своих функций. Однако, если Ваши классы не используют правильно соглашения, Вы можете добавить необходимую конфигурацию вручную, для того, что б EF смог правильно понимать их.
Используя подход Code Firs, Вы можете определить эти конфигурации двумя способами. Первый — использовать простые атрибуты, называемые аннотациями (DataAnnotations). Второй – использовать Fluent API, который позволяет описывать конфигурации императивно в коде.
В данной статье внимание уделено настройке с помощью Fluent API. Конвенции Code Firs очень удобно использовать для описания отношений между классами, основанных на свойствах, указывающих на потомков или отдельные классы. Если в Ваших классах нет внешних ключей, Code Firs может сам их создать. Но бывают случаи, когда описание класса не предоставляет достаточно информации относительно отношений, что б Code Firs смог правильно все понять и правильно добавить «не хватающие» части.
Рассмотрим модель
Начнем с двух простых классов «Blog» и «Post», где Blog имеет отношение один-ко-многим к Post.
public class Blog
{
public int Id { get; set; }
public string Title { get; set; }
public string BloggerName { get; set; }
public virtual ICollection<Post> Posts { get; set; }
}
public class Post
{
public int Id { get; set; }
public string Title { get; set; }
public DateTime DateCreated { get; set; }
public string Content { get; set; }
public int BlogId { get; set; }
public Blog Blog { get; set; }
}
Понимание конвенции отношения один-ко-многим
Одним из распространенных способов определить связь один-ко-многим в классе является создание дочерней коллекции в одном классе и внешнего ключа вместе со свойством навигации (navigation property) в дочернем классе. В выше приведенном примере, в Blog имеется свойство Posts, которое и является коллекцией ICollection типа Post. А Post в свою очередь имеет внешний ключ, BlogID, так же известный как свойство навигации (navigation property) Blog, который указывает обратно на своего предка Blog.
Этих данных достаточно для того, что б Code First на основе конвенции смог создать следующие таблицы в БД:
Обратите внимание, что Code First сделал поле BlogId внешним ключом (определяется ограничение между первичным ключом/внешним ключом Posts.BlogId и Blogs.Id) и оно является non-nullable. Эти выводы Code First сделал основываясь на конвенции, описанной в классе.
Использование HasRequired, когда не указано свойство внешнего ключа
Что произойдет, если Вы не зададите свойство BlogId в классе Post, но зададите свойство навигации (navigation property) Blog. Code First все равно создаст необходимую связь, так как знает, что свойство Blog указывает обратно на сущность Blog. Он создаст внешний ключ Posts.Blog_Id, как указано на рисунке 2 и свяжет его с Blog.Id.
Однако есть одна важная деталь. Blog_Id является nullable. Тогда появляется возможность добавлять Posts, которые не связаны с Blog. Так Code First изучив конвенцию класса понял его, однако это не совсем то, чего добивался разработчик. Применим Fluent API для исправления той неточности.
Конфигурация Fluent API используется в Code First при построении модели из класса. Вы можете внедрить конфигурации путем переопределения метода OnModelCreating класса DbContext, как показано далее:
public class BlogContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
//configure model with fluent API
}
DbModelBuilder дает вам возможность перехватить настройки. Мы сообщаем строителю модели (model builder), что мы хотим изменить одну из сущностей. Для этого используем генерики (generics), что изменять будем сущность Post. После получения к ней доступа появляется возможность использовать метод HasRequired, который является частью отношений, для указания, что свойство навигации является обязательным — в данном случае свойство Blog.
modelBuilder.Entity<Post>().HasRequired(p => p.Blog);
В итоге получится двойной эффект. Первый – это поле Blog_Id станет снова not null. А так же EF будет выполнять валидацию по требованию или перед сохранением в БД, что б убедиться что выполняются все указанные условия.
Настройка нестандартных имен внешних ключей
Даже если Вы указали внешний ключ, его име не всегда совпадает с конвенциями Code First. По конвенции имя ключа должно совпадать с именем класса или «имякласа_Id» или «имякласаId». Вот поэтому Code First и смог нормально работать с оригинальным свойством класса, BlogId.
Но что произойдет, если имя будет не по конвенции? Например «FK» + имя_родительского_класса + «Id».
public int FKBlogId { get; set; }
Code First не знает, что FKBlogId должен выступать внешним ключом. Он создаст стандартную колонку для FKBlogId и еще Blog_Id, который и станет внешним ключом, так как он необходим для связи свойства Blog.
Также, работая дальше с Blog и Post, FKBlogId никогда не будет восприниматься программой как внешний ключа, указывающий обратно на blog.
Используя Fluent API можно решить и эту проблему — воспринимать FKBlogId как внешний ключ в отношениях к Blog.
При настройке Вы должны указать оба конца отношений – свойство в Blog, указывающее на множественное отношение (Posts) и свойство Post, указывающее обратно на родителя (Blog).
Для начала необходимо добавить метод WithMany, который позволит указать, какое именно свойство Blog содержит множественное отношение.
modelBuilder.Entity<Post>().HasRequired(p => p.Blog)
.WithMany(b => b.Posts)
Потом можно добавить метод HasForeignKey, указывающий какое именно свойство Post является внешним ключом и указывает обратно на blog. Полный код:
modelBuilder.Entity<Post>().HasRequired(p => p.Blog)
.WithMany(b => b.Posts)
.HasForeignKey(p => p.FKBlogId);
Теперь Code First может собрать все необходимые данные для правильного построения схемы БД и связей, которую и предполагает разработчик.
Определение схемы связующих таблиц и связи многие-ко-многим
При необходимости Вы можете легко определять связи многие-ко-многим с помощью свойств, указывающих друг на друга. Например, если Вы добавите класс Tag в модель для отслеживания Tags в Posts, Вам понадобится связь многие-ко-многим, между ними.
Новый класс Tag:
public class Tag
{
public int TagId{ get; set; }
public string Name { get; set; }
public ICollection<Post> Posts { get; set; }
}
Новое свойство, которое нужно добавить в класс Post:
public ICollection<Tag> Tags { get; set; }
Code first предполагает, что имя связующей таблицы будет состоять из комбинации имен двух классов, а в таблице будут содержаться внешние ключи, имена которых состоят из комбинации имени класса и имени ключа. В данном случае мы работаем с Post.Id и Tag.TagId. Если Вы позволите Code first самому создать связующую таблицу, то это будет выглядит следующим образом:
Также, если Вы позволяете Code first создавать БД самому, то никаких проблем как правило не возникает. А вот если идет маппинг на уже существующую БД, то могут возникнуть проблемы с названиями таблиц, столбцов. Используйте Fluent API для указания названий таблиц и столбцов.
Рассмотрим пример, когда нужно указать три имени. Имя таблицы должно быть PostJoinTag, а имена столбцов TagId и PostId.
Начнем с метода маппинга Entity. Что описывать первым Post илиTag не имеет значения. Выберем Post. Обозначим оба конца взаимосвязей. И подобно тому, как указывали связь один-ко-многим в предыдущем примере, сделаем тоже самое, используя методы HasMany и WithMany. Укажем, что сущность Post имеет множественные связи со свойством Tags, а сущность Tag имеет множественный связи со свойством Posts:
modelBuilder.Entity<Post>()
.HasMany(p => p.Tags)
.WithMany(t => t.Posts)
.Map(mc =>
{
mc.ToTable("PostJoinTag");
mc.MapLeftKey("PostId");
mc.MapRightKey("TagId");
});
Нужно обратить внимание при определении левого и правого ключей маппинга (MapLeftKey, MapRightKey). Левый – этоключ первого класса, на который Вы указали, тоесть Post, а правый – второго класса. Если опменять их местами, то данные будут сохраняться не правильно, что приведет к известным негативным последствиям.
Выводы
Вы ознакомились с некоторыми возможностями описания отношений используя Code First fluent API. Есть и другие маппинги, которые можно использовать отдельно или в комбинации. Некоторые из них могут показаться избыточными или запутанными, например IsRequired и HasRequired или WithMany и HasMany. Однако Вы должны были увидеть задачи, за которые отвечает каждый из них, а так же преимущества их использования.
Более подробно про Entity Framework можно почитать на MSDN или на блоге команды Entity Framework.
Автор: struggleendlessly