¿Cómo mapear una clave compuesta con JPA e Hibernate?
En este código, cómo generar una clase Java para la clave compuesta (cómo componer la clave en hibernación):
create table Time (
levelStation int(15) not null,
src varchar(100) not null,
dst varchar(100) not null,
distance int(15) not null,
price int(15) not null,
confPathID int(15) not null,
constraint ConfPath_fk foreign key(confPathID) references ConfPath(confPathID),
primary key (levelStation, confPathID)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Para asignar una clave compuesta, puede utilizar las anotaciones EmbeddedId
oIdClass
. Sé que esta pregunta no se trata estrictamente de JPA, pero también se aplican las reglas definidas por la especificación. Así que aquí están:
2.1.4 Claves primarias e identidad de la entidad
...
Una clave primaria compuesta debe corresponder a un único campo o propiedad persistente o a un conjunto de dichos campos o propiedades como se describe a continuación. Se debe definir una clase de clave primaria para representar una clave primaria compuesta. Las claves primarias compuestas suelen surgir cuando se asignan desde bases de datos heredadas cuando la clave de la base de datos se compone de varias columnas. Las anotaciones
EmbeddedId
yIdClass
se utilizan para indicar claves primarias compuestas. Ver secciones 9.1.14 y 9.1.15....
Se aplican las siguientes reglas para las claves primarias compuestas:
- La clase de clave principal debe ser pública y debe tener un constructor público sin argumentos.
- Si se utiliza el acceso basado en propiedades, las propiedades de la clase de clave principal deben ser públicas o protegidas.
- La clase de clave principal debe ser
serializable
.- La clase de clave principal debe definir
equals
yhashCode
métodos. La semántica de igualdad de valores para estos métodos debe ser coherente con la igualdad de la base de datos para los tipos de bases de datos a los que está asignada la clave.- Una clave primaria compuesta debe representarse y asignarse como una clase integrable (consulte la Sección 9.1.14, “Anotación EmbeddedId”) o debe representarse y asignarse a múltiples campos o propiedades de la clase de entidad (consulte la Sección 9.1.15, “IdClass Anotación").
- Si la clase de clave primaria compuesta se asigna a múltiples campos o propiedades de la clase de entidad, los nombres de los campos o propiedades de clave primaria en la clase de clave primaria y los de la clase de entidad deben corresponder y sus tipos deben ser los mismos.
Con unIdClass
La clase para la clave primaria compuesta podría verse así (podría ser una clase interna estática):
public class TimePK implements Serializable {
protected Integer levelStation;
protected Integer confPathID;
public TimePK() {}
public TimePK(Integer levelStation, Integer confPathID) {
this.levelStation = levelStation;
this.confPathID = confPathID;
}
// equals, hashCode
}
Y la entidad:
@Entity
@IdClass(TimePK.class)
class Time implements Serializable {
@Id
private Integer levelStation;
@Id
private Integer confPathID;
private String src;
private String dst;
private Integer distance;
private Integer price;
// getters, setters
}
La IdClass
anotación asigna varios campos a la tabla PK.
ConEmbeddedId
La clase para la clave primaria compuesta podría verse así (podría ser una clase interna estática):
@Embeddable
public class TimePK implements Serializable {
protected Integer levelStation;
protected Integer confPathID;
public TimePK() {}
public TimePK(Integer levelStation, Integer confPathID) {
this.levelStation = levelStation;
this.confPathID = confPathID;
}
// equals, hashCode
}
Y la entidad:
@Entity
class Time implements Serializable {
@EmbeddedId
private TimePK timePK;
private String src;
private String dst;
private Integer distance;
private Integer price;
//...
}
La @EmbeddedId
anotación asigna una clase PK a la tabla PK.
Diferencias:
- Desde el punto de vista del modelo físico, no existen diferencias.
@EmbeddedId
de alguna manera comunica más claramente que la clave es una clave compuesta y, en mi opinión, tiene sentido cuando el pk combinado es una entidad significativa en sí misma o se reutiliza en su código .@IdClass
Es útil especificar que alguna combinación de campos es única pero estos no tienen un significado especial .
También afectan la forma en que escribe las consultas (haciéndolas más o menos detalladas):
con
IdClass
select t.levelStation from Time t
con
EmbeddedId
select t.timePK.levelStation from Time t
Referencias
- Especificación JPA 1.0
- Sección 2.1.4 "Claves primarias e identidad de entidad"
- Sección 9.1.14 "Anotación EmbeddedId"
- Sección 9.1.15 "Anotación IdClass"
Necesitas usar @EmbeddedId
:
@Entity
class Time {
@EmbeddedId
TimeId id;
String src;
String dst;
Integer distance;
Integer price;
}
@Embeddable
class TimeId implements Serializable {
Integer levelStation;
Integer confPathID;
}
Suponiendo que tiene las siguientes tablas de base de datos:
Primero, debe crear el @Embeddable
identificador compuesto que contiene:
@Embeddable
public class EmployeeId implements Serializable {
@Column(name = "company_id")
private Long companyId;
@Column(name = "employee_number")
private Long employeeNumber;
public EmployeeId() {
}
public EmployeeId(Long companyId, Long employeeId) {
this.companyId = companyId;
this.employeeNumber = employeeId;
}
public Long getCompanyId() {
return companyId;
}
public Long getEmployeeNumber() {
return employeeNumber;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof EmployeeId)) return false;
EmployeeId that = (EmployeeId) o;
return Objects.equals(getCompanyId(), that.getCompanyId()) &&
Objects.equals(getEmployeeNumber(), that.getEmployeeNumber());
}
@Override
public int hashCode() {
return Objects.hash(getCompanyId(), getEmployeeNumber());
}
}
Con esto implementado, podemos mapear la Employee
entidad que usa el identificador compuesto anotándola con @EmbeddedId
:
@Entity(name = "Employee")
@Table(name = "employee")
public class Employee {
@EmbeddedId
private EmployeeId id;
private String name;
public EmployeeId getId() {
return id;
}
public void setId(EmployeeId id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
La Phone
entidad que tiene una @ManyToOne
asociación con Employee
, necesita hacer referencia al identificador compuesto de la clase principal a través de dos @JoinColumn
asignaciones:
@Entity(name = "Phone")
@Table(name = "phone")
public class Phone {
@Id
@Column(name = "`number`")
private String number;
@ManyToOne
@JoinColumns({
@JoinColumn(
name = "company_id",
referencedColumnName = "company_id"),
@JoinColumn(
name = "employee_number",
referencedColumnName = "employee_number")
})
private Employee employee;
public Employee getEmployee() {
return employee;
}
public void setEmployee(Employee employee) {
this.employee = employee;
}
public String getNumber() {
return number;
}
public void setNumber(String number) {
this.number = number;
}
}
La clase de clave principal debe definir métodos iguales y hashCode
- Al implementar iguales, debe usar instancia de para permitir la comparación con subclases. Si Hibernate carga de forma diferida una relación uno a uno o muchos a uno, tendrá un proxy para la clase en lugar de la clase simple. Un proxy es una subclase. Comparar los nombres de las clases fallaría.
Más técnicamente: debes seguir el principio de sustitución de Liskows e ignorar la simetría. - El siguiente problema es usar algo como name.equals(that.name) en lugar de name.equals(that.getName()) . El primero fallará, si es un proxy.
http://www.laliluna.de/jpa-hibernate-guide/ch06s06.html
Parece que estás haciendo esto desde cero. Intente utilizar herramientas de ingeniería inversa disponibles, como Netbeans Entities from Database, para al menos automatizar los conceptos básicos (como identificadores integrados). Esto puede convertirse en un gran dolor de cabeza si tienes muchas mesas. Sugiero evitar reinventar la rueda y utilizar tantas herramientas disponibles como sea posible para reducir la codificación a la parte mínima y más importante, lo que pretendes hacer.