Relación opcional uno a uno usando Entity Framework Fluent API

79

Queremos usar una relación opcional uno a uno usando Entity Framework Code First. Tenemos dos entidades.

public class PIIUser
{
    public int Id { get; set; }

    public int? LoyaltyUserDetailId { get; set; }
    public LoyaltyUserDetail LoyaltyUserDetail { get; set; }
}

public class LoyaltyUserDetail
{
    public int Id { get; set; }
    public double? AvailablePoints { get; set; }

    public int PIIUserId { get; set; }
    public PIIUser PIIUser { get; set; }
}

PIIUserpuede tener un LoyaltyUserDetailpero LoyaltyUserDetaildebe tener un PIIUser. Probamos estas técnicas de enfoque fluido.

modelBuilder.Entity<PIIUser>()
            .HasOptional(t => t.LoyaltyUserDetail)
            .WithOptionalPrincipal(t => t.PIIUser)
            .WillCascadeOnDelete(true);

Este enfoque no creó LoyaltyUserDetailIduna clave externa en la PIIUserstabla.

Después de eso, probamos el siguiente código.

modelBuilder.Entity<LoyaltyUserDetail>()
            .HasRequired(t => t.PIIUser)
            .WithRequiredDependent(t => t.LoyaltyUserDetail);

Pero esta vez EF no creó ninguna clave externa en estas 2 tablas.

¿Tiene alguna idea para este tema? ¿Cómo podemos crear una relación opcional uno a uno utilizando la api fluida del marco de entidad?

İlkay İlknur
fuente

Respuestas:

101

Apoyos 1:1y 1:0..1relaciones de EF Code First . Este último es lo que está buscando ("uno a cero o uno").

Sus intentos de fluidez dicen que es obligatorio en ambos extremos en un caso y opcional en ambos extremos en el otro.

Lo que necesita es opcional en un extremo y obligatorio en el otro.

Aquí hay un ejemplo del libro Programming EF Code First

modelBuilder.Entity<PersonPhoto>()
.HasRequired(p => p.PhotoOf)
.WithOptional(p => p.Photo);

La PersonPhotoentidad tiene una propiedad de navegación llamada PhotoOfque apunta a un Persontipo. El Persontipo tiene una propiedad de navegación llamada Photoque apunta al PersonPhototipo.

En las dos clases relacionadas, usa la clave principal de cada tipo , no las claves externas . es decir, no utilizará las propiedades LoyaltyUserDetailIdo PIIUserId. En cambio, la relación depende de los Idcampos de ambos tipos.

Si está utilizando la API fluida como se indicó anteriormente, no necesita especificar LoyaltyUser.Idcomo una clave externa, EF lo resolverá.

Entonces, sin tener su código para probarme a mí mismo (odio hacer esto desde mi cabeza) ... traduciría esto a su código como

public class PIIUser
{
    public int Id { get; set; }    
    public LoyaltyUserDetail LoyaltyUserDetail { get; set; }
}

public class LoyaltyUserDetail
{
    public int Id { get; set; }
    public double? AvailablePoints { get; set; }    
    public PIIUser PIIUser { get; set; }
}

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
  modelBuilder.Entity<LoyaltyUserDetail>()
  .HasRequired(lu => lu.PIIUser )
  .WithOptional(pi => pi.LoyaltyUserDetail );
}

Eso significa que la PIIUserpropiedad LoyaltyUserDetails es obligatoria y la LoyaltyUserDetailpropiedad PIIUser es opcional.

Podrías empezar desde el otro extremo:

modelBuilder.Entity<PIIUser>()
.HasOptional(pi => pi.LoyaltyUserDetail)
.WithRequired(lu => lu.PIIUser);

que ahora dice que la LoyaltyUserDetailpropiedad de PIIUser es opcional y la PIIUserpropiedad de LoyaltyUser es obligatoria.

Siempre tienes que usar el patrón TIENE / CON.

Las relaciones HTH y FWIW, uno a uno (o uno a cero / uno) son una de las relaciones más confusas para configurar primero en el código, ¡así que no estás solo! :)

Julie Lerman
fuente
1
¿No limita eso al usuario en qué campo FK elegir? (Creo que quiere eso, pero no ha informado, así que no lo sé), ya que parece que quiere tener un campo de FK presente en la clase.
Frans Bouma
1
Convenido. Es una limitación de cómo funciona EF. 1: 1 y 1: 0..1 dependen de las claves primarias. De lo contrario, creo que se está desviando hacia el "FK único" que todavía no es compatible con EF. :( (Eres más un experto en db ... ¿es correcto ... esto realmente se trata de un FK único?) Y no va a estar en el próximo Ef6 según: entityframework.codeplex.com/workitem/299
Julie Lerman
1
Sí @FransBouma, queríamos usar los campos PIIUserId y LoyaltUserId como claves externas. Pero EF nos limita en esta situación, como usted y Julie mencionaron. Gracias por las respuestas.
İlkay İlknur
@JulieLerman ¿por qué usas WithOptional? ¿Cuál es la diferencia con WithRequiredDependenty WithRequiredOptional?
Willy
1
WithOptional apunta a una relación que es opcional, por ejemplo 0..1 (cero o uno) porque el OP dijo "PIIUser puede tener un LoyaltyUserDetail". Y su confusión que lleva a la segunda pregunta es porque se equivocó en uno de los términos. ;) Aquellos son WithRequiredDependent y WithRequiredPrincipal. ¿Eso tiene más sentido? Apuntando a un fin requerido que es el dependiente (también conocido como "hijo") o el principal también conocido como "padre". Incluso en una relación uno a uno donde ambos son iguales, EF necesita referirse a uno como principal y uno como dependiente. HTH!
Julie Lerman
27

Simplemente haz como si tuvieras una relación de uno a muchos entre ellos LoyaltyUserDetaily PIIUsertu mapeo debería ser

modelBuilder.Entity<LoyaltyUserDetail>()
       .HasRequired(m => m.PIIUser )
       .WithMany()
       .HasForeignKey(c => c.LoyaltyUserDetailId);

EF debería crear todas las claves externas que necesita y simplemente no le importa WithMany .

jr de Yaoundé
fuente
3
La respuesta de Julie Lerman fue aceptada (y debería permanecer aceptada, en mi humilde opinión) porque responde a la pregunta y detalla por qué es la forma correcta con detalles de respaldo y discusión. Las respuestas de copiar y pegar ciertamente están disponibles en todo SO y yo mismo he usado algunas, pero como desarrollador profesional, debería preocuparse más por convertirse en un mejor programador, no solo por obtener el código para compilar. Dicho esto, jr lo hizo bien, aunque un año después.
Mike Devenney
1
@ClickOk Sé que esta es una vieja pregunta y comentario, pero esta no es la respuesta correcta porque usó uno a muchos como solución alternativa, esto no crea una relación uno a uno o uno a cero
Mahmoud Darwish
3

Hay varios problemas con su código.

Una relación 1: 1 es: PK <-PK , donde un lado PK es también un FK, o PK <-FK + UC , donde el lado FK no es PK y tiene UC. Su código muestra que tiene FK <-FK , ya que define ambos lados para tener un FK, pero eso está mal. Recon PIIUseres el lado PK y LoyaltyUserDetailes el lado FK. Esto significa PIIUserque no tiene un campo FK, pero lo LoyaltyUserDetailtiene.

Si el 1: 1 es opcional, el lado FK debe tener al menos 1 campo anulable.

pswg anterior respondió a su pregunta, pero cometió un error de que también definió un FK en PIIUser, que por supuesto es incorrecto como describí anteriormente. Así que defina el campo FK anulable en LoyaltyUserDetail, defina el atributo en LoyaltyUserDetailpara marcarlo como el campo FK, pero no especifique un campo FK en PIIUser.

Obtiene la excepción que describe arriba debajo de la publicación de pswg, porque ningún lado es el lado PK (final del principio).

EF no es muy bueno en 1: 1 ya que no puede manejar restricciones únicas. No soy un experto en Code primero, así que no sé si es capaz de crear una UC o no.

(editar) por cierto: A 1: 1 B (FK) significa que solo hay 1 restricción FK creada, en el objetivo de B apuntando al PK de A, no a 2.

Frans Bouma
fuente
3
public class User
{
    public int Id { get; set; }
    public int? LoyaltyUserId { get; set; }
    public virtual LoyaltyUser LoyaltyUser { get; set; }
}

public class LoyaltyUser
{
    public int Id { get; set; }
    public virtual User MainUser { get; set; }
}

        modelBuilder.Entity<User>()
            .HasOptional(x => x.LoyaltyUser)
            .WithOptionalDependent(c => c.MainUser)
            .WillCascadeOnDelete(false);

esto resolverá el problema de REFERENCIA y LLAVES EXTRANJERAS

al ACTUALIZAR o BORRAR un registro

pampi
fuente
Esto no funcionará como se esperaba. Da como resultado un código de migraciones que no usa LoyaltyUserId como clave externa, por lo que User.Id y LoyaltyUser.Id terminarán con el mismo valor, y LoyaltyUserId se dejará vacío.
Aaron Queenan
2

Intente agregar el ForeignKeyatributo a la LoyaltyUserDetailpropiedad:

public class PIIUser
{
    ...
    public int? LoyaltyUserDetailId { get; set; }
    [ForeignKey("LoyaltyUserDetailId")]
    public LoyaltyUserDetail LoyaltyUserDetail { get; set; }
    ...
}

Y la PIIUserpropiedad:

public class LoyaltyUserDetail
{
    ...
    public int PIIUserId { get; set; }
    [ForeignKey("PIIUserId")]
    public PIIUser PIIUser { get; set; }
    ...
}
pswg
fuente
1
Probamos las anotaciones de datos antes de todos estos enfoques fluidos de API. Pero las anotaciones de datos no funcionaron. si agrega las anotaciones de datos que mencionó anteriormente, EF lanza esta excepción => No se puede determinar el final principal de una asociación entre los tipos 'LoyaltyUserDetail' y 'PIIUser'. El extremo principal de esta asociación debe configurarse explícitamente utilizando la API fluida de relación o las anotaciones de datos.
İlkay İlknur
@ İlkayİlknur ¿Qué sucede si agrega el ForeignKeyatributo a solo un extremo de la relación? es decir, solo en PIIUser o LoyaltyUserDetail .
pswg
Ef lanza la misma excepción.
İlkay İlknur
1
@pswg ¿Por qué FK en PIIUser? LoyaltyUserDetail tiene un FK, no un PIIUser. Por lo tanto, debe ser [Key, ForeignKey ("PIIUser")] en la propiedad PIIUserId de LoyaltyUserDetail. Pruebe esto
user808128
@ user808128 Puede colocar la anotación en la propiedad de navegación o en el ID, no hay diferencia.
Thomas Boby
1

Lo único que resulta confuso con las soluciones anteriores es que la clave principal se define como " Id" en ambas tablas y si tiene una clave principal basada en el nombre de la tabla, no funcionaría, he modificado las clases para ilustrar lo mismo, es decir, la tabla opcional no debería definir su propia clave primaria, sino que debería utilizar el mismo nombre de clave de la tabla principal.

public class PIIUser
{
    // For illustration purpose I have named the PK as PIIUserId instead of Id
    // public int Id { get; set; }
    public int PIIUserId { get; set; }

    public int? LoyaltyUserDetailId { get; set; }
    public LoyaltyUserDetail LoyaltyUserDetail { get; set; }
}

public class LoyaltyUserDetail
{
    // Note: You cannot define a new Primary key separately as it would create one to many relationship
    // public int LoyaltyUserDetailId { get; set; }

    // Instead you would reuse the PIIUserId from the primary table, and you can mark this as Primary Key as well as foreign key to PIIUser table
    public int PIIUserId { get; set; } 
    public double? AvailablePoints { get; set; }

    public int PIIUserId { get; set; }
    public PIIUser PIIUser { get; set; }
}

Y luego seguido de

modelBuilder.Entity<PIIUser>()
.HasOptional(pi => pi.LoyaltyUserDetail)
.WithRequired(lu => lu.PIIUser);

Haría el truco, la solución aceptada no explica claramente esto, y me confundió durante unas horas para encontrar la causa

skjagini
fuente
1

Esto no es de utilidad para el póster original, pero para cualquiera que todavía esté en EF6 y necesite que la clave externa sea diferente de la clave principal, aquí se explica cómo hacerlo:

public class PIIUser
{
    public int Id { get; set; }

    //public int? LoyaltyUserDetailId { get; set; }
    public LoyaltyUserDetail LoyaltyUserDetail { get; set; }
}

public class LoyaltyUserDetail
{
    public int Id { get; set; }
    public double? AvailablePoints { get; set; }

    public int PIIUserId { get; set; }
    public PIIUser PIIUser { get; set; }
}

modelBuilder.Entity<PIIUser>()
    .HasRequired(t => t.LoyaltyUserDetail)
    .WithOptional(t => t.PIIUser)
    .Map(m => m.MapKey("LoyaltyUserDetailId"));

Tenga en cuenta que no puede usar el LoyaltyUserDetailIdcampo porque, por lo que puedo decir, solo se puede especificar usando la API fluida. (He probado tres formas de hacerlo usando elForeignKey atributo y ninguna funcionó).

Aaron Queenan
fuente