Entity Framework
Relación de mapeo con Entity Framework Code First: One-to-one y variaciones
Buscar..
Introducción
Este tema explica cómo asignar relaciones de tipo uno a uno mediante Entity Framework.
Mapeo de uno a cero o uno
Así que digamos de nuevo que tienes el siguiente modelo:
public class Person
{
public int PersonId { get; set; }
public string Name { get; set; }
}
public class Car
{
public int CarId { get; set; }
public string LicensePlate { get; set; }
}
public class MyDemoContext : DbContext
{
public DbSet<Person> People { get; set; }
public DbSet<Car> Cars { get; set; }
}
Y ahora desea configurarlo para que pueda expresar la siguiente especificación: una persona puede tener uno o cero automóviles, y cada automóvil pertenece exactamente a una persona (las relaciones son bidireccionales, por lo tanto, si CarA pertenece a PersonA, entonces PersonA posee 'CarA).
Así que modifiquemos un poco el modelo: agregue las propiedades de navegación y las propiedades de clave externa:
public class Person
{
public int PersonId { get; set; }
public string Name { get; set; }
public int CarId { get; set; }
public virtual Car Car { get; set; }
}
public class Car
{
public int CarId { get; set; }
public string LicensePlate { get; set; }
public int PersonId { get; set; }
public virtual Person Person { get; set; }
}
Y la configuración:
public class CarEntityTypeConfiguration : EntityTypeConfiguration<Car>
{
public CarEntityTypeConfiguration()
{
this.HasRequired(c => c.Person).WithOptional(p => p.Car);
}
}
En este momento esto debería ser autoexplicativo. El auto tiene una persona requerida ( HasRequired () ), y la persona tiene un auto opcional ( WithOptional () ). Nuevamente, no importa de qué lado configures esta relación, solo ten cuidado cuando uses la combinación correcta de Has / With y Required / Optional. Desde el lado de la Person
, se vería así:
public class PersonEntityTypeConfiguration : EntityTypeConfiguration<Person>
{
public PersonEntityTypeConfiguration()
{
this.HasOptional(p => p.Car).WithOptional(c => c.Person);
}
}
Ahora vamos a ver el esquema de db:
Mire detenidamente: puede ver que no hay FK en People
para referirse a Car
. Además, el FK en Car
no es el PersonId
, sino el CarId
. Aquí está el script real para el FK:
ALTER TABLE [dbo].[Cars] WITH CHECK ADD CONSTRAINT [FK_dbo.Cars_dbo.People_CarId] FOREIGN KEY([CarId])
REFERENCES [dbo].[People] ([PersonId])
Por lo tanto, esto significa que las propiedades clave de CarId
y PersonId
foregn que tenemos en el modelo se ignoran básicamente. Están en la base de datos, pero no son claves externas, como podría esperarse. Esto se debe a que las asignaciones uno a uno no admiten agregar el FK a su modelo EF. Y eso se debe a que las asignaciones uno a uno son bastante problemáticas en una base de datos relacional.
La idea es que cada persona puede tener exactamente un automóvil, y ese automóvil solo puede pertenecer a esa persona. O puede haber registros personales, que no tienen automóviles asociados con ellos.
Entonces, ¿cómo podría ser representado con claves externas? Obviamente, podría haber un PersonId
en el Car
y un CarId
en las People
. Para imponer que cada persona pueda tener solo un auto, PersonId
tendría que ser único en Car
. Pero si PersonId
es único en People
, ¿cómo puede agregar dos o más registros donde PersonId
es NULL
(más de un auto que no tiene dueño)? Respuesta: no puede (bueno, en realidad, puede crear un índice único filtrado en SQL Server 2008 y más reciente, pero olvidemos este tecnicismo por un momento; por no mencionar otros RDBMS). Sin mencionar el caso donde se especifican ambos extremos de la relación ...
La única forma real de hacer cumplir esta regla si las tablas de People
y Car
tienen la clave principal 'misma' (los mismos valores en los registros conectados). Y para hacer esto, CarId
in Car
debe ser tanto PK como FK para PK de People. Y esto hace que todo el esquema sea un desastre. Cuando uso esto prefiero nombrar el PK / FK en Car
PersonId
, y lo configuro en consecuencia:
public class Person
{
public int PersonId { get; set; }
public string Name { get; set; }
public virtual Car Car { get; set; }
}
public class Car
{
public string LicensePlate { get; set; }
public int PersonId { get; set; }
public virtual Person Person { get; set; }
}
public class CarEntityTypeConfiguration : EntityTypeConfiguration<Car>
{
public CarEntityTypeConfiguration()
{
this.HasRequired(c => c.Person).WithOptional(p => p.Car);
this.HasKey(c => c.PersonId);
}
}
No ideal, pero tal vez un poco mejor. Aún así, debes estar alerta cuando utilices esta solución, porque va en contra de las convenciones de nombres habituales, lo que podría desviarte. Aquí está el esquema generado a partir de este modelo:
Entonces, esta relación no es impuesta por el esquema de la base de datos, sino por Entity Framework. Es por eso que debes tener mucho cuidado al usar esto, no permitir que nadie se enoje directamente con la base de datos.
Mapeo uno a uno
Mapear uno a uno (cuando se requieren ambos lados) también es algo complicado.
Imaginemos cómo podría representarse esto con claves foráneas. De nuevo, un CarId
in People
que se refiere a CarId
in Car
, y un PersonId
in Car que se refiere al PersonId
in People
.
Ahora, ¿qué pasa si quieres insertar un registro de coche? Para que esto tenga éxito, debe haber un PersonId
especificado en este registro de auto, porque es necesario. Y para que este PersonId
sea válido, debe existir el registro correspondiente en People
. OK, entonces sigamos adelante e insertemos el registro de persona. Pero para que esto CarId
éxito, un CarId
válido debe estar en el registro de la persona, ¡pero el auto aún no está insertado! No puede ser, porque primero tenemos que insertar el registro de la persona referida. Pero no podemos insertar el registro de la persona referida, porque se refiere al registro del automóvil, por lo que debe insertarse primero (clave-clave extranjera :)).
Entonces, esto tampoco puede ser representado de la manera 'lógica'. De nuevo, tienes que soltar una de las claves foráneas. La que dejes caer depende de ti. El lado que queda con una clave externa se llama "dependiente", el lado que queda sin una clave externa se llama "principal". Y nuevamente, para garantizar la singularidad en el dependiente, el PK debe ser el FK, por lo que no se admite la adición de una columna FK ni la importación a su modelo.
Así que aquí está la configuración:
public class CarEntityTypeConfiguration : EntityTypeConfiguration<Car>
{
public CarEntityTypeConfiguration()
{
this.HasRequired(c => c.Person).WithRequiredDependent(p => p.Car);
this.HasKey(c => c.PersonId);
}
}
En este momento, realmente debería haber obtenido la lógica de la misma :) Solo recuerde que también puede elegir el otro lado, solo tenga cuidado de usar las versiones Dependiente / Principal de WithRequired (y todavía tiene que configurar la PK en el automóvil).
public class PersonEntityTypeConfiguration : EntityTypeConfiguration<Person>
{
public PersonEntityTypeConfiguration()
{
this.HasRequired(p => p.Car).WithRequiredPrincipal(c => c.Person);
}
}
Si verifica el esquema de DB, encontrará que es exactamente igual que en el caso de la solución uno a uno o cero. Eso es porque, de nuevo, esto no se aplica por el esquema, sino por el propio EF. Así que de nuevo, ten cuidado :)
Mapeo de uno o cero a uno o cero
Y para terminar, veamos brevemente el caso cuando ambos lados son opcionales.
A estas alturas, debería estar realmente aburrido de estos ejemplos :), así que no voy a entrar en detalles y jugar con la idea de tener dos FK-s y los posibles problemas y le advierto sobre los peligros de no aplicar estas reglas en el esquema pero solo en EF mismo.
Aquí está la configuración que necesita aplicar:
public class CarEntityTypeConfiguration : EntityTypeConfiguration<Car>
{
public CarEntityTypeConfiguration()
{
this.HasOptional(c => c.Person).WithOptionalPrincipal(p => p.Car);
this.HasKey(c => c.PersonId);
}
}
De nuevo, también puede configurar desde el otro lado, solo tenga cuidado de usar los métodos correctos :)