Entity Framework Code First - deux clés étrangères de la même table

260

Je viens de commencer à utiliser le code EF en premier, donc je suis un débutant total dans cette rubrique.

Je voulais créer des relations entre les équipes et les matchs:

1 match = 2 équipes (domicile, invité) et résultat.

J'ai pensé qu'il était facile de créer un tel modèle, alors j'ai commencé à coder:

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

Et je reçois une exception:

La relation référentielle se traduira par une référence cyclique qui n'est pas autorisée. [Nom de la contrainte = Match_GuestTeam]

Comment puis-je créer un tel modèle, avec 2 clés étrangères vers la même table?

Jarek
la source

Réponses:

297

Essaye ça:

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

Les clés primaires sont mappées par convention par défaut. L'équipe doit avoir deux collections de matchs. Vous ne pouvez pas avoir une seule collection référencée par deux FK. La correspondance est mappée sans suppression en cascade car elle ne fonctionne pas dans ces plusieurs à plusieurs auto-référencés.

Ladislav Mrnka
la source
3
Et si deux équipes sont autorisées à jouer une seule fois?
ca9163d9
4
@NickW: C'est quelque chose que vous devez gérer dans votre application et non dans le mappage. Du point de vue de la cartographie, les paires sont autorisées à jouer deux fois (chacune est invitée et une fois à la maison).
Ladislav Mrnka
2
J'ai un modèle similaire. Quelle est la bonne façon de gérer la suppression en cascade si une équipe est supprimée? J'ai cherché à créer un déclencheur INSTEAD OF DELETE mais je ne sais pas s'il existe une meilleure solution? Je préférerais gérer cela dans la base de données, pas dans l'application.
Woodchipper
1
@mrshickadance: C'est la même chose. Une approche utilise une API fluide et une autre annotation de données.
Ladislav Mrnka
1
Si j'utilise WillCascadeOnDelete false, alors si je veux supprimer l'équipe, alors c'est une erreur de lancement. Une relation de l'ensemble d'association 'Team_HomeMatches' est à l'état 'Supprimé'. Compte tenu des contraintes de multiplicité, un «Team_HomeMatches_Target» correspondant doit également être à l'état «Supprimé».
Rupesh Kumar Tiwari
55

Il est également possible de spécifier l' ForeignKey()attribut sur la propriété de navigation:

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

De cette façon, vous n'avez pas besoin d'ajouter de code à la OnModelCreateméthode

ShaneA
la source
4
Je reçois la même exception de toute façon.
Jo Smo
11
C'est ma façon standard de spécifier des clés étrangères qui fonctionne dans tous les cas SAUF lorsqu'une entité contient plus d'une propriété nav du même type (similaire au scénario HomeTeam et GuestTeam), auquel cas EF est confus lors de la génération du SQL. La solution consiste à ajouter du code OnModelCreateselon la réponse acceptée ainsi que les deux collections pour les deux côtés de la relation.
Steven Manuel
j'utilise onmodelcreating dans tous les cas sauf le cas mentionné, j'utilise la clé étrangère d'annotation de données, ainsi je ne sais pas pourquoi elle n'est pas acceptée !!
hosam hemaily
48

Je sais que c'est un poste vieux de plusieurs années et vous pouvez résoudre votre problème avec la solution ci-dessus. Cependant, je veux juste suggérer d'utiliser InverseProperty pour quelqu'un qui en a encore besoin. Au moins, vous n'avez rien à changer dans OnModelCreating.

Le code ci-dessous n'est pas testé.

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

Vous pouvez en savoir plus sur InverseProperty sur MSDN: https://msdn.microsoft.com/en-us/data/jj591583?f=255&MSPPError=-2147217396#Relationships

khoa_chung_89
la source
1
Merci pour cette réponse, mais cela rend les colonnes de clé étrangère nullables dans la table de correspondance.
RobHurd
Cela a très bien fonctionné pour moi dans EF 6 où des collections nullables étaient nécessaires.
Pynt
Si vous voulez éviter une API fluide (pour quelque raison que ce soit #differentdiscussion), cela fonctionne de manière fantastique. Dans mon cas, je devais inclure une annotation foriegnKey supplémentaire sur l'entité "Match", car mes champs / tables ont des chaînes pour les PK.
DiscipleMichael
1
Cela a beaucoup fonctionné pour moi. Btw. si vous ne voulez pas que les colonnes puissent être annulées, vous pouvez simplement spécifier la clé étrangère avec l'attribut [ForeignKey]. Si la clé n'est pas annulable, alors vous êtes prêt.
Jakub Holovsky
16

Vous pouvez également essayer ceci:

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

Lorsque vous faites une colonne FK autoriser NULLS, vous rompez le cycle. Ou nous trichons simplement le générateur de schéma EF.

Dans mon cas, cette simple modification résout le problème.

Maico
la source
3
Attention lecteurs. Bien que cela puisse contourner le problème de définition de schéma, cela modifie la sémantique. Ce n'est probablement pas le cas qu'un match puisse avoir lieu sans deux équipes.
N8allan
14

En effet, les suppressions en cascade sont activées par défaut. Le problème est que lorsque vous appelez une suppression sur l'entité, elle supprimera également chacune des entités référencées par la touche f. Vous ne devez pas rendre les valeurs «requises» nulles pour résoudre ce problème. Une meilleure option serait de supprimer la convention de suppression en cascade d'EF Code First:

modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>(); 

Il est probablement plus sûr d'indiquer explicitement quand effectuer une suppression en cascade pour chacun des enfants lors du mappage / de la configuration. l'entité.

juls
la source
Alors qu'est-ce que c'est après que cela soit exécuté? Restrictau lieu de Cascade?
Jo Smo
4

InverseProperty dans EF Core rend la solution simple et propre.

InverseProperty

La solution souhaitée serait donc:

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
la source