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;


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"

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


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


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.


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.


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>

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

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
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.