Jedna do jednej opcjonalnej relacji przy użyciu interfejsu API Entity Framework Fluent

79

Chcemy użyć relacji jeden do jednej opcjonalnej przy użyciu Entity Framework Code First. Mamy dwie istoty.

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; }
}

PIIUsermoże mieć, LoyaltyUserDetailale LoyaltyUserDetailmusi mieć PIIUser. Wypróbowaliśmy te techniki płynnego podejścia.

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

To podejście nie spowodowało utworzenia LoyaltyUserDetailIdklucza obcego w PIIUserstabeli.

Następnie wypróbowaliśmy następujący kod.

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

Ale tym razem EF nie utworzył żadnych kluczy obcych w tych 2 tabelach.

Czy masz jakieś pomysły na ten numer? Jak możemy utworzyć relację jeden do jednego, używając Fluent API platformy encji?

İlkay İlknur
źródło

Odpowiedzi:

101

EF Code First obsługuje 1:1i 1:0..1relacje. To ostatnie jest tym, czego szukasz („jeden do zera lub jeden”).

Twoje próby mówienia płynnie mówią, że w jednym przypadku są wymagane z obu stron, a w drugim opcjonalne .

To, czego potrzebujesz, jest opcjonalne z jednej strony, a wymagane z drugiej.

Oto przykład z książki Programming EF Code First

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

PersonPhotoJednostka ma właściwość nawigacji o nazwie PhotoOf, która wskazuje na Personrodzaj. PersonTypu ma właściwość nawigacji o nazwie Photo, który wskazuje na PersonPhototyp.

W dwóch pokrewnych klasach używasz klucza podstawowego każdego typu , a nie kluczy obcych . tj. nie użyjesz właściwości LoyaltyUserDetailIdlub PIIUserId. Zamiast tego relacja zależy od Idpól obu typów.

Jeśli używasz interfejsu Fluent API, jak powyżej, nie musisz określać LoyaltyUser.Id jako klucza obcego, EF to rozgryzie.

Więc bez twojego kodu do przetestowania siebie (nienawidzę tego robić z głowy) ... przetłumaczyłbym to na twój kod jako

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 );
}

To mówi LoyaltyUserDetails PIIUserwłaściwość jest wymagana i PIIUser użytkownikaLoyaltyUserDetail właściwość jest opcjonalna.

Możesz zacząć od drugiego końca:

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

który teraz mówi, że LoyaltyUserDetailwłasność PIIUser jest opcjonalna, a PIIUserwłasność LoyaltyUser jest wymagana.

Zawsze musisz użyć wzoru HAS / WITH.

HTH i FWIW, relacje jeden do jednego (lub jeden do zera / jeden) to jedne z najbardziej zagmatwanych relacji, które należy najpierw skonfigurować w kodzie, więc nie jesteś sam! :)

Julie Lerman
źródło
1
Czy to nie ogranicza użytkownika, na którym polu FK wybrać? (Myślę, że on tego chce, ale nie zgłosił się, więc nie wiem), ponieważ wygląda na to, że chce mieć pole FK w klasie.
Frans Bouma
1
Zgoda. Jest to ograniczenie sposobu działania EF. 1: 1 i 1: 0..1 zależą od kluczy podstawowych. W przeciwnym razie myślę, że zbłądzisz w „unikalnym FK”, który nadal nie jest obsługiwany w EF. :( (Jesteś bardziej ekspertem od db ... czy to prawda ... naprawdę chodzi o unikalny FK?) I nie będzie w nadchodzącym Ef6, jak na: entityframework.codeplex.com/workitem/299
Julie Lerman
1
Tak @FransBouma, chcieliśmy użyć pól PIIUserId i LoyaltUserId jako kluczy obcych. Ale EF ogranicza nas w tej sytuacji, jak wspomnieliście z Julie. Dzięki za odpowiedzi.
İlkay İlknur
@JulieLerman dlaczego używasz WithOptional? Czym różni się od WithRequiredDependenti WithRequiredOptional?
Willy
1
WithOptional wskazuje na relację, która jest opcjonalna, np. 0..1 (zero lub jeden), ponieważ OP powiedział: „PIIUser może mieć LoyaltyUserDetail”. A twój zamęt, który prowadzi do drugiego pytania, jest taki, że jeden z terminów jest zły. ;) Są to WithRequiredDependent i WithRequiredPrincipal. Czy to ma większy sens? Wskazanie na wymagany koniec, którym jest osoba pozostająca na utrzymaniu (inaczej „dziecko”) lub główny, czyli „rodzic”. Nawet w relacji jeden do jednego, w której oba są równe, EF musi odnosić się do jednego jako głównego i jednego jako zależnego. HTH!
Julie Lerman,
27

Po prostu zrób tak, jeśli masz relację jeden do wielu między, LoyaltyUserDetaila PIIUserwięc mapowanie powinno być

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

EF powinien utworzyć wszystkie potrzebne klucze obce i po prostu nie przejmować się WithMany !

jr z Yaoundé
źródło
3
Odpowiedź Julie Lerman została zaakceptowana (i powinna pozostać akceptowana, IMHO), ponieważ odpowiada na pytanie i szczegółowo wyjaśnia, dlaczego jest to właściwa droga, wraz z dodatkowymi szczegółami i dyskusją. Odpowiedzi typu „kopiuj i wklej” są z pewnością dostępne w każdym miejscu, a ja sam użyłem kilku, ale jako profesjonalni programiści powinieneś bardziej skupić się na tym, by stać się lepszym programistą, a nie tylko zdobyć kod do zbudowania. To powiedziawszy, jr ma rację, choć rok później.
Mike Devenney
1
@ClickOk Wiem, że to stare pytanie i komentarz, ale to nie jest poprawna odpowiedź, ponieważ zastosowano jeden do wielu jako obejście, to nie tworzy relacji jeden do jednego ani jeden do zera
Mahmoud Darwish
3

Z Twoim kodem jest kilka błędów.

1: 1 związku jest: PK <-PK , gdzie jedna strona PK również FK lub PK <-FK + UC , gdzie boczny FK jest nie-PK i ma UC. Twój kod pokazuje, że masz FK <-FK , ponieważ definiujesz obie strony, aby miały FK, ale to źle. Ja rekonesans PIIUserto strona PK i LoyaltyUserDetailstrona FK. Oznacza to, PIIUserże nie ma pola FK, aleLoyaltyUserDetail ma.

Jeśli relacja 1: 1 jest opcjonalna, strona FK musi mieć co najmniej 1 pole dopuszczające wartość null.

pswg powyżej odpowiedział na twoje pytanie, ale popełnił błąd, że zdefiniował również FK w PIIUser, co jest oczywiście błędne, jak opisałem powyżej. Zdefiniuj więc pole FK dopuszczające wartość null w LoyaltyUserDetail, zdefiniuj atrybut w, LoyaltyUserDetailaby oznaczyć go jako pole FK, ale nie określaj pola FK w PIIUser.

Dostajesz wyjątek, który opisałeś powyżej pod postem pswg, ponieważ żadna strona nie jest stroną PK (koniec zasady).

EF nie jest zbyt dobry w stosunku 1: 1, ponieważ nie jest w stanie obsłużyć unikalnych ograniczeń. Nie jestem najpierw ekspertem od kodu, więc nie wiem, czy jest on w stanie stworzyć UC, czy nie.

(edytuj) btw: A 1: 1 B (FK) oznacza, że ​​zostało utworzone tylko 1 ograniczenie FK, na celu B wskazującym na PK A, a nie 2.

Frans Bouma
źródło
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);

to rozwiąże problem na ODNIESIENIU i KLUCZACH ZAGRANICZNYCH

podczas AKTUALIZACJI lub USUWANIA rekordu

pampi
źródło
To nie zadziała zgodnie z oczekiwaniami. Powoduje to, że kod migracji nie używa LoyaltyUserId jako klucza obcego, więc User.Id i LoyaltyUser.Id będą miały tę samą wartość, a LoyaltyUserId pozostanie puste.
Aaron Queenan
2

Spróbuj dodać ForeignKeyatrybut do LoyaltyUserDetailwłaściwości:

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

A PIIUsernieruchomość:

public class LoyaltyUserDetail
{
    ...
    public int PIIUserId { get; set; }
    [ForeignKey("PIIUserId")]
    public PIIUser PIIUser { get; set; }
    ...
}
pswg
źródło
1
Próbowaliśmy adnotacji danych przed wszystkimi tymi płynnymi podejściami do interfejsu API. Ale adnotacje danych nie działały. jeśli dodasz adnotacje do danych, o których wspomniałeś powyżej, EF zgłasza ten wyjątek => Nie można określić głównego końca powiązania między typami „LoyaltyUserDetail” i „PIIUser”. Główny koniec tego powiązania musi być jawnie skonfigurowany przy użyciu interfejsu API Fluent relacji lub adnotacji danych.
İlkay İlknur
@ İlkayİlknur Co się stanie, jeśli dodasz ForeignKeyatrybut tylko na jednym końcu relacji? czyli tylko na PIIUser lub LoyaltyUserDetail .
pswg
Ef zgłasza ten sam wyjątek.
İlkay İlknur
1
@pswg Dlaczego FK at PIIUser? LoyaltyUserDetail ma FK, a nie PIIUser. Musi więc być [Key, ForeignKey ("PIIUser")] we właściwości PIIUserId obiektu LoyaltyUserDetail. Spróbuj tego
user808128
@ user808128 Możesz umieścić adnotację we właściwości nawigacji lub identyfikatorze, bez różnicy.
Thomas Boby
1

Jedyną rzeczą, która jest myląca z powyższymi rozwiązaniami, jest to, że klucz podstawowy jest zdefiniowany jako „ Id” w obu tabelach i jeśli masz klucz podstawowy oparty na nazwie tabeli, to nie zadziała, zmodyfikowałem klasy, aby to zilustrować, tj. opcjonalna tabela nie powinna definiować swojego własnego klucza podstawowego, zamiast tego powinna używać tej samej nazwy klucza z tabeli głównej.

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; }
}

A potem

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

Zrobiłoby to, przyjęte rozwiązanie nie wyjaśnia tego jasno i wyrzuciło mnie na kilka godzin, aby znaleźć przyczynę

skjagini
źródło
1

Nie przydaje się to w przypadku oryginalnego plakatu, ale dla każdego, kto nadal korzysta z EF6, który potrzebuje klucza obcego innego niż klucz podstawowy, oto jak to zrobić:

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"));

Zwróć uwagę, że nie możesz użyć tego LoyaltyUserDetailIdpola, ponieważ o ile wiem, można je określić tylko za pomocą interfejsu API Fluent. (Wypróbowałem trzy sposoby zrobienia tego za pomocą ForeignKeyatrybutu i żaden z nich nie działał).

Aaron Queenan
źródło