Como mapear uma chave composta com JPA e Hibernate?

204

Neste código, como gerar uma classe Java para a chave composta (como compor a chave no hibernate):

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
fonte
1
Um conjunto realmente bom de exemplos: vladmihalcea.com/2016/08/01/…
TecHunter 27/04

Respostas:

415

Para mapear uma chave composta, você pode usar as EmbeddedId ou as IdClassanotações. Eu sei que esta pergunta não é estritamente sobre JPA, mas as regras definidas pela especificação também se aplicam. Então aqui estão eles:

2.1.4 Chaves primárias e identidade da entidade

...

Uma chave primária composta deve corresponder a um único campo ou propriedade persistente ou a um conjunto desses campos ou propriedades, conforme descrito abaixo. Uma classe de chave primária deve ser definida para representar uma chave primária composta. As chaves primárias compostas geralmente surgem ao mapear a partir de bancos de dados herdados quando a chave do banco de dados é composta por várias colunas. As anotações EmbeddedIde IdClasssão usadas para denotar chaves primárias compostas. Consulte as seções 9.1.14 e 9.1.15.

...

As regras a seguir se aplicam às chaves primárias compostas:

  • A classe de chave primária deve ser pública e deve ter um construtor público no-arg.
  • Se o acesso baseado em propriedade for usado, as propriedades da classe de chave primária deverão ser públicas ou protegidas.
  • A classe de chave primária deve ser serializable.
  • A classe de chave primária deve definir equalse hashCode métodos. A semântica da igualdade de valor para esses métodos deve ser consistente com a igualdade do banco de dados para os tipos de banco de dados para os quais a chave está mapeada.
  • Uma chave primária composta deve ser representada e mapeada como uma classe incorporável (consulte a Seção 9.1.14, “Anotação EmbeddedId”) ou deve ser representada e mapeada para vários campos ou propriedades da classe de entidade (consulte a Seção 9.1.15, “IdClass Anotação").
  • Se a classe de chave primária composta for mapeada para vários campos ou propriedades da classe de entidade, os nomes dos campos ou propriedades da chave primária na classe de chave primária e os da classe de entidade devem corresponder e seus tipos devem ser os mesmos.

Com um IdClass

A classe da chave primária composta pode parecer (poderia ser uma classe 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
}

E a entidade:

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

A IdClassanotação mapeia vários campos para a tabela PK.

Com EmbeddedId

A classe da chave primária composta pode parecer (poderia ser uma classe 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
}

E a entidade:

@Entity
class Time implements Serializable {
    @EmbeddedId
    private TimePK timePK;

    private String src;
    private String dst;
    private Integer distance;
    private Integer price;

    //...
}

A @EmbeddedIdanotação mapeia uma classe PK para a tabela PK.

Diferenças:

  • Do ponto de vista do modelo físico, não há diferenças
  • @EmbeddedIdde alguma forma, comunica com mais clareza que a chave é uma chave composta e o IMO faz sentido quando a pk combinada é uma entidade significativa em si ou é reutilizada no seu código .
  • @IdClass é útil especificar que alguma combinação de campos é única, mas esses não têm um significado especial .

Eles também afetam a maneira como você escreve as consultas (tornando-as mais ou menos detalhadas):

  • com IdClass

    select t.levelStation from Time t
  • com EmbeddedId

    select t.timePK.levelStation from Time t

Referências

  • Especificação JPA 1.0
    • Seção 2.1.4 "Chaves primárias e identidade da entidade"
    • Seção 9.1.14 "Anotação EmbeddedId"
    • Seção 9.1.15 "Anotação IdClass"
Pascal Thivent
fonte
15
Há também uma solução específica para o Hibernate: Mapeie várias propriedades como propriedades @Id sem declarar uma classe externa como o tipo de identificador (e use a anotação IdClass). Veja 5.1.2.1. Identificador composto no manual do Hibernate.
9118 Johan Johan Berg
Você poderia dar uma olhada nesta pergunta, por favor? Estou tendo problemas com uma chave primária composta desde o campo de membro idé sempre nulle não são geradas: /
displayname
Poderia, por favor, dar um exemplo com um getter e um setter, pois estou tendo dificuldade em ver onde eles entram em jogo nos dois casos. Especialmente o exemplo IdClass. obrigado. Ah, e incluindo nomes de colunas, obrigado.
Jeremy
embora a solução específica de hibernação esteja obsoleta.
Nikhil Sahu
Nos documentos do Hibernate Annotations , sobre @IdClass: "Ele foi herdado da idade das trevas do EJB 2 para compatibilidade com versões anteriores e recomendamos que você não o use (por uma questão de simplicidade)".
Marco Ferrari
49

Você precisa 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
fonte
@ Thierry-DimitriRoy, como eu poderia atribuir o timeId.levelStation e o timeId.confPathID. Você poderia dar um exemplo, por favor?
Duc Tran
@ Thierry-DimitriRoy A classe principal não pode ser uma classe interna estática da classe de entidade?
Nikhil Sahu
Sim, poderia ser
Samy Omar
17

Como expliquei neste artigo , supondo que você tenha as seguintes tabelas de banco de dados:

insira a descrição da imagem aqui

Primeiro, você precisa criar a @Embeddableretenção do identificador composto:

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

Com isso, podemos mapear a Employeeentidade que usa o identificador composto anotando-o com @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;
    }
}

A Phoneentidade que tem uma @ManyToOneassociação Employeeprecisa fazer referência ao identificador composto da classe pai por meio de dois @JoinColumnmapeamentos:

@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 mais detalhes, consulte este artigo .

Vlad Mihalcea
fonte
Existe uma ferramenta que pode gerar EmployeeId a partir do esquema db?
Leon
Experimente o Hibernate Tools. Tem uma ferramenta de engenharia reversa para isso.
Vlad Mihalcea
7

A classe de chave primária deve definir métodos iguais e hashCode

  1. Ao implementar iguais, você deve usar instanceof para permitir a comparação com subclasses. Se o Hibernate preguiçoso carregar uma relação de um para um ou muitos para um, você terá um proxy para a classe em vez da classe simples. Um proxy é uma subclasse. Comparar os nomes das classes falharia.
    Mais tecnicamente: você deve seguir o Princípio de substituição de Liskows e ignorar a simetria.
  2. A próxima armadilha é usar algo como name.equals (that.name) em vez de name.equals (that.getName ()) . O primeiro falhará, se for um proxy.

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

Mike
fonte
6

Parece que você está fazendo isso do zero. Tente usar as ferramentas de engenharia reversa disponíveis, como o Netbeans Entities from Database, para pelo menos automatizar o básico (como IDs incorporados). Isso pode se tornar uma enorme dor de cabeça se você tiver muitas tabelas. Sugiro evitar reinventar a roda e usar o maior número possível de ferramentas para reduzir a codificação à parte mínima e mais importante, o que você pretende fazer.

javydreamercsw
fonte
5

Vamos dar um exemplo simples. Digamos duas tabelas nomeadas teste customersão descritas 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));

Existe mais uma tabela que mantém o controle de tests e 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 na tabela tests_purchaseda chave primária é uma chave composta, portanto, usaremos a <composite-id ...>...</composite-id>tag no hbm.xmlarquivo de mapeamento. Então, a PurchasedTest.hbm.xmlaparência será:

<?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>

Mas isso não termina aqui. No Hibernate, usamos session.load ( entityClass, id_type_object) para localizar e carregar a entidade usando a chave primária. No caso de chaves compostas, o objeto ID deve ser uma classe de ID separada (no caso acima, uma PurchasedTestIdclasse) que apenas declara os atributos da chave primária, como abaixo :

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

O ponto importante é que também implementamos as duas funções hashCode()e equals()o Hibernate depende delas.

dinesh kandpal
fonte
2

Outra opção é mapear como um Mapa de elementos compostos na tabela ConfPath.

Esse mapeamento se beneficiaria de um índice em (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;
    }
}

Mapeamento:

<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
fonte
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 anotação

Classe de chave composta

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

Classe de entidade

@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
fonte
1
Não faz sentido, ele precisa da chave primária
Mazen Embaby
no título, ele diz que a chave composta, que não precisa ser primária, é o
Enerccio
por favor, verifique o que ele escreveu chave primária
Mazen Embaby