Entity Framework Code First - due chiavi esterne dalla stessa tabella


260

Ho appena iniziato a utilizzare il codice EF, quindi sono un principiante assoluto in questo argomento.

Volevo creare relazioni tra squadre e partite:

1 partita = 2 squadre (casa, ospite) e risultato.

Ho pensato che fosse facile creare un modello del genere, quindi ho iniziato a scrivere codice:

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

E ottengo un'eccezione:

La relazione referenziale si tradurrà in un riferimento ciclico non consentito. [Nome vincolo = Match_GuestTeam]

Come posso creare un modello del genere, con 2 chiavi esterne nella stessa tabella?

Risposte:


297

Prova questo:

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

Le chiavi primarie sono mappate per convenzione predefinita. La squadra deve avere due raccolte di partite. Non è possibile avere un'unica raccolta a cui fanno riferimento due FK. La corrispondenza è mappata senza eliminazione a cascata perché non funziona in questi molti riferimenti a molti.


3
Cosa succede se due squadre possono giocare solo una volta?
ca9163d9,

4
@NickW: è qualcosa che devi gestire nella tua applicazione e non nella mappatura. Dal punto di vista della mappatura, le coppie possono giocare due volte (ognuna è ospite e casa una volta).
Ladislav Mrnka,

2
Ho un modello simile. Qual è il modo corretto di gestire l'eliminazione in cascata se una squadra viene rimossa? Ho cercato di creare un trigger INSTEAD OF DELETE ma non sono sicuro se esiste una soluzione migliore? Preferirei gestirlo nel DB, non nell'applicazione.
Woodchipper

1
@mrshickadance: è lo stesso. Un approccio utilizza API fluenti e altre annotazioni di dati.
Ladislav Mrnka,

1
Se uso WillCascadeOnDelete false allora Se voglio eliminare il Team, allora si sta generando un errore. Una relazione dall'associazione 'Team_HomeMatches' è nello stato 'Eliminato'. Dati i vincoli di molteplicità, un corrispondente "Team_HomeMatches_Target" deve anche nello stato "Eliminato".
Rupesh Kumar Tiwari,

55

È anche possibile specificare l' ForeignKey()attributo sulla proprietà di navigazione:

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

In questo modo non è necessario aggiungere alcun codice al OnModelCreatemetodo


4
Ottengo la stessa eccezione in entrambi i casi.
Jo Smo,

11
Questo è il mio modo standard di specificare chiavi esterne che funziona in tutti i casi TRANNE quando un'entità contiene più di una proprietà nav dello stesso tipo (simile allo scenario HomeTeam e GuestTeam), nel qual caso EF viene confuso nel generare l'SQL. La soluzione è aggiungere il codice OnModelCreatesecondo la risposta accettata e le due raccolte per entrambi i lati della relazione.
Steven Manuel,

utilizzo onmodelcreating in tutti i casi tranne il caso citato, utilizzo la chiave esterna di annotazione dei dati e non so perché non sia accettata !!
Hosam Hemaily

48

So che è un post di diversi anni e potresti risolvere il tuo problema con la soluzione sopra. Tuttavia, voglio solo suggerire di utilizzare InverseProperty per qualcuno che ha ancora bisogno. Almeno non è necessario modificare nulla in OnModelCreating.

Il codice seguente non è testato.

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

Puoi leggere ulteriori informazioni su InverseProperty su MSDN: https://msdn.microsoft.com/en-us/data/jj591583?f=255&MSPPError=-2147217396#Relationships


1
Grazie per questa risposta, tuttavia rende le colonne Chiave esterna nulla nella tabella delle corrispondenze.
RobHurd,

Questo ha funzionato perfettamente per me in EF 6, dove erano necessarie raccolte nullable.
Pynt,

Se vuoi evitare api fluenti (per qualsiasi motivo #differente discussione) questo funziona in modo fantastico. Nel mio caso avevo bisogno di includere un'annotazione foriegnKey aggiuntiva sull'entità "Match", perché i miei campi / tabelle hanno stringhe per PK.
Discepolo

1
Questo ha funzionato molto per me. Btw. se non vuoi che le colonne siano nulle, puoi semplicemente specificare la chiave esterna con l'attributo [ForeignKey]. Se la chiave non è nullable allora sei pronto.
Jakub Holovsky,

16

Puoi provare anche questo:

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

Quando si crea una colonna FK che consente NULL, si interrompe il ciclo. Oppure stiamo semplicemente ingannando il generatore di schemi EF.

Nel mio caso, questa semplice modifica risolve il problema.


3
Lettori di attenzione. Sebbene ciò possa aggirare il problema di definizione dello schema, altera la semantica. Probabilmente non è possibile che una partita possa essere disputata senza due squadre.
N8allan,

14

Questo perché le eliminazioni in cascata sono abilitate per impostazione predefinita. Il problema è che quando si chiama un'eliminazione sull'entità, verranno eliminate anche tutte le entità referenziate con il tasto f. Non è necessario rendere nulla 'richiesto' valori annullabili per risolvere questo problema. Un'opzione migliore sarebbe quella di rimuovere la convenzione di eliminazione in cascata di EF Code First:

modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>(); 

Probabilmente è più sicuro indicare esplicitamente quando eseguire un'eliminazione in cascata per ciascuno dei bambini durante la mappatura / configurazione. l'entità.


Cosa succede dopo che questo è stato eseguito? Restrictinvece di Cascade?
Jo Smo,

4

InverseProperty in EF Core rende la soluzione semplice e pulita.

InverseProperty

Quindi la soluzione desiderata sarebbe:

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; }
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.