Después de haber roto el hielo con Code First y también con las migraciones, ahora me propongo hablar sobre las relaciones.
Como ya sabrás, Code First utiliza convenciones para inferir el esquema de base de datos necesario para persistir el modelo. Después de un tiempo trabajando con Code First, tengo la sensación de que las relaciones entre entidades y como se mapean en la base de datos, son las que antes olvido y además me generan más problemas cuando vuelvo a mi código tiempo después.
Siendo así, en este post simplemente quiero mostrar algunos escenarios básicos que ilustren el cómo funcionan las convenciones a tener en cuenta cuando trabajamos con relaciones entre entidades.
Para todos los ejemplos se asumirá una entidad Cliente y una entidad Dirección (que a veces será el lado varios, a veces el lado 1, etc.).
Relación de 1 a varios
class Cliente { public int ClienteId { get; set; } public string NombreCliente { get; set; } public ICollection<Direccion> Direcciones { get; set; } }
[Table("Direcciones")] class Direccion { public int DireccionId { get; set; } public string NombreDireccion { get; set; } } |
En este ejemplo podemos ver que se creó el campo Cliente_ClienteId porque CF no encontró en la tabla Direcciones ningún campo donde poder guardar la clave externa a Clientes. En realidad, CF buscó alguno de los siguientes campos:
· [Target Type Key Name]
· [Target Type Name] + [Target Type Key Name]
Es decir, si hubiera existido la propiedad Direccion.ClienteId o Direccion.ClienteClienteId se hubiera utilizado esa propiedad en vez de crear una nueva. Además, también puedes ver en este ejemplo que no es necesario que una propiedad de navegación sea bidireccional, por ejemplo desde Direccion no se puede navegar a Cliente y no hay ningún problema.
Para solucionar el problema anterior, agregaremos el campo ClienteId a Direccion:
[Table("Direcciones")] class Direccion { public int DireccionId { get; set; } public string NombreDireccion { get; set; } public int ClienteId { get; set; } } |
La verdad es que siempre es una buena idea crear una propiedad de clave externa, además de la propiedad de navegación. Además de para resolver el mapeo, en operaciones de manipulación de datos, podremos simplemente asignar la propiedad de clave externa sin tener que disponer de un objeto para asignar a la propiedad de navegación. Es decir, ¿Qué es más intuitivo? Direccion.ClienteId = 5 o tener que recuperar un cliente y asignarlo a la propiedad de navegación como Direccion.Cliente = unCliente.
Otro escenario que suele ocurrir es que la propiedad de clave externa exista pero su nombre no coincida con ninguna convención. Por ejemplo:
[Table("Direcciones")] class Direccion { public int DireccionId { get; set; } public string NombreDireccion { get; set; } public int ClienteAsociadoId { get; set; } } |
La solución pasa por la utilización del atributo ForeignKey.
class Cliente { public int ClienteId { get; set; } public string NombreCliente { get; set; } [ForeignKey("ClienteAsociadoId")] public ICollection<Direccion> Direcciones { get; set; } } |
Si queremos resolver esto mismo con Fluent API, estamos obligados a crear una propiedad de navegación Cliente en la clase Direccion porque para poder especificar el comportamiento de una relación con Fluent API tenemos que definir la relación entera.
[Table("Direcciones")] class Direccion { public int DireccionId { get; set; } public string NombreDireccion { get; set; } public int ClienteAsociadoId { get; set; } public Cliente Cliente { get; set; } }
class ConvencionesEF : DbContext { public DbSet<Cliente> Clientes { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Entity<Cliente>(). HasMany(p => p.Direcciones). WithRequired(p => p.Cliente). HasForeignKey(p=>p.ClienteAsociadoId); } } |
Ahora que ya hemos introducido la propiedad de navegación Cliente en Direccion, también podemos utilizar DataAnnotations (en vez de Fluent API) para especificar la propiedad de clave externa:
class Cliente
{
public int ClienteId { get; set; }
public string NombreCliente { get; set; }
public ICollection<Direccion> Direcciones { get; set; }
}
[Table("Direcciones")]
class Direccion
{
public int DireccionId { get; set; }
public string NombreDireccion { get; set; }
[ForeignKey("Cliente")]
public int ClienteAsociadoId { get; set; }
public Cliente Cliente { get; set; }
}
También sería válido
[Table("Direcciones")]
class Direccion
{
public int DireccionId { get; set; }
public string NombreDireccion { get; set; }
public int ClienteAsociadoId { get; set; }
[ForeignKey("ClienteAsociadoId")]
public Cliente Cliente { get; set; }
}
Incluso ambas 2 a la vez tampoco hay problema (aunque es redundante):
[Table("Direcciones")]
class Direccion
{
public int DireccionId { get; set; }
public string NombreDireccion { get; set; }
[ForeignKey("Cliente")]
public int ClienteAsociadoId { get; set; }
[ForeignKey("ClienteAsociadoId")]
public Cliente Cliente { get; set; }
}
Relación de muchos a muchos
En esta situación, la tabla intermedia (que no acepta ningún atributo más excepto las claves primarias de ambas tablas), se creará automáticamente por nosotros cuando CF detecte 2 propiedades de navegación de colección entre 2 entidades:
class Cliente { public int ClienteId { get; set; } public string NombreCliente { get; set; } public ICollection<Direccion> }
[Table("Direcciones")] class Direccion { public int DireccionId { get; set; } public string NombreDireccion { get; set; } public ICollection<Cliente> } |
Si queremos controlar la tabla que guardará nuestra relación de varios a varios, así como los nombres de los campos creados, tendremos que utilizar Fluent API:
modelBuilder.Entity<Cliente>(). HasMany(p => p.Direcciones). WithMany(p => p.Clientes) .Map(p => p.ToTable("ClientesDirecciones"). MapLeftKey("ClienteId"). MapRightKey("DireccionId")); |
Relación de 1 a 1
En este tipo de relaciones siempre se requiere configuración extra porque CF no es capaz de inferir quién es el lado principal y quien el lado dependiente en la relación.
Con este código:
class Cliente
{
public int ClienteId { get; set; }
public string NombreCliente { get; set; }
public int DireccionId { get; set; }
public Direccion Direccion { get; set; }
}
[Table("Direcciones")]
class Direccion
{
public int DireccionId { get; set; }
public string NombreDireccion { get; set; }
public int ClienteId { get; set; }
public Cliente Cliente { get; set; }
}
Obtendremos la siguiente excepción:
Para resolver esto, lo más sencillo es utilizar el atributo ForeignKey en la tabla dependiente. Además CF requiere que la propiedad de clave externa sea también clave primaria en la tabla dependiente.
[Table("Direcciones")] class Direccion { public int DireccionId { get; set; } public string NombreDireccion { get; set; } [Key] [ForeignKey("Cliente")] public int? ClienteId { get; set; } public Cliente Cliente { get; set; } } |
Para hacer esto mismo con Fluent API:
modelBuilder.Entity<Direccion>().
HasRequired(p => p.Cliente).
WithRequiredDependent(p => p.Direccion);
Lo cierto es que hay muchos más escenarios que se no cubren en este post, pero como introducción espero te haya servido.
Un saludo!
Me sirvió, muchísimas gracias!!!
ResponderEliminarUn saludo
@2Flores
Esta genial.
ResponderEliminarUn saludo
Gracias, comentarios así animan a seguir escribiendo ;-)
ResponderEliminarMe alegro que os haya servido!
Hola Sergio,
ResponderEliminarMuy instructivo, me están viniendo bien tus posts para conocer EF, pero hay una cosa con la que no estoy muy de acuerdo:
"La verdad es que siempre es una buena idea crear una propiedad de clave externa, además de la propiedad de navegación"
Por una parte me parece que es saltarse el encaspulado de información que hace Cliente (¿qué le importa a dirección cómo se identifica un cliente) y por otro lado me parece peligroso poder llegar a una situación en la que direccion.ClienteId != direccion.Cliente.Id.
Un saludo,
Juanma.
Hola Juanma,
EliminarUn placer que comentes en mi blog ;-)
Yo utilizo las propiedades de clave externa porque a veces no tengo una referencia al objeto dependiente (y tampoco quiero hacer una query a la bd para cargarlo) y sin embargo sí tengo un id (un triste int) con el que puedo hacer la asignación. De hecho, creo recordar que al principio no existían las propiedades de clave externa y la gente las pidió por este mismo escenario que te cuento.
Luego también puede haber ciertos problemas con grafos de objetos, por ejemplo si Cliente está para añadir (estado Added) y luego asigno un objeto existente Direccion a Cliente.Direccion, en algunos escenarios puede ser que EF tome también al objeto Direccion como Added e intente añadirlo también (esto es difícil de explicar en un comentario pero el trabajo con grafos tiene su "aquel"). Sin embargo, si simplemente asigno la propiedad de clave externa, EF no se hará ningún lío.
Lo de ClienteId!=direccion.Cliente.Id, pues es cierto, poder podría pasar :-(
Gracias por comentar.
Comprendo, parece más una limitación de EF que otra cosa.
EliminarEn NHibernate (que es a lo que mejor conozco) el caso de tener un Id y querer ahorrarte la consulta se resuelve con el método Load de la sesión (el DBContext, para entendernos).
Este método crea un proxy del objeto en cuestión pero no toca la BD si no es necesario (es decir, si no se accede a ninguna propiedad).
De esa forma puedes tener sólo la "propiedad de navegación" sin tener una penalización grande en rendimiento (es cierto que crear el proxy es más caro que usar el int, pero despreciable en comparación con tocar la BD).
Lo de los problemas al hacer aplicar persistencia por alcance en el grafo de objetos, las veces que lo he visto (en NH) solía estar relacionado con tener mal definidos los métodos Equals o GetHashCode. No tiene sentido que EF se líe con eso, seguramente sea por un problema similar con la definición de igualdad.
Saludos.
Hola Juanma,
EliminarEn EF no existe ningún concepto similar a los proxies de NH, lo único parecido es el id de la relacionada que sirve para relaciones 1:1 y 1:M. Para relaciones M:M hay que cargar la instancia... http://blogs.taiga.nl/martijn/2011/12/15/entity-framework-4-tips-for-nhibernate-users/
Por otro lado, por lo que tengo leido nunca puede darse direccion.ClienteId != direccion.Cliente.Id ya que la sincronización es automática...
amigo, esta excepcional la explicacion, hay varios post que hablan sobre esto, pero creo que lejos es la mejor explicacion para el tema del manejo de las rlaciones entre tablas.
ResponderEliminarsaludos