Можно ли зафиксировать отношения от 0..1 до 0..1 в Entity Framework?

Есть ли способ сделать свойство обратной привязки для NULL для отношения NULL-отношения с внешним ключом в Entity Framework? В языке базы данных отношение 0..1 к 0..1.

Я пробовал, как показано ниже, но я продолжаю получать сообщение об ошибке:

Невозможно определить главный конец ассоциации между типами Type1 и Type2. Основной конец этой ассоциации должен быть явно сконфигурирован с использованием либо свободного API API, либо аннотаций данных.

public class Type1 {

    public int ID { get; set; }

    public int? Type2ID { get; set; }
    public Type2 Type2 { get; set; }
}

public class Type2 {

    public int ID { get; set; }

    public int? Type1ID { get; set; }
    public Type1 Type1 { get; set; }
}

Я понимаю, что целочисленный столбец может существовать только в одной таблице или другой, но, безусловно, должно быть возможно охватить все необходимые случаи? Например:

Type1      Type2
========   ===============
ID         ID   | Type1ID
--------   ---------------
1          1    | null
2          2    | 2

Я попытался использовать аннотации данных (например, [ForeignKey] на одном конце, [InverseProperty] на обоих), но ни один из них, похоже, не помогает.

Если возможно, решение по аннотации данных будет предпочтительнее Fluent API. Кроме того, свойство int? не является строго необходимым с точки зрения домена для любого класса, если это помогает.

Здесь есть интересный , который подразумевает, что невозможно зафиксировать этот вид отношений в Entity Framework (фактически, элемент, который необязательно входит в число коллекции) - если да, есть ли какая-либо документация, которая бы поддерживала это?

Ответ 1

"Это не сложно" - это то, что я, когда читаю ваш вопрос. Но снова я обнаружил, что ассоциации "один к одному" - предательские ублюдки. Здесь мы идем.

Я предполагаю, что под 0..1 – 0..1 вы подразумеваете, что два объекта могут существовать независимо друг от друга, но также могут быть исключительно связаны друг с другом.

Позволяет сделать его конкретным. Car и Driver. Представьте себе множество автомобилей и водителей, среди которых CarA и DriverA. Теперь предположим, что вы хотите, чтобы CarA связался с DriverA, и ваша реализация заключается в том, что DriverA связывается с CarA. Но как только DriverA сделает это, вы хотите, чтобы CarA был только для DriverA, Ассоциация CarA больше не является необязательной, поэтому ее также следует установить сразу.

Как это реализовать?

Вариант 1:

Если это рабочая модель:

public class Car
{
    public int CarId { get; set; }
    public string Name { get; set; }
    public int? DriverId { get; set; }
    public virtual Driver Driver { get; set; }
}

public class Driver
{
    public int DriverId { get; set; }
    public string Name { get; set; }
    public int? CarId { get; set; }
    public virtual Car Car { get; set; }
}

технически DriverA может иметь внешний ключ для CarA и CarA внешнего ключа DriverB.

enter image description here

Поэтому, когда установлен внешний ключ DriverA-CarA, вы должны "одновременно" установить обратный внешний ключ CarA-DriverA. Это то, что вы должны делать в коде, что означает, что это бизнес-правило. И на самом деле это не атомная операция, поэтому вы должны убедиться, что это было сделано в одной транзакции с базой данных.

Модель класса по крайней мере поддерживает прецедент, но это тоже разрешительно. Он должен быть ограничен. Что еще более важно, он не будет работать с EF. EF жалуется на то, что нужно установить основной конец. И если вы это сделаете, EF не создаст двунаправленную связь.

Альтернативное отображение было предложено здесь. Я пробовал это, но с двумя необязательными ассоциациями:

В конфигурации отображения Driver:

this.HasOptional(t => t.Car).WithMany().HasForeignKey(d => d.CarId);

В конфигурации отображения Car:

this.HasOptional(t => t.Driver).WithMany().HasForeignKey(c => c.DriverId);

(альтернативы аннотации данных нет)

Я обнаружил, что EF устанавливает только одно значение внешнего ключа в базе данных при создании нового драйвера и автомобиля. Вы должны устанавливать и сохранять обе ассоциации отдельно, управляя своей собственной транзакцией. С существующими объектами вы все равно должны установить оба внешних ключа, хотя это можно сохранить одним вызовом SaveChanges.

Лучшие варианты? Посмотрим...

Вариант 2:

Это ассоциация "один ко многим", как указано в ссылке, на которую вы ссылаетесь. Эта модель требует внешних ограничений, но создание ассоциации является атомарным. И у вас все еще есть ссылка на одном конце и коллекция на другом конце. И он легко сопоставляется с EF.

Вариант 3:

Вы можете создать таблицу соединений CarDriver, которая имеет два внешних ключа, до Car и Driver, оба из которых содержат свой уникальный первичный ключ:

enter image description here

Это регулярная ассоциация "многие-ко-многим". По умолчанию EF будет отображать это как модель класса, в которой Car и Driver имеют свойства сопоставления, указывающие друг на друга, а таблица соединений не отображается напрямую:

public class Car
{
    public int CarId { get; set; }
    public string Name { get; set; }
    public virtual ICollection<Driver> Drivers { get; set; }
}

public class Driver
{
    public int DriverId { get; set; }
    public string Name { get; set; }
    public virtual ICollection<Car> Cars { get; set; }
}

Теперь создание ассоциации - это атомная операция. Совершенно возможно сопоставить эту модель с EF. Общие ссылки исчезли, но вы по-прежнему можете получить FirstOrDefault() свойств коллекции как суррогатную ссылку.

Но там важная добыча. Теперь каждый объект может иметь любое количество ассоциированных копий. Если вы создаете ассоциацию, вам нужно закодированное бизнес-правило, которое проверяет, нет ли у задействованных объектов каких-либо ассоциаций. Возможно, этот вариант еще хуже, чем вариант 2. Но я упомянул об этом из-за следующей опции:

Вариант 4

Вариант 3 является атомарным, но он также требует внешних ограничений. Чтобы сделать ассоциацию исключительной, оба столбца в CarDriver должны иметь уникальные ключи, поэтому каждый автомобиль или драйвер может появляться только один раз в таблице. По этим показателям модель реализует двунаправленную необязательную ассоциацию 1:1 сама по себе. Любой код, работающий над ним, должен подчиняться правилам. Безопасный и надежный...

Но вы хотите этого?

Вы никогда не собираетесь настраивать параметр 4 в коде EF-first. Не с плавным отображением и аннотациями данных. Вы должны использовать "Миграции" для этого или сначала работать с базой данных.

Если база данных используется несколькими приложениями, лучшим вариантом может быть привязка ограничений в модели данных. Если правило вряд ли изменится, это также может быть жизнеспособным вариантом. Однако, если в базе данных работают только одно (или два) приложения, и в будущем может измениться бизнес-правило, я предпочел бы более либеральную модель данных, сопровождаемую закодированными правилами. Кодовая бизнес-логика намного легче изменить, чем модель данных.

Кроме того, даже с опцией 4 вам нужна бизнес-логика, чтобы проверить существование ассоциации, прежде чем пытаться ее создать, иначе для вас это сделает уродливая ошибка базы данных.

Выводы

Вариант 1 близок к тому, что вы хотите. Но мне не нравится обязательство устанавливать оба внешних ключа, это легко забывается или игнорируется будущими разработчиками.

Но Вариант 2 и 3 имеют еще более серьезные требования в отношении закодированных бизнес-правил, которые можно забыть. И коллекции неестественны, поскольку суррогатное "1" заканчивается. Вариант 3 имеет некоторое обращение ко мне, потому что Car и Driver полностью независимы в базе данных, а ассоциация - это запись с непустыми внешними ключами (DBA тоже любят это).

Вариант 4 имеет такую ​​же привлекательность, и это лучший вариант, когда нескольким приложениям придется реализовывать внешние ограничения, которые должны быть наложены на опции 2 и 3. Кроме того, даже если закодированные правила забыты, ограничения базы данных окончательный улов. Но он не может быть легко реализован с помощью EF-кода.

Ответ 2

Это не то, как вы создаете таблицы с инфраструктурой сущностей. Правильное объявление для этих классов:

public class Type1 {
    public int ID { get; set; }
}

public class Type2 {

    public int ID { get; set; }
    public virtual Type1 @Type1 { get; set; }
}

Изменить: Я думаю, что самый простой способ сделать то, что вы хотите, это:

    public class Type1 {
        public int ID { get; set; }
        public virtual ContainerClass {get; set;}
    }

    public class Type2 {

        public int ID { get; set; }
        public virtual ContainerClass {get; set;}
    }

    public class ContainerClass {
         public int ID {get;set;}
         public virtual Type1 @Type1 {get;set;}
         public virtual Type2 @Type2 {get;set;}
}

Ответ 3

Выполнение этого из памяти, к сожалению, не протестировано:

public Type1
{
    [Key]        
    public int ID { get; set; }

    [ForeignKey("Type2")]
    public int? Type2ID { get; set; }
    public virtual Type2 Type2 { get; set; }
}

public Type2
{
    [Key]        
    public int ID { get; set; }

    [ForeignKey("Type1")]
    public int? Type1ID { get; set; }
    public virtual Type1 Type1 { get; set; }
}

Как в стороне, использование явного ForeignKey в сущности позволяет проверить, есть ли связанный объект без необходимости вызова базы данных. например IEnumerable<Type1>.Where(t => t.Type2ID.HasValue) будет возвращать все объекты Type1, у которых есть связанный Type2! См. этот вопрос для получения более подробной информации.