Завершим изучение Entity Framework Fluent API рассмотрением возможностей настройки связей.
Настройка связей с использованием Fluent API
Fluent API позволяет указать связи, которые должны быть установлены между классами Модели и, как следствие, между таблицами, создаваемыми в базе данных. Это может потребоваться в том случае, если соглашения по умолчанию не подходят или могут быть использованы.
Установка связи типа One-to–Zero-or-One (одна-к-нулю-или-одной)
В этом случае каждой записи из одной таблицы соответствует одна запись из другой. Но значение null, то есть отсутствие связи, также допустимо.
Для примера возьмем ситуацию, когда каждый судья (класс Arbitter) может или быть назначен на одну игру (класс Game) или вообще не участвовать в соревнованиях. Но при этом на любой игре должен быть судья. В этом случае связь будет задана следующим образом:
modelBuilder.Entity<Game>()
.HasRequired(g => g.Arbitter)
.WithOptional(a => a.Game);
Установка связи типа One-to–One (одна-к-одной)
В прошлом варианте связь была не обязательной. Но что если необходимо чтобы записи существовали с обоих сторон отношения? Такой вариант также легко описать с помощью Fluent API.
Однако здесь есть один интересный момент. В ситуации, когда обе стороны связи обязательны, Entity Framework не может определить, какая из них является главной (principal), а какая зависимой (dependent). Для уточнения существуют методы WithRequiredPrincipal() и WithRequiredDependent(), соответственно. Они вызываются после HasRequired(), который указывает требуемое свойство в выбранном классе:
modelBuilder.Entity<Arbitter>()
.HasRequired(a => a.Game)
.WithRequiredPrincipal(g => g.Arbitter);
Кроме того, аналогично можно указать и вариант отношений, когда обе стороны его опциональны. Но в этом случае используются WithOptionalPrincipal() и WithOptionalDependent() после вызова HasOptional():
modelBuilder.Entity<Arbitter>()
.HasOptional(a => a.Game)
.WithOptionalPrincipal(g => g.Arbitter);
Установка связи типа Many-to–Many (многие-ко-многим)
В создаваемом проекте любой из книг может быть присвоено любое число ключевых слов. НО и само ключевое слово может быть назначено любому числу книг. Такой тип отношений описывается с помощью методов HasMany() и WithMany():
modelBuilder.Entity<BookDetails>()
.HasMany(b => b.Tags)
.WithMany(t => t.Books);
Однонаправленные отношения одна-к-одному
Если с одной из сторон отсутствует навигационное свойство, то такое отношение называют однонаправленным. Соглашения по умолчанию в Code First считают их типом "одна-ко-многим". При необходимости это можно переопределить для конкретной связи:
modelBuilder.Entity<Arbitter>()
.HasRequired(a => a.Game)
.WithRequiredPrincipal();
Настройка каскадного удаления
Если внешний ключ не может содержать значение null, то для таких колонок Code First указывает флаг каскадного удаления записей. Это означает, что если в главной таблице будет удалена строка с определенным Id, то из зависимых таблиц будут удалены все записи, у которых есть внешний ключ, указывающий на неё. В некоторых ситуациях это не только не желательно, но и может привести к ошибке или зацикливанию удалений.
Такое поведение можно переопределить двумя способами. Первый подразумевает полное отключение данного соглашения по умолчанию с помощью вызовов:
modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();
modelBuilder.Conventions.Remove<ManyToManyCascadeDeleteConvention>();
Кроме того, можно отменить это поведение для конкретной связи, используя WillCascadeOnDelete():
modelBuilder.Entity<BookDetails>()
.HasRequired(b => b.Language)
.WithMany(l => l.Books)
.WillCascadeOnDelete(false);
В данном случае, при удалении записи из таблицы Language автоматического удаления всех книг, написанных на нем, не произойдёт.
Указание внешнего ключа
Иногда имя свойства, предназначенного для содержания внешнего ключа, не подходит под соглашения по умолчанию. В этом случае его можно отметить самостоятельно с помощью метода HasForeignKey():
modelBuilder.Entity<Game>()
.HasRequired(g => g.GameTypes)
.WithMany(g => g.Games)
.HasForeignKey(g => g.GTId);
Создаем в базе данных записи по умолчанию
Перед тем как перейти к изучению контроля данных в ASP.NET MVC 3 давайте сделаем небольшое изменение в создаваемом веб-приложении, которое упростит работу в дальнейшем. Как было уже отмечено, при изменении классов Модели необходимо пересоздавать базу данных, что приводит к потере уже существующих в ней данных. Это не страшно, так как на этапе разработки важных данных в таблицах нет. Но вот сама необходимость каждый раз создавать записи может сильно изматывать.
Поэтому создадим метод, который будет наполнять базу значениями, но только в случае если она была только что создана. Такую возможность предоставляет Entity Framework.
Для этого в папке DbContext создадим класс CatalogInitializer, который должен быть унаследован от одного из следующих классов:
- DropCreateDatabaseIfModelChanges<TContext> – обеспечивает автоматическое удаление базы данных в случае изменения любого класса Модели.
- DropCreateDatabaseAlways<TContext> – всегда перед запуском удаляет базу данных и воссоздается её с определенными пользователем данными.
- CreateDatabaseIfNotExists<TContext> – создает базу данных и заполняет её значениями только если она отсутствует.
В качестве параметра шаблона TContext необходимо указать тип контекста базы данных.
Выберем DropCreateDatabaseIfModelChanges<T>. Теперь, в созданном CatalogInitializer, необходимо переопределить метод Seed(). Он и будет отвечать за добавление новых записей в пустые таблицы. Давайте посмотрим на исходный код:
namespace BookCatalog.Models.DbContext
{
using System;
using System.Data.Entity;
public class CatalogInitializer : DropCreateDatabaseIfModelChanges<CatalogContext>
{
protected override void Seed(CatalogContext context)
{
base.Seed(context);
var englishLang = new Language() { Name = "English" };
context.Languages.Add(englishLang);
var russianLang = new Language() { Name = "Russian" };
context.Languages.Add(russianLang);
var microsoftPress = new Publisher() {
Title = "Microsoft Press",
Homepage = @"http://www.microsoft.com/mspress/"
};
context.Publishers.Add(microsoftPress);
var tagVS2010 = new Tag() { Text = "VS2010" };
context.Tags.Add(tagVS2010);
var tagASPNET = new Tag() { Text = "ASP.NET" };
context.Tags.Add(tagASPNET);
var tagMVC = new Tag() { Text = "MVC" };
context.Tags.Add(tagMVC);
var tagCSharp = new Tag() { Text = "C#" };
context.Tags.Add(tagCSharp);
var tagWP7 = new Tag() { Text = "WP7" };
context.Tags.Add(tagWP7);
context.Books.Add(new BookDetails() {
Title = "ASP.NET Web Pages with Razor Syntax",
Author = "P. Pelland, P. Pare, Ken Haines",
Language = englishLang,
Publisher = microsoftPress,
PublishedAt = new DateTime(2010, 09, 10),
Url = @"http://blogs.msdn.com/b/microsoft_press/archive/2010/09/13/free-ebook-moving-to-microsoft-visual-studio-2010.aspx",
Description = "For developers moving from Visual Studio 2003,2005,2008 to Visual Studio 2010.",
Rating = 4,
Tags = new Tag[1] { tagVS2010 },
IsFree = true,
IsVisible = true
});
context.Books.Add(new BookDetails() {
Title = "Programming Windows Phone 7",
Author = "Charles Petzold",
Language = englishLang,
Publisher = microsoftPress,
PublishedAt = new DateTime(2010, 10, 28),
Url = @"http://www.charlespetzold.com/phone/",
Description = "This book is a gift from the Windows Phone 7 team at Microsoft to the programming community.",
Rating = 5,
Tags = new Tag[3] { tagVS2010, tagCSharp, tagWP7 },
IsFree = true,
IsVisible = true
});
}
}
}
В данном методе все достаточно просто: создаются объекты заданных типов и наполняются данными. После чего, с помощью контекста базы данных, происходит их передача в соответствующие таблицы.
Остается только указать ядру Entity Framework на необходимость использование созданного инициализатора. Для этого в методе Application_Start() вызовем Database.SetInitializer():
protected void Application_Start()
{
System.Data.Entity.Database.SetInitializer(
new BookCatalog.Models.DbContext.CatalogInitializer());
AreaRegistration.RegisterAllAreas();
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
}
Если в проекте уже есть база данных, то теперь она будет автоматически удалена в случае изменения любого класса Модели. После чего в нее будут добавлены данные.
Теперь можно запустить веб-приложение. В открывшейся таблице будут видно две записи о книгах.
На этом завершим краткий обзор Entity Framework и вернемся к рассмотрению ASP.NET MVC.
Исходный код проекта (C#, Visual Studio 2010):
mvc3-in-depth-ef-05.zip