Entity Framework Code First - dwa klucze obce z tej samej tabeli

260

Właśnie zacząłem używać kodu EF, więc jestem całkowitym początkującym w tym temacie.

Chciałem stworzyć relacje między drużynami i meczami:

1 mecz = 2 drużyny (gospodarz, gość) i wynik.

Pomyślałem, że łatwo jest stworzyć taki model, więc zacząłem kodować:

public class Team
{
    [Key]
    public int TeamId { get; set;} 
    public string Name { get; set; }

    public virtual ICollection<Match> Matches { get; set; }
}


public class Match
{
    [Key]
    public int MatchId { get; set; }

    [ForeignKey("HomeTeam"), Column(Order = 0)]
    public int HomeTeamId { get; set; }
    [ForeignKey("GuestTeam"), Column(Order = 1)]
    public int GuestTeamId { get; set; }

    public float HomePoints { get; set; }
    public float GuestPoints { get; set; }
    public DateTime Date { get; set; }

    public virtual Team HomeTeam { get; set; }
    public virtual Team GuestTeam { get; set; }
}

I dostaję wyjątek:

Relacja referencyjna spowoduje cykliczne odwołanie, które jest niedozwolone. [Ograniczenie nazwa = Match_GuestTeam]

Jak mogę stworzyć taki model z 2 kluczami obcymi do tej samej tabeli?

Jarek
źródło

Odpowiedzi:

296

Spróbuj tego:

public class Team
{
    public int TeamId { get; set;} 
    public string Name { get; set; }

    public virtual ICollection<Match> HomeMatches { get; set; }
    public virtual ICollection<Match> AwayMatches { get; set; }
}

public class Match
{
    public int MatchId { get; set; }

    public int HomeTeamId { get; set; }
    public int GuestTeamId { get; set; }

    public float HomePoints { get; set; }
    public float GuestPoints { get; set; }
    public DateTime Date { get; set; }

    public virtual Team HomeTeam { get; set; }
    public virtual Team GuestTeam { get; set; }
}


public class Context : DbContext
{
    ...

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Match>()
                    .HasRequired(m => m.HomeTeam)
                    .WithMany(t => t.HomeMatches)
                    .HasForeignKey(m => m.HomeTeamId)
                    .WillCascadeOnDelete(false);

        modelBuilder.Entity<Match>()
                    .HasRequired(m => m.GuestTeam)
                    .WithMany(t => t.AwayMatches)
                    .HasForeignKey(m => m.GuestTeamId)
                    .WillCascadeOnDelete(false);
    }
}

Klucze podstawowe są mapowane domyślnie. Drużyna musi mieć dwa zbiory meczów. Nie możesz mieć pojedynczej kolekcji, do której odnoszą się dwa FK. Dopasowanie jest mapowane bez kaskadowego usuwania, ponieważ nie działa w tych odniesieniach wielu do wielu.

Ladislav Mrnka
źródło
3
Co jeśli dwie drużyny mogą grać tylko raz?
ca9163d9
4
@NickW: To jest coś, z czym musisz sobie poradzić w aplikacji, a nie w mapowaniu. Z perspektywy mapowania pary mogą grać dwukrotnie (każda jest gościem i gospodarzem raz).
Ladislav Mrnka
2
Mam podobny model. Jaki jest właściwy sposób obsługi usuwania kaskadowego, jeśli zespół zostanie usunięty? Szukałem wyzwalacza INSTEAD OF DELETE, ale nie jestem pewien, czy istnieje lepsze rozwiązanie? Wolałbym sobie z tym poradzić w DB, a nie w aplikacji.
Świstak
1
@mrshickadance: To jest to samo. Jedno podejście wykorzystuje płynny interfejs API i inne adnotacje danych.
Ladislav Mrnka
1
Jeśli użyję wartości WillCascadeOnDelete false, to jeśli chcę usunąć zespół, wówczas występuje błąd generowania. Relacja z zestawu „Team_HomeMatches” AssociationSet jest w stanie „Usunięte”. Biorąc pod uwagę ograniczenia wielokrotności, odpowiedni „Team_HomeMatches_Target” musi również znajdować się w stanie „Usunięte”.
Rupesh Kumar Tiwari
55

Możliwe jest również określenie ForeignKey()atrybutu we właściwości nawigacji:

[ForeignKey("HomeTeamID")]
public virtual Team HomeTeam { get; set; }
[ForeignKey("GuestTeamID")]
public virtual Team GuestTeam { get; set; }

W ten sposób nie musisz dodawać żadnego kodu do OnModelCreatemetody

ShaneA
źródło
4
Tak czy inaczej dostaję ten sam wyjątek.
Jo Smo,
11
Jest to mój standardowy sposób określania kluczy obcych, który działa we wszystkich przypadkach Z WYJĄTKIEM, gdy jednostka zawiera więcej niż jedną właściwość nav tego samego typu (podobną do scenariusza HomeTeam i GuestTeam), w którym to przypadku EF mylnie się podczas generowania kodu SQL. Rozwiązaniem jest dodanie kodu OnModelCreatezgodnie z przyjętą odpowiedzią, a także dwóch kolekcji dla obu stron relacji.
Steven Manuel,
Używam onmodelcreating we wszystkich przypadkach oprócz wymienionego przypadku, używam klucza obcego adnotacji danych, a także nie wiem, dlaczego nie jest akceptowany !!
hosam co pół
48

Wiem, że to post ma kilka lat i możesz rozwiązać swój problem z powyższym rozwiązaniem. Chcę tylko zasugerować użycie InverseProperty dla kogoś, kto nadal potrzebuje. Przynajmniej nie musisz nic zmieniać w OnModelCreating.

Poniższy kod nie został przetestowany.

public class Team
{
    [Key]
    public int TeamId { get; set;} 
    public string Name { get; set; }

    [InverseProperty("HomeTeam")]
    public virtual ICollection<Match> HomeMatches { get; set; }

    [InverseProperty("GuestTeam")]
    public virtual ICollection<Match> GuestMatches { get; set; }
}


public class Match
{
    [Key]
    public int MatchId { get; set; }

    public float HomePoints { get; set; }
    public float GuestPoints { get; set; }
    public DateTime Date { get; set; }

    public virtual Team HomeTeam { get; set; }
    public virtual Team GuestTeam { get; set; }
}

Możesz przeczytać więcej o InverseProperty na MSDN: https://msdn.microsoft.com/en-us/data/jj591583?f=255&MSPPError=-2147217396#Relationships

khoa_chung_89
źródło
1
Dzięki za tę odpowiedź, jednak powoduje, że kolumny klucza obcego są zerowalne w tabeli dopasowania.
RobHurd
Działa to dla mnie świetnie w EF 6, gdzie potrzebne były kolekcje zerowalne.
Pynt
Jeśli chcesz uniknąć płynnego API (z dowolnego powodu #differentdiscussion), działa to fantastycznie. W moim przypadku musiałem dołączyć dodatkową adnotację foriegnKey do encji „Dopasuj”, ponieważ moje pola / tabele zawierają ciągi znaków PK.
DiscipleMichael
1
To działało dla mnie bardzo dobrze. Btw. jeśli nie chcesz, aby kolumny były zerowalne, możesz po prostu określić klucz obcy za pomocą atrybutu [ForeignKey]. Jeśli klucz nie ma wartości zerowej, oznacza to, że wszystko jest ustawione.
Jakub Holovsky,
16

Możesz też spróbować:

public class Match
{
    [Key]
    public int MatchId { get; set; }

    [ForeignKey("HomeTeam"), Column(Order = 0)]
    public int? HomeTeamId { get; set; }
    [ForeignKey("GuestTeam"), Column(Order = 1)]
    public int? GuestTeamId { get; set; }

    public float HomePoints { get; set; }
    public float GuestPoints { get; set; }
    public DateTime Date { get; set; }

    public virtual Team HomeTeam { get; set; }
    public virtual Team GuestTeam { get; set; }
}

Kiedy tworzysz kolumnę FK zezwalającą na NULLS, przerywasz cykl. Lub po prostu oszukujemy generator schematu EF.

W moim przypadku ta prosta modyfikacja rozwiązuje problem.

Maico
źródło
3
Uwaga czytelnicy. Chociaż może to obejść problem z definicją schematu, zmienia semantykę. Prawdopodobnie nie jest tak, że mecz można rozegrać bez dwóch drużyn.
N8allan,
14

Wynika to z tego, że usuwanie kaskadowe jest domyślnie włączone. Problem polega na tym, że wywołanie operacji usuwania na encji spowoduje również usunięcie każdej z encji, do których odwołuje się klawisz F. Nie należy dopuszczać, aby wartości „wymagane” były zerowane, aby rozwiązać ten problem. Lepszym rozwiązaniem byłoby usunięcie konwencji usuwania kaskadowego kodu EF First:

modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>(); 

Prawdopodobnie bezpieczniej jest jednoznacznie wskazać, kiedy wykonać mapowanie kaskadowe dla każdego z dzieci podczas mapowania / konfiguracji. jednostka.

juls
źródło
Więc co to jest po wykonaniu? Restrictzamiast Cascade?
Jo Smo,
4

InverseProperty w EF Core sprawia, że ​​rozwiązanie jest łatwe i czyste.

InverseProperty

Tak więc pożądanym rozwiązaniem byłoby:

public class Team
{
    [Key]
    public int TeamId { get; set;} 
    public string Name { get; set; }

    [InverseProperty(nameof(Match.HomeTeam))]
    public ICollection<Match> HomeMatches{ get; set; }

    [InverseProperty(nameof(Match.GuestTeam))]
    public ICollection<Match> AwayMatches{ get; set; }
}


public class Match
{
    [Key]
    public int MatchId { get; set; }

    [ForeignKey(nameof(HomeTeam)), Column(Order = 0)]
    public int HomeTeamId { get; set; }
    [ForeignKey(nameof(GuestTeam)), Column(Order = 1)]
    public int GuestTeamId { get; set; }

    public float HomePoints { get; set; }
    public float GuestPoints { get; set; }
    public DateTime Date { get; set; }

    public Team HomeTeam { get; set; }
    public Team GuestTeam { get; set; }
}
pritesh agrawal
źródło