Отношение один-к-одному (one-to-one) между таблицами

126

Отношение один-к-одному (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:

Два внешних ключа в каждой из таблиц при отношении ноль-или-один-к-ноль-или-одному

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

Лучший чат для C# программистов