Código de marco de entidad primero: dos claves externas de la misma tabla

260

Acabo de empezar a usar el código EF primero, así que soy un principiante en este tema.

Quería crear relaciones entre equipos y partidos:

1 partido = 2 equipos (local, invitado) y resultado.

Pensé que era fácil crear un modelo así, así que comencé a codificar:

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

Y me sale una excepción:

La relación referencial dará como resultado una referencia cíclica que no está permitida. [Nombre de restricción = Match_GuestTeam]

¿Cómo puedo crear un modelo así, con 2 claves foráneas para la misma tabla?

Jarek
fuente

Respuestas:

297

Prueba esto:

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

Las claves primarias se asignan por convención predeterminada. El equipo debe tener dos colecciones de partidos. No puede tener una sola colección referenciada por dos FK. La coincidencia se asigna sin eliminación en cascada porque no funciona en estas auto-referencias de muchos a muchos.

Ladislav Mrnka
fuente
3
¿Qué pasa si dos equipos solo pueden jugar una vez?
ca9163d9
44
@NickW: Eso es algo que debe manejar en su aplicación y no en el mapeo. Desde la perspectiva del mapeo, las parejas pueden jugar dos veces (cada una es invitada y una vez en casa).
Ladislav Mrnka
2
Tengo un modelo similar ¿Cuál es la forma correcta de manejar la eliminación en cascada si se elimina un equipo? Busqué crear un disparador INSTEAD OF DELETE pero no estoy seguro de si hay una mejor solución. Preferiría manejar esto en la base de datos, no en la aplicación.
Woodchipper
1
@mrshickadance: es lo mismo. Un enfoque utiliza API fluidas y otras anotaciones de datos.
Ladislav Mrnka
1
Si uso WillCascadeOnDelete false entonces si quiero eliminar el equipo, entonces está arrojando un error. Una relación del conjunto de asociaciones 'Team_HomeMatches' está en el estado 'Eliminado'. Dadas las restricciones de multiplicidad, un 'Team_HomeMatches_Target' correspondiente también debe estar en el estado 'Eliminado'.
Rupesh Kumar Tiwari
55

También es posible especificar el ForeignKey()atributo en la propiedad de navegación:

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

De esa manera, no necesita agregar ningún código al OnModelCreatemétodo

ShaneA
fuente
44
Tengo la misma excepción de cualquier manera.
Jo Smo
11
Esta es mi forma estándar de especificar claves foráneas que funciona para todos los casos, EXCEPTO cuando una entidad contiene más de una propiedad de navegación del mismo tipo (similar al escenario HomeTeam y GuestTeam), en cuyo caso EF se confunde al generar el SQL. La solución es agregar código OnModelCreatesegún la respuesta aceptada, así como las dos colecciones para ambos lados de la relación.
Steven Manuel
utilizo onmodelcreating en todos los casos, excepto en el caso mencionado, utilizo la clave externa de anotación de datos, ¡además no sé por qué no se acepta!
hosam hemaily
48

Sé que es una publicación de varios años y puede resolver su problema con la solución anterior. Sin embargo, solo quiero sugerir el uso de InverseProperty para alguien que todavía lo necesita. Al menos no necesitas cambiar nada en OnModelCreating.

El siguiente código no se ha probado.

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

Puede leer más sobre InverseProperty en MSDN: https://msdn.microsoft.com/en-us/data/jj591583?f=255&MSPPError=-2147217396#Relationships

khoa_chung_89
fuente
1
Gracias por esta respuesta, sin embargo, hace que las columnas de clave externa se puedan anular en la tabla de coincidencias.
RobHurd
Esto funcionó muy bien para mí en EF 6, donde se necesitaban colecciones anulables.
Pynt
Si desea evitar una API fluida (por cualquier razón #diferente discusión) esto funciona fantásticamente. En mi caso, necesitaba incluir una anotación foriegnKey adicional en la entidad "Match", porque mis campos / tablas tienen cadenas para PK.
DiscipleMichael
1
Esto funcionó mucho para mí. Por cierto. si no desea que las columnas admitan nulos, puede especificar una clave externa con el atributo [ForeignKey]. Si la clave no es anulable, entonces está todo listo.
Jakub Holovsky
16

Puedes probar esto también:

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

Cuando hace que una columna FK permita NULLS, está rompiendo el ciclo. O simplemente estamos engañando al generador de esquemas EF.

En mi caso, esta simple modificación resuelve el problema.

Maico
fuente
3
Precaución lectores. Aunque esto podría solucionar el problema de definición de esquema, altera la semántica. Probablemente no sea el caso que se pueda tener un partido sin dos equipos.
N8allan
14

Esto se debe a que las eliminaciones en cascada están habilitadas de forma predeterminada. El problema es que cuando llama a una eliminación en la entidad, eliminará también cada una de las entidades referenciadas con la tecla f. No debe hacer que los valores 'obligatorios' puedan anularse para solucionar este problema. Una mejor opción sería eliminar la convención de eliminación en cascada de EF Code First:

modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>(); 

Probablemente sea más seguro indicar explícitamente cuándo hacer una eliminación en cascada para cada uno de los elementos secundarios al mapear / configurar. la entidad.

juls
fuente
Entonces, ¿qué es después de que esto se ejecute? Restricten lugar de Cascade?
Jo Smo
4

InverseProperty EF Core hace que la solución sea fácil y limpia.

Propiedad inversa

Entonces la solución deseada sería:

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
fuente