Отношение один-к-одному (one-to-one) между таблицами
126Работа с базами данных в .NET Framework --- Entity Framework 6 --- Отношение один-к-одному (one-to-one) между таблицами
Отношение один-к-одному (one-to-one) между двумя таблицами базы данных указывает на то, что для одной строки главной таблицы, обязательно определяется одна связанная строка в зависимой таблице. Это отношение имеет еще один подвид, который называется ноль-или-один-к-одному (zero-or-one-to-one). При этом отношении, в зависимой таблице необязательно указывать связанную строку, т.е. одной строке в главной таблице, может соответствовать ноль или одна строка в зависимой таблице.
При определении отношения один-к-одному в модели данных Entity Framework, вы должны использовать только двустороннюю связь между классами, т.е. нужно указывать навигационное свойство в каждой таблице (если вы используете Fluent API для настройки, следовать этому правилу необязательно). Эти навигационные свойства должны иметь тип ссылки (а не коллекции), чтобы Code-First понял, что используется связь один-к-одному, при этом используемой по умолчанию связью, будет ноль-или-один-к-одному.
Давайте добавим в нашу модель класс профиля покупателя Profile, который свяжем с таблицей Customer:
public class Customer
{
public int CustomerId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
public int Age { get; set; }
public byte[] Photo { get; set; }
public List<Order> Orders { get; set; }
public Profile Profile { get; set; }
}
public class Profile
{
[Key]
public int CustomerId { get; set; }
public DateTime RegistrationDate { get; set; }
public DateTime LastLoginDate { get; set; }
public Customer CustomerOf { get; set; }
}
public class Order
{
// ... эта таблица не изменилась
}
В этом примере изначально допущена ошибка. Code-First не сможет определить, какой класс является зависимым в этой ситуации (т.е. для какого класса нужно создать внешний ключ). Если вы запустите приложение с этой моделью, то EF сгенерирует исключение InvalidOperationException, указав, что не может определить основной класс в связке Profile-Customer.
Простым решением этой проблемы является явное указание внешнего ключа в зависимой таблице. При создании отношения один-к-одному Entity Framework требует, чтобы внешний ключ в зависимой таблице являлся и первичным ключом для этой таблицы. В нашем случае в классе Profile определен первичный ключ CustomerId, для которого мы должны указать, что он является и внешним ключом. Как описывалось ранее, это можно сделать указав свойству CustomerId атрибут ForeignKey, с переданным ему именем навигационного свойства:
public class Profile
{
[Key]
[ForeignKey("CustomerOf")]
public int CustomerId { get; set; }
public DateTime RegistrationDate { get; set; }
public DateTime LastLoginDate { get; set; }
public Customer CustomerOf { get; set; }
}
Если вы теперь запустите приложение, то Entity Framework корректно создаст таблицу Profiles. Как показано на рисунке ниже, ключ CustomerId является как первичным, так и внешним ключом для этой таблицы:

Ранее, при обсуждении использования внешних ключей мы говорили, что атрибут ForeignKey можно применить не только к свойству ключа в модели, но и к навигационному свойству. Для нашей модели такой подход является ошибочным, если вы примените атрибут [ForeignKey(“CustomerId”)] к свойству CustomerOf в классе Profile, Code-First не поймет, в какой таблице нужно будет создать внешний ключ, т.к. и таблица Customer и Profile содержат поле CustomerId. Так что данный способ определения внешних ключей не подходит для нашего сценария.
На следующем рисунке показана диаграмма, на которой можно видеть отношение ноль-или-один-к-одному между таблицами Customers и Profiles:

В контексте нашего примера, отношение ноль-или-один-к-одному означает, что для покупателя не обязательно указывать данные профиля, но при этом для профиля всегда есть один связанный с ним покупатель. В примере ниже показано, как настроить отношение ноль-или-один-к-одному с помощью Fluent API без использования атрибута ForeignKey:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Profile>()
.HasRequired(p => p.CustomerOf)
.WithOptional(c => c.Profile);
}
Обратите внимание, что здесь не нужно использовать метод HasForeignKey() для явного указания внешнего ключа. Этого кода на самом деле достаточно для Code-First, чтобы показать, что класс Profile является зависимым, а свойство Profile.CustomerId должно являться внешним ключом. Это связано с тем, что при определении отношения one-to-one (или zero-or-one-to-one) первичный ключ в зависимой таблице должен автоматически быть внешним ключом.
Теперь давайте смоделируем явное отношение один-к-одному между нашими таблицами. В контексте нашего примера это означает, что для покупателя всегда должна определяться информация профиля. Одному покупателю соответствует один профиль. В аннотациях данных это можно сделать с помощью атрибута Required, который применяется к навигационному свойству главной таблицы, как показано в примере ниже:
public class Customer
{
// ...
[Required]
public Profile Profile { get; set; }
}
В результате, если мы попытаемся вставить данные нового заказчика, не указав для него профиль, то Entity Framework сгенерирует ошибку. Следует отметить, что при реализации отношения один-к-одному структура базы данных не меняется, изменения касаются только модели, которые уведомляют EF об обоюдной зависимости таблиц Customer и Profile.
Отношение один-к-одному можно создать, используя также Fluent API. Логично было бы предположить, что для этого используется вызов метода HasRequired с последующим вызовом WithRequired. Однако, Fluent API предлагает вместо вызова WithRequired использовать два следующих метода: WithRequiredDependent или WithRequiredPrincipal. Выбор нужного метода зависит от того, к какой таблице он применяется – зависимой (WithRequiredPrincipal укажет ссылку на основную таблицу) или основной (WithRequiredDependent укажет ссылку на зависимую таблицу). Чтобы в этом разобраться, достаточно взглянуть на простой пример:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Profile>()
// Этот метод вернет ссылку на объект конфигурации Customer
.HasRequired(p => p.CustomerOf)
// Поэтому мы применяем метод WithRequiredDependent,
// т.к. Customer является основной таблицей
.WithRequiredDependent(c => c.Profile);
// Следующая настройка аналогична предыдущей
modelBuilder.Entity<Customer>()
.HasRequired(c => c.Profile)
.WithRequiredPrincipal(p => p.CustomerOf);
}
Здесь важно отметить, что если вы перепутаете в этом примере вызовы этих методов, например:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Profile>()
.HasRequired(p => p.CustomerOf)
.WithRequiredPrincipal(c => c.Profile);
}
Тогда Entity Framework создаст таблицу Customer как зависимую от Profile. Это выражается в том, что внешний ключ будет установлен в поле Customer.CustomerId, а не в Profile.CustomerId, как это делалось раньше.
В примерах выше мы использовали перегруженные методы WithRequiredDependent() и WithRequiredPrincipal() передавая в параметре ссылку на навигационное свойство. Однако эти методы можно указывать без параметров. Делать это следует только в том случае, если в классах таблиц, к которым они применяются, отсутствуют соответствующие навигационные свойства. Т.е. благодаря вызову этих методов без параметров, мы можем моделировать связь один-к-одному используя одностороннее отношение между классами (когда навигационное свойство указывается только в одном из классов).
Существует еще одна разновидность отношения один-к-одному, которую мы еще не рассмотрели, это отношение ноль-или-один-к-ноль-или-одному (zero-or-one-to-zero-or-one). В контексте нашего примера это отношение говорит о том, что для любого покупателя может существовать ноль или один профиль, и для любого профиля может существовать ноль или один покупатель. В модели магазина такая связь не имеет логического смысла – как может существовать профиль пользователя без пользователя? Однако, в некоторых случаях вам может понадобиться создать эту связь.
Для реализации связи ноль-или-один-к-ноль-или-одному в Fluent API существуют специальные методы WithOptionalDependent() и WithOptionalPrincipal(), смысл которых аналогичен соответствующим методам WithRequired… , но они применяются после вызова метода HasOptional(), как показано в примере ниже:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Profile>()
.HasOptional(p => p.CustomerOf)
.WithOptionalDependent()
.Map(p => p.MapKey("ProfileKey"));
// Аналогичная настройка
modelBuilder.Entity<Customer>()
.HasOptional(c => c.Profile)
.WithOptionalPrincipal()
.Map(c => c.MapKey("CustomerKey"));
}
Обратите внимание, что в этом примере мы явно задаем имя для внешних ключей, используя вспомогательный метод Map(). В результате, Entity Framework добавит внешний ключ как в таблицу Customers, так и в таблицу Profiles:

Фактически, отношение ноль-или-один-к-ноль-или-одному идентично двум отношениям ноль-или-один-к-одному между нашими таблицами.