¿Cómo mapear una clave compuesta con JPA e Hibernate?

205

En este código, cómo generar una clase Java para la clave compuesta (cómo componer 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;
kaaf
fuente

Respuestas:

416

Para asignar una clave compuesta, puede usar las anotaciones EmbeddedId oIdClass . Sé que esta pregunta no es estrictamente sobre 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 solo 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 generalmente surgen cuando se asignan bases de datos heredadas cuando la clave de la base de datos se compone de varias columnas. Las anotaciones EmbeddedIdy IdClassse utilizan para denotar claves primarias compuestas. Ver secciones 9.1.14 y 9.1.15.

...

Las siguientes reglas se aplican a las claves primarias compuestas:

  • La clase de clave primaria 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 primaria deben ser públicas o protegidas.
  • La clase de clave primaria debe ser serializable.
  • La clase de clave primaria debe definir equalsy hashCode métodos.La semántica de la 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 se asigna la clave.
  • Una clave primaria compuesta debe representarse y asignarse como una clase incorporable (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 un IdClass

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
}

los IdClass anotación asigna múltiples campos a la tabla PK.

Con EmbeddedId

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;

    //...
}

los @EmbeddedId anotación asigna una clase PK a la tabla PK.

Diferencias:

  • Desde el punto de vista del modelo físico, no hay diferencias
  • @EmbeddedIdde alguna manera se comunica más claramente que la clave es una clave compuesta y la OMI tiene sentido cuando el pk combinado es una entidad significativa en sí misma o se reutiliza en su código .
  • @IdClass es útil para especificar que alguna combinación de campos es única, pero estos no tienen un significado especial .

También afectan la forma en que escribe 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 la entidad"
    • Sección 9.1.14 "Anotación de EmbeddedId"
    • Sección 9.1.15 "Anotación de IdClass"
Pascal Thivent
fuente
15
También hay una solución específica para Hibernate: mapee varias propiedades como propiedades @Id sin declarar que una clase externa sea el tipo de identificador (y use la anotación IdClass). Ver 5.1.2.1. Identificador compuesto en el manual de Hibernate.
Johan Boberg
¿Podría echar un vistazo a esta pregunta por favor? Tengo problemas con una clave primaria compuesta ya que el campo miembro idsiempre nullse genera y no se genera: /
displayname
Podría poner un ejemplo con un getter y setter ya que tengo dificultades para ver dónde entran en juego en cualquier caso. Especialmente el ejemplo IdClass. Gracias. Ah e incluidos los nombres de columna, gracias.
Jeremy
aunque la solución específica de hibernación está en desuso.
Nikhil Sahu
De los documentos de Anotaciones de Hibernate , sobre @IdClass: "Se ha heredado de la edad oscura de EJB 2 por compatibilidad con versiones anteriores y le recomendamos que no lo use (por simplicidad)".
Marco Ferrari
49

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;
}
Thierry-Dimitri Roy
fuente
@ Thierry-DimitriRoy, ¿cómo podría asignar timeId.levelStation y timeId.confPathID. ¿Podría dar un ejemplo por favor?
Duc Tran
@ Thierry-DimitriRoy ¿Puede la clase primaria no ser una clase interna estática de la clase de entidad?
Nikhil Sahu
Sí, podría ser
Samy Omar
17

Como expliqué en este artículo , suponiendo que tenga las siguientes tablas de base de datos:

ingrese la descripción de la imagen aquí

Primero, debe crear el contenido @Embeddabledel identificador compuesto:

@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 en su lugar, podemos mapear la Employeeentidad que usa el identificador compuesto al anotarlo 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 Phoneentidad que tiene una @ManyToOneasociación con Employee, necesita hacer referencia al identificador compuesto de la clase padre a través de dos @JoinColumnasignaciones:

@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;
    }
}

Para más detalles, mira este artículo .

Vlad Mihalcea
fuente
¿Existe alguna herramienta que pueda generar EmployeeId a partir del esquema db?
Leon
Prueba Hibernate Tools. Tiene una herramienta de ingeniería inversa para eso.
Vlad Mihalcea
7

La clase de clave primaria debe definir métodos igual y hashCode

  1. Cuando implemente equals, debe usar instanceof para permitir la comparación con subclases. Si Hibernate lazy carga 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 clase fallaría.
    Más técnicamente: debe seguir el Principio de sustitución de Liskows e ignorar la simetría.
  2. El siguiente escollo es usar algo como name.equals (that.name) en lugar de name.equals (that.getName ()) . El primero fallará, si eso es un proxy.

http://www.laliluna.de/jpa-hibernate-guide/ch06s06.html

Miguel
fuente
6

Parece que estás haciendo esto desde cero. Intente utilizar las herramientas de ingeniería inversa disponibles como las Entidades Netbeans de la Base de Datos 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 como sea posible para reducir la codificación a la parte mínima y más importante, lo que intenta hacer.

javydreamercsw
fuente
5

Tomemos un ejemplo simple. Digamos dos tablas nombradas testy customerse describen allí como:

create table test(
  test_id int(11) not null auto_increment,
  primary key(test_id));

create table customer(
  customer_id int(11) not null auto_increment,
  name varchar(50) not null,
  primary key(customer_id));

Hay una tabla más que mantiene la pista de testsy customer:

create table tests_purchased(
  customer_id int(11) not null,
  test_id int(11) not null,
  created_date datetime not null,
  primary key(customer_id, test_id));

Podemos ver que en la tabla tests_purchasedla clave primaria es una clave compuesta, por lo que utilizaremos la <composite-id ...>...</composite-id>etiqueta en el hbm.xmlarchivo de mapeo. Entonces PurchasedTest.hbm.xmlse verá así:

<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
  "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
  "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">

<hibernate-mapping>
  <class name="entities.PurchasedTest" table="tests_purchased">

    <composite-id name="purchasedTestId">
      <key-property name="testId" column="TEST_ID" />
      <key-property name="customerId" column="CUSTOMER_ID" />
    </composite-id>

    <property name="purchaseDate" type="timestamp">
      <column name="created_date" />
    </property>

  </class>
</hibernate-mapping>

Pero no termina aquí. En Hibernate usamos session.load ( entityClass, id_type_object) para encontrar y cargar la entidad usando la clave primaria. En el caso de las claves compuestas, el objeto de ID debe ser una clase de ID separada (en el caso anterior, una PurchasedTestIdclase) que simplemente declara los atributos de la clave primaria como a continuación :

import java.io.Serializable;

public class PurchasedTestId implements Serializable {
  private Long testId;
  private Long customerId;

  // an easy initializing constructor
  public PurchasedTestId(Long testId, Long customerId) {
    this.testId = testId;
    this.customerId = customerId;
  }

  public Long getTestId() {
    return testId;
  }

  public void setTestId(Long testId) {
    this.testId = testId;
  }

  public Long getCustomerId() {
    return customerId;
  }

  public void setCustomerId(Long customerId) {
    this.customerId = customerId;
  }

  @Override
  public boolean equals(Object arg0) {
    if(arg0 == null) return false;
    if(!(arg0 instanceof PurchasedTestId)) return false;
    PurchasedTestId arg1 = (PurchasedTestId) arg0;
    return (this.testId.longValue() == arg1.getTestId().longValue()) &&
           (this.customerId.longValue() == arg1.getCustomerId().longValue());
  }

  @Override
  public int hashCode() {
    int hsCode;
    hsCode = testId.hashCode();
    hsCode = 19 * hsCode+ customerId.hashCode();
    return hsCode;
  }
}

El punto importante es que también implementamos las dos funciones hashCode()y equals()como Hibernate depende de ellas.

dinesh kandpal
fuente
2

Otra opción es mapear es como un mapa de elementos compuestos en la tabla ConfPath.

Sin embargo, esta asignación se beneficiaría de un índice en (ConfPathID, levelStation).

public class ConfPath {
    private Map<Long,Time> timeForLevelStation = new HashMap<Long,Time>();

    public Time getTime(long levelStation) {
        return timeForLevelStation.get(levelStation);
    }

    public void putTime(long levelStation, Time newValue) {
        timeForLevelStation.put(levelStation, newValue);
    }
}

public class Time {
    String src;
    String dst;
    long distance;
    long price;

    public long getDistance() {
        return distance;
    }

    public void setDistance(long distance) {
        this.distance = distance;
    }

    public String getDst() {
        return dst;
    }

    public void setDst(String dst) {
        this.dst = dst;
    }

    public long getPrice() {
        return price;
    }

    public void setPrice(long price) {
        this.price = price;
    }

    public String getSrc() {
        return src;
    }

    public void setSrc(String src) {
        this.src = src;
    }
}

Cartografía:

<class name="ConfPath" table="ConfPath">
    <id column="ID" name="id">
        <generator class="native"/>
    </id>
    <map cascade="all-delete-orphan" name="values" table="example"
            lazy="extra">
        <key column="ConfPathID"/>
        <map-key type="long" column="levelStation"/>
        <composite-element class="Time">
            <property name="src" column="src" type="string" length="100"/>
            <property name="dst" column="dst" type="string" length="100"/>
            <property name="distance" column="distance"/>
            <property name="price" column="price"/>
        </composite-element>
    </map>
</class>
Maurice Perry
fuente
1

Usando hbm.xml

    <composite-id>

        <!--<key-many-to-one name="productId" class="databaselayer.users.UserDB" column="user_name"/>-->
        <key-property name="productId" column="PRODUCT_Product_ID" type="int"/>
        <key-property name="categoryId" column="categories_id" type="int" />
    </composite-id>  

Usando Anotación

Clase de clave compuesta

public  class PK implements Serializable{
    private int PRODUCT_Product_ID ;    
    private int categories_id ;

    public PK(int productId, int categoryId) {
        this.PRODUCT_Product_ID = productId;
        this.categories_id = categoryId;
    }

    public int getPRODUCT_Product_ID() {
        return PRODUCT_Product_ID;
    }

    public void setPRODUCT_Product_ID(int PRODUCT_Product_ID) {
        this.PRODUCT_Product_ID = PRODUCT_Product_ID;
    }

    public int getCategories_id() {
        return categories_id;
    }

    public void setCategories_id(int categories_id) {
        this.categories_id = categories_id;
    }

    private PK() { }

    @Override
    public boolean equals(Object o) {
        if ( this == o ) {
            return true;
        }

        if ( o == null || getClass() != o.getClass() ) {
            return false;
        }

        PK pk = (PK) o;
        return Objects.equals(PRODUCT_Product_ID, pk.PRODUCT_Product_ID ) &&
                Objects.equals(categories_id, pk.categories_id );
    }

    @Override
    public int hashCode() {
        return Objects.hash(PRODUCT_Product_ID, categories_id );
    }
}

Clase de entidad

@Entity(name = "product_category")
@IdClass( PK.class )
public  class ProductCategory implements Serializable {
    @Id    
    private int PRODUCT_Product_ID ;   

    @Id 
    private int categories_id ;

    public ProductCategory(int productId, int categoryId) {
        this.PRODUCT_Product_ID = productId ;
        this.categories_id = categoryId;
    }

    public ProductCategory() { }

    public int getPRODUCT_Product_ID() {
        return PRODUCT_Product_ID;
    }

    public void setPRODUCT_Product_ID(int PRODUCT_Product_ID) {
        this.PRODUCT_Product_ID = PRODUCT_Product_ID;
    }

    public int getCategories_id() {
        return categories_id;
    }

    public void setCategories_id(int categories_id) {
        this.categories_id = categories_id;
    }

    public void setId(PK id) {
        this.PRODUCT_Product_ID = id.getPRODUCT_Product_ID();
        this.categories_id = id.getCategories_id();
    }

    public PK getId() {
        return new PK(
            PRODUCT_Product_ID,
            categories_id
        );
    }    
}
Mazen Embaby
fuente
1
No tiene sentido, necesita la clave principal
Mazen Embaby
en el título dice clave compuesta, que no tiene que ser primaria
Enerccio
compruebe lo que escribió la clave primaria
Mazen Embaby