En una aplicación orientada a objetos, los objetos solo viven en memoria y al momento de persistirlos en una base de datos relacional necesitamos transformar estos objetos en registros de una tabla. En este articulo vamos a ver específicamente como JPA nos brinda anotaciones para mapear atributos de objetos a relaciones de cardinalidad que se necesitan en un esquema de base de datos relacional.
De esta manera podemos abstraernos de nuestro esquema de base de datos, y centrarnos en nuestro modelo de objetos, pudiendo relacionar tanto atributos de un solo objeto, como atributos de colecciones tan solo con una anotación. En JPA tenemos 4 anotaciones para mapear las relaciones de cardinalidad.
-
Relación Uno a Uno (@OneToOne)
Partimos de nuestro modelo de objetos, donde tenemos una clase Equipo, con un atributo Entrenador. Como un Entrenador solo puede pertenecer a un Club, y éste va a tener solo un Entrenador, utilizaremos la relación @OneToOne.
@Entity
@Table(name = "clubes")
public class Club {
@Id
private Long id;
@OneToOne
private Entrenador entrenador;
}
@Entity
@Table(name = "entrenadores")
public class Entrenador {
@Id
private Long id;
private String nombre;
private String apellido;
private int edad;
private String nacionalidad;
}
Como podemos ver, en el modelo de objetos, el Entrenador no conoce al Club al cual pertenece y para no tener una relación bidirecional, decidimos que quien sea el propietario de la relación sea el Club, ya que un Entrenador no tiene razón de existir por si solo en nuestro dominio, sino que depende de la existencia de un Club. Pero a su vez, tiene la suficiente relevancia para ser una entidad en nuestro modelo.
Una vez realizado el mapeo, en el modelo de datos tenemos una relación de uno a uno entre un Club de futbol y su Entrenador, donde un club va a tener solo un Entrenador, y un Entrenador va a pertenecer solo a un Club. En esta relación, el propietario de la misma es el Club ya que tiene la FK al Entrenador.
-
Relación Uno a Muchos (@OneToMany)
Ahora a nuestro club vamos a agregarle el plantel de jugadores. En nuestro modelo de objetos vamos a tener un una lista de tipo Jugador perteneciente a la clase Club. Esto nos permite tener una colección de jugadores que van a pertenecer solo a un club, y por tal motivo podemos utilizar @OneToMany.
@Entity
@Table(name = "clubes")
public class Club {
@Id
private Long id;
@OneToOne
private Entrenador entrenador;
@OneToMany
private List<Jugador> jugadores;
}
@Entity
@Table(name = "jugadores")
public class Jugador {
@Id
private Long id;
private String nombre;
private String apellido;
private int numero;
private String posicion;
}
Si vemos el modelo de datos, podemos observar que se creo una nueva tabla "jugadores" y una tabla intermedia "clubes_jugadores". Al ser una relación de uno a muchos, pensando solamente nuestro diseño de datos, hubiésemos esperado tener una FK de la entidad perteneciente a la relación del "muchos", referenciando al id de la entidad del "uno". Es decir, tener un campo id_equipo en la tabla "jugador" referenciando a la tabla "club". Pero al no especificar nada, JPA entiende esta relación con "muchos a muchos" y nos crea ésta tabla intermedia.
Para evitar la creación de la tabla intermedia, y que la la tabla "jugador" tenga la FK a "club", debemos agregar la annotation @JoinColumn a nuestra lista de jugadores, con una propiedad "name" en la cual vamos a indicar como se llamara el campo que hace referencia a la tabla Club. Naturalmente podríamos pensar en que es necesario hacer explicito el campo al cual hace referencia, y se puede hacer con la propiedad referencedColumnName, pero por lo general no se suele utilizar ya que JPA infiere que la columna es el ID de la entidad referenciada. Esta annotation tiene otras propiedades, pero en este caso solo necesitamos la mencionada y dejaremos las demás para artículos posteriores.
@Entity
@Table(name = "clubes")
public class Club {
@Id
private Long id;
@OneToOne
private Entrenador entrenador;
@OneToMany
@JoinColumn(name = "id_club")
private List<Jugador> jugadores;
}
Ahora si, nuestro modelo de datos solo tendrá las tablas maestras relacionadas por la FK, sin necesidad de una tabla intermedia.
-
Relación Muchos a Uno (@ManyToOne)
Todo club de futbol, pertenece a una asociacion determinada, como puede ser River Plate pertenece a la AFA (Asociacion de Futbol Argentino), y ademas, una asociacion puede tener muchos Club afiliados. Para solucionar esta necesidad, podemos utilizar la relacion @ManyToOne.
@Entity
@Table(name = "clubes")
public class Club {
@Id
private Long id;
@OneToOne
private Entrenador entrenador;
@OneToMany
@JoinColumn(name = "id_club")
private List<Jugador> jugadores
@ManyToOne
private Asociacion asociacion;
}
@Entity
@Table(name = "asociaciones")
public class Asociacion {
@Id
private Long id;
private String nombre;
private String pais;
private String presidente;
}
De esta manera vamos a definir que un Club pertenece a una Asociación, y una Asociaciónpuede pertenecer a muchos Clubes. Tan solo agregando esta annotation, en nuestro modelo de datos se va a crear un campo "asociacion_id" en la tabla "clubes", haciendo referencia como FK a la tabla "asociaciones". De igual manera que la relacion anterior, podriamos personalizar el campo que se creara, con la annotation @JoinColumn, poniendo el nombre que queramos en la propiedad name.
-
Relación Muchos a Muchos (@ManyToMany)
Por ultimo, queremos modelar en nuestro dominio de objetos, las competiciones en las que participan todos los clubes. De este modo podemos definir que un Club puede participar en muchas Competiciones (como por ejemplo el club Boca Juniors participa de la Superliga Argentina, Copa Argentina y Copa Libertadores), y una competencia obviamente va a tener muchos Clubes participantes. Así es como utilizaremos la ultima relación que veremos, y es @ManyToMany.
@Entity
@Table(name = "clubes")
public class Club {
@Id
private Long id;
@OneToOne
private Entrenador entrenador;
@OneToMany
@JoinColumn(name = "id_club")
private List<Jugador> jugadores
@ManyToOne
private Asociacion asociacion;
@ManyToMany
private List<Competicion> competiciones;
}
@Entity
@Table(name = "competiciones")
public class Competicion {
@Id
private Long id;
private String nombre;
private int montoPremio;
private LocalDate fechaInicio;
private LocalDate fechaFin
}
Decidimos agregar en nuestra clase Club una lista de Competiciones en la que participa, aunque de igual modo podríamos haber decidido agregar una lista de Clubes en la Competicion, pero trataremos de evitar relaciones bidireccionales.
En nuestro modelo de datos, podemos ver que se crea una nueva tabla "clubes_competiciones" con referencia a los IDs de las dos tablas relacionadas. Esta tabla no tiene entidad por si misma, ya que solo nos sirve para romper con la relación de muchos a muchos, a diferencia de una tabla intermedia la cual cada registro tiene un id y posiblemente otros datos que no dependen de las tablas relacionadas.
De la misma manera, podríamos cambiar la relación en nuestro modelo de objetos, agregando una lista de Clubes participantes en cada instancia de una Competicion, y nuestro esquema de base de datos será igual independientemente de que como modelemos en objetos.
@Entity
@Table(name = "clubes")
public class Club {
@Id
private Long id;
@OneToOne
private Entrenador entrenador;
@OneToMany
@JoinColumn(name = "id_club")
private List<Jugador> jugadores
@ManyToOne
private Asociacion asociacion;
}
@Entity
@Table(name = "competiciones")
public class Competicion {
@Id
private Long id;
private String nombre;
private int montoPremio;
private LocalDate fechaInicio;
private LocalDate fechaFin
@ManyToMany
private List<Club> clubes;
}
Luego de éste breve repaso sobre las relaciones de cardinalidad con JPA, podemos concluir que con tal solo unas pocas anotaciones nos podemos abstraer de la construcción de nuestro modelo de datos, ya que solo nosotros debemos ocuparnos de hacer los mapeos y JPA se encargara del resto. Además, notamos que nuestro modelo de objetos no necesita relaciones bidireccionales para poder realizar relaciones entre muchos registros, ya que solo nos alcanza con agregar el mappeo en los atributos y así nos evitamos relaciones bidireccionales que nos pueden traer complicaciones a nivel objetos.