Необязательные отношения один к одному с использованием Entity Framework Fluent API

79

Мы хотим использовать необязательные отношения один к одному, используя Entity Framework Code First. У нас есть две сущности.

public class PIIUser
{
    public int Id { get; set; }

    public int? LoyaltyUserDetailId { get; set; }
    public LoyaltyUserDetail LoyaltyUserDetail { get; set; }
}

public class LoyaltyUserDetail
{
    public int Id { get; set; }
    public double? AvailablePoints { get; set; }

    public int PIIUserId { get; set; }
    public PIIUser PIIUser { get; set; }
}

PIIUserможет иметь, LoyaltyUserDetailно LoyaltyUserDetailдолжен иметь PIIUser. Мы попробовали эти техники беглого подхода.

modelBuilder.Entity<PIIUser>()
            .HasOptional(t => t.LoyaltyUserDetail)
            .WithOptionalPrincipal(t => t.PIIUser)
            .WillCascadeOnDelete(true);

Этот подход не создавал LoyaltyUserDetailIdвнешний ключ в PIIUsersтаблице.

После этого мы попробовали следующий код.

modelBuilder.Entity<LoyaltyUserDetail>()
            .HasRequired(t => t.PIIUser)
            .WithRequiredDependent(t => t.LoyaltyUserDetail);

Но на этот раз EF не создавал никаких внешних ключей в этих двух таблицах.

У вас есть идеи по этому поводу? Как мы можем создать необязательные отношения один-к-одному, используя Fluent API Entity Framework?

İlkay İlknur
источник

Ответы:

101

EF Code First поддерживает 1:1и 1:0..1отношения. Последнее - то, что вы ищете («один к нулю или одному»).

Ваши попытки беглого языка говорят о необходимости с обеих сторон в одном случае и необязательной с обеих сторон в другом.

То, что вам нужно, необязательно с одной стороны и обязательно с другой.

Вот пример из книги Programming EF Code First

modelBuilder.Entity<PersonPhoto>()
.HasRequired(p => p.PhotoOf)
.WithOptional(p => p.Photo);

PersonPhotoОбъект имеет свойство навигации под названием , PhotoOfчто указывает на Personтип. PersonТип имеет свойство навигации под названием , Photoчто указывает на PersonPhotoтип.

В двух связанных классах вы используете первичный ключ каждого типа , а не внешние ключи . т.е. вы не будете использовать свойства LoyaltyUserDetailIdили PIIUserId. Вместо этого отношения зависят от Idполей обоих типов.

Если вы используете текучий API, как указано выше, вам не нужно указывать LoyaltyUser.Idв качестве внешнего ключа, EF разберется с этим.

Поэтому, не имея вашего кода для проверки себя (я ненавижу делать это из головы) ... я бы перевел это в ваш код как

public class PIIUser
{
    public int Id { get; set; }    
    public LoyaltyUserDetail LoyaltyUserDetail { get; set; }
}

public class LoyaltyUserDetail
{
    public int Id { get; set; }
    public double? AvailablePoints { get; set; }    
    public PIIUser PIIUser { get; set; }
}

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
  modelBuilder.Entity<LoyaltyUserDetail>()
  .HasRequired(lu => lu.PIIUser )
  .WithOptional(pi => pi.LoyaltyUserDetail );
}

Это означает, что PIIUserсвойство LoyaltyUserDetails является обязательным, а LoyaltyUserDetailсвойство PIIUser - необязательным.

Вы можете начать с другого конца:

modelBuilder.Entity<PIIUser>()
.HasOptional(pi => pi.LoyaltyUserDetail)
.WithRequired(lu => lu.PIIUser);

который теперь говорит, что LoyaltyUserDetailсвойство PIIUser является необязательным, а PIIUserсвойство LoyaltyUser является обязательным.

Всегда нужно использовать шаблон HAS / WITH.

HTH и FWIW, отношения один к одному (или один к нулю / одному) - одни из самых запутанных отношений, которые нужно сначала настроить в коде, поэтому вы не одиноки! :)

Джули Лерман
источник
1
Разве это не ограничивает пользователя, какое поле FK выбирать? (Я думаю, он этого хочет, но он не ответил, поэтому я не знаю), поскольку кажется, что он хочет, чтобы в классе присутствовало поле FK.
Frans Bouma
1
Согласовано. Это ограничение того, как работает EF. 1: 1 и 1: 0..1 зависят от первичных ключей. В противном случае, я думаю, вы заблуждаетесь о «уникальном FK», который все еще не поддерживается в EF. :( (Вы больше являетесь экспертом по базам данных ... это правильно ... это действительно об уникальном FK?) И не будет в предстоящем Ef6 согласно: entityframework.codeplex.com/workitem/299
Джули Лерман
1
Да, @FransBouma, мы хотели использовать поля PIIUserId и LoyaltUserId в качестве внешних ключей. Но EF ограничивает нас в этой ситуации, как упомянули вы и Джули. Спасибо за ответы.
İlkay İlknur
@JulieLerman, почему вы используете WithOptional? Чем отличается от WithRequiredDependentи WithRequiredOptional?
Вилли
1
WithOptional указывает на взаимосвязь, которая является необязательной, например 0..1 (ноль или один), потому что OP сказал, что «PIIUser может иметь LoyaltyUserDetail». И ваше замешательство, которое приводит ко второму вопросу, связано с тем, что вы неправильно поняли один из терминов. ;) Это WithRequiredDependent и WithRequiredPrincipal. В этом больше смысла? Указание на требуемый конец, который является либо зависимым (он же «дочерний»), либо основным, также известным как «родитель». Даже в отношениях «один к одному», когда оба равны, EF должен называть одного принципалом, а другого - зависимым. HTH!
Джули Лерман
27

Просто сделайте это так, как если бы между вами была связь "один ко многим", LoyaltyUserDetailи PIIUserпоэтому отображение должно быть

modelBuilder.Entity<LoyaltyUserDetail>()
       .HasRequired(m => m.PIIUser )
       .WithMany()
       .HasForeignKey(c => c.LoyaltyUserDetailId);

EF должен создать весь необходимый вам внешний ключ и просто не заботиться о WithMany !

младший из Яунде
источник
3
Ответ Джули Лерман был принят (и должен оставаться принятым, ИМХО), потому что он отвечает на вопрос и подробно описывает, почему это правильный путь, с подробным подтверждением и обсуждением. Ответы на копирование и вставку, безусловно, доступны повсюду в SO, и я сам использовал некоторые из них, но как профессиональные разработчики вы должны больше заботиться о том, чтобы стать лучшим программистом, а не просто получить код для сборки. Тем не менее, младший понял это правильно, хотя и годом позже.
Майк Девенни 08
1
@ClickOk Я знаю, что это старый вопрос и комментарий, но это неправильный ответ, потому что он использовал один ко многим в качестве обходного пути, это не делает отношения один к одному или один к нулю
Махмуд Дарвиш
3

В вашем коде есть несколько ошибок.

1: 1 отношения либо: ПК <-PK , где одна сторона ПК также ФК или ПК <-fk + UC , где сторона FK не-ПК и имеет UC. Ваш код показывает, что у вас FK <-FK , поскольку вы определяете, что обе стороны имеют FK, но это неправильно. Я разведываю PIIUserна стороне ПК и LoyaltyUserDetailна стороне ФК. Это означает, что у PIIUserнего нет поля FK, ноLoyaltyUserDetail есть.

Если соотношение 1: 1 является необязательным, сторона FK должна иметь как минимум 1 поле, допускающее значение NULL.

pswg выше ответил на ваш вопрос, но сделал ошибку, так как он / она также определил FK в PIIUser, что, конечно, неправильно, как я описал выше. Поэтому определите поле FK, допускающее обнуление, в LoyaltyUserDetail, определите атрибут, LoyaltyUserDetailчтобы пометить его как поле FK, но не указывайте поле FK в PIIUser.

Вы получите исключение, которое вы описали выше под сообщением pswg, потому что никакая сторона не является стороной PK (основной конец).

EF не очень хорош в соотношении 1: 1, поскольку не может обрабатывать уникальные ограничения. Я не специалист по коду в первую очередь, поэтому я не знаю, может ли он создать UC или нет.

(править) кстати: A 1: 1 B (FK) означает, что создано только 1 ограничение FK для цели B, указывающей на PK A, а не 2.

Франс Баума
источник
3
public class User
{
    public int Id { get; set; }
    public int? LoyaltyUserId { get; set; }
    public virtual LoyaltyUser LoyaltyUser { get; set; }
}

public class LoyaltyUser
{
    public int Id { get; set; }
    public virtual User MainUser { get; set; }
}

        modelBuilder.Entity<User>()
            .HasOptional(x => x.LoyaltyUser)
            .WithOptionalDependent(c => c.MainUser)
            .WillCascadeOnDelete(false);

это решит проблему с ССЫЛКАМИ и ИНОСТРАННЫМИ КЛЮЧАМИ

при ОБНОВЛЕНИИ или УДАЛЕНИИ записи

пампи
источник
Это не сработает так, как ожидалось. Это приводит к тому, что код миграции не использует LoyaltyUserId в качестве внешнего ключа, поэтому User.Id и LoyaltyUser.Id будут иметь одно и то же значение, а LoyaltyUserId останется пустым.
Аарон Куинан,
2

Попробуйте добавить ForeignKeyатрибут к LoyaltyUserDetailсвойству:

public class PIIUser
{
    ...
    public int? LoyaltyUserDetailId { get; set; }
    [ForeignKey("LoyaltyUserDetailId")]
    public LoyaltyUserDetail LoyaltyUserDetail { get; set; }
    ...
}

И PIIUserсвойство:

public class LoyaltyUserDetail
{
    ...
    public int PIIUserId { get; set; }
    [ForeignKey("PIIUserId")]
    public PIIUser PIIUser { get; set; }
    ...
}
pswg
источник
1
Мы пробовали аннотации данных перед всеми этими плавными подходами API. Но аннотации данных не работали. если вы добавляете аннотации данных, упомянутые выше, EF генерирует это исключение => Невозможно определить основной конец связи между типами «LoyaltyUserDetail» и «PIIUser». Главный конец этой связи должен быть явно настроен с использованием либо API текучей связи, либо аннотаций данных.
İlkay İlknur
@ İlkayİlknur Что произойдет, если вы добавите ForeignKeyатрибут только к одному концу отношения? т.е. только на PIIUser или LoyaltyUserDetail .
pswg
Ef выдает то же исключение.
İlkay İlknur
1
@pswg Почему FK в PIIUser? LoyaltyUserDetail имеет FK, а не PIIUser. Таким образом, это должно быть [Key, ForeignKey ("PIIUser")] в свойстве PIIUserId объекта LoyaltyUserDetail. Попробуйте это
user808128 02
@ user808128 Вы можете разместить аннотацию либо на свойстве навигации, либо на идентификаторе, без разницы.
Thomas Boby
1

Единственное, что сбивает с толку вышеупомянутые решения, заключается в том, что первичный ключ определяется как « Id» в обеих таблицах, и если у вас есть первичный ключ на основе имени таблицы, это не сработает, я изменил классы, чтобы проиллюстрировать то же самое, т.е. необязательная таблица не должна определять свой собственный первичный ключ, а должна использовать то же имя ключа из основной таблицы.

public class PIIUser
{
    // For illustration purpose I have named the PK as PIIUserId instead of Id
    // public int Id { get; set; }
    public int PIIUserId { get; set; }

    public int? LoyaltyUserDetailId { get; set; }
    public LoyaltyUserDetail LoyaltyUserDetail { get; set; }
}

public class LoyaltyUserDetail
{
    // Note: You cannot define a new Primary key separately as it would create one to many relationship
    // public int LoyaltyUserDetailId { get; set; }

    // Instead you would reuse the PIIUserId from the primary table, and you can mark this as Primary Key as well as foreign key to PIIUser table
    public int PIIUserId { get; set; } 
    public double? AvailablePoints { get; set; }

    public int PIIUserId { get; set; }
    public PIIUser PIIUser { get; set; }
}

А затем последовало

modelBuilder.Entity<PIIUser>()
.HasOptional(pi => pi.LoyaltyUserDetail)
.WithRequired(lu => lu.PIIUser);

Сделал бы трюк, принятое решение не может четко объяснить это, и это отбросило меня на несколько часов, чтобы найти причину.

Skjagini
источник
1

Это бесполезно для оригинального плаката, но для тех, кто все еще использует EF6, кому нужно, чтобы внешний ключ отличался от первичного, вот как это сделать:

public class PIIUser
{
    public int Id { get; set; }

    //public int? LoyaltyUserDetailId { get; set; }
    public LoyaltyUserDetail LoyaltyUserDetail { get; set; }
}

public class LoyaltyUserDetail
{
    public int Id { get; set; }
    public double? AvailablePoints { get; set; }

    public int PIIUserId { get; set; }
    public PIIUser PIIUser { get; set; }
}

modelBuilder.Entity<PIIUser>()
    .HasRequired(t => t.LoyaltyUserDetail)
    .WithOptional(t => t.PIIUser)
    .Map(m => m.MapKey("LoyaltyUserDetailId"));

Обратите внимание, что вы не можете использовать это LoyaltyUserDetailIdполе, потому что, насколько я могу судить, его можно указать только с помощью свободного API. (Я пробовал три способа сделать это с помощью ForeignKeyатрибута, и ни один из них не работал).

Аарон Куинен
источник