Entity Framework Code First - zwei Fremdschlüssel aus derselben Tabelle

260

Ich habe gerade erst angefangen, EF-Code zu verwenden, daher bin ich ein absoluter Anfänger in diesem Thema.

Ich wollte Beziehungen zwischen Teams und Spielen herstellen:

1 Spiel = 2 Mannschaften (Heim, Gast) und Ergebnis.

Ich dachte, es sei einfach, ein solches Modell zu erstellen, und begann mit dem Codieren:

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

Und ich bekomme eine Ausnahme:

Die Referenzbeziehung führt zu einer zyklischen Referenz, die nicht zulässig ist. [Einschränkungsname = Match_GuestTeam]

Wie kann ich ein solches Modell mit 2 Fremdschlüsseln für dieselbe Tabelle erstellen?

Jarek
quelle

Antworten:

296

Versuche dies:

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

Primärschlüssel werden standardmäßig zugeordnet. Die Mannschaft muss zwei Spiele haben. Sie können keine einzelne Sammlung haben, auf die von zwei FKs verwiesen wird. Match wird ohne kaskadierendes Löschen zugeordnet, da es in diesen selbstreferenzierenden Many-to-Many nicht funktioniert.

Ladislav Mrnka
quelle
3
Was ist, wenn zwei Teams nur einmal spielen dürfen?
ca9163d9
4
@NickW: Das müssen Sie in Ihrer Anwendung und nicht im Mapping behandeln. Aus der Mapping-Perspektive dürfen Paare zweimal spielen (jedes ist einmal Gast und zu Hause).
Ladislav Mrnka
2
Ich habe ein ähnliches Modell. Was ist der richtige Weg, um das Löschen von Kaskaden zu behandeln, wenn ein Team entfernt wird? Ich habe versucht, einen STATT DELETE-Trigger zu erstellen, bin mir aber nicht sicher, ob es eine bessere Lösung gibt? Ich würde es vorziehen, dies in der Datenbank zu behandeln, nicht in der Anwendung.
Woodchipper
1
@ Mrshickadance: Es ist das gleiche. Ein Ansatz verwendet eine fließende API und andere Datenanmerkungen.
Ladislav Mrnka
1
Wenn ich WillCascadeOnDelete false verwende, wird ein Fehler ausgegeben, wenn ich das Team löschen möchte. Eine Beziehung aus dem AssociationSet 'Team_HomeMatches' befindet sich im Status 'Gelöscht'. Bei Multiplizitätsbeschränkungen muss sich ein entsprechendes 'Team_HomeMatches_Target' ebenfalls im Status 'Gelöscht' befinden.
Rupesh Kumar Tiwari
55

Es ist auch möglich, das ForeignKey()Attribut in der Navigationseigenschaft anzugeben :

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

Auf diese Weise müssen Sie der OnModelCreateMethode keinen Code hinzufügen

ShaneA
quelle
4
Ich bekomme so oder so die gleiche Ausnahme.
Jo Smo
11
Dies ist meine Standardmethode zum Angeben von Fremdschlüsseln, die in allen Fällen funktioniert, AUSSER wenn eine Entität mehr als eine nav-Eigenschaft desselben Typs enthält (ähnlich dem HomeTeam- und GuestTeam-Szenario). In diesem Fall wird EF beim Generieren von SQL verwirrt. Die Lösung besteht darin, Code OnModelCreategemäß der akzeptierten Antwort sowie den beiden Sammlungen für beide Seiten der Beziehung hinzuzufügen .
Steven Manuel
Ich benutze onmodelcreating in allen Fällen außer dem genannten Fall. Ich benutze den Fremdschlüssel für Datenanmerkungen. Ich weiß auch nicht, warum er nicht akzeptiert wird.
Hosam Hemaily
48

Ich weiß, dass es ein mehrere Jahre alter Beitrag ist und Sie Ihr Problem mit der oben genannten Lösung lösen können. Ich möchte jedoch nur vorschlagen, InverseProperty für jemanden zu verwenden, der es noch benötigt. Zumindest müssen Sie in OnModelCreating nichts ändern.

Der folgende Code ist nicht getestet.

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

Weitere Informationen zu InverseProperty finden Sie auf MSDN: https://msdn.microsoft.com/en-us/data/jj591583?f=255&MSPPError=-2147217396#Relationships

khoa_chung_89
quelle
1
Vielen Dank für diese Antwort. Dadurch werden die Fremdschlüsselspalten in der Match-Tabelle jedoch auf Null gesetzt.
RobHurd
Dies funktionierte hervorragend für mich in EF 6, wo nullfähige Sammlungen benötigt wurden.
Pynt
Wenn Sie eine fließende API vermeiden möchten (aus welchem ​​Grund auch immer #differentdiscussion), funktioniert dies fantastisch. In meinem Fall musste ich eine zusätzliche foriegnKey-Annotation in die Entität "Match" einfügen, da meine Felder / Tabellen Zeichenfolgen für PKs enthalten.
DiscipleMichael
1
Das hat bei mir sehr gut funktioniert. Übrigens. Wenn Sie nicht möchten, dass die Spalten nullwertfähig sind, können Sie einfach einen Fremdschlüssel mit dem Attribut [ForeignKey] angeben. Wenn der Schlüssel nicht nullwertfähig ist, sind Sie fertig.
Jakub Holovsky
16

Sie können dies auch versuchen:

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

Wenn Sie eine FK-Spalte NULLS zulassen, unterbrechen Sie den Zyklus. Oder wir betrügen nur den EF-Schema-Generator.

In meinem Fall löst diese einfache Änderung das Problem.

Maico
quelle
3
Achtung Leser. Dies könnte zwar das Problem der Schemadefinition umgehen, ändert jedoch die Semantik. Es ist wahrscheinlich nicht der Fall, dass ein Match ohne zwei Teams stattfinden kann.
N8allan
14

Dies liegt daran, dass Kaskadenlöschungen standardmäßig aktiviert sind. Das Problem ist, dass beim Aufrufen eines Löschvorgangs für die Entität auch alle Entitäten gelöscht werden, auf die mit dem F-Schlüssel verwiesen wird. Sie sollten 'erforderliche' Werte nicht auf null setzen, um dieses Problem zu beheben. Eine bessere Option wäre, die Cascade-Löschkonvention von EF Code First zu entfernen:

modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>(); 

Es ist wahrscheinlich sicherer, beim Zuordnen / Konfigurieren explizit anzugeben, wann für jedes der untergeordneten Elemente eine Kaskadenlöschung durchgeführt werden soll. die Entität.

juls
quelle
Was ist es also, nachdem dies ausgeführt wurde? Restrictstatt Cascade?
Jo Smo
4

InverseProperty in EF Core macht die Lösung einfach und sauber.

InverseProperty

Die gewünschte Lösung wäre also:

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
quelle