Inicial > Programação > A Melhor Forma de Mapear um Relacionamento OneToOne com JPA

A Melhor Forma de Mapear um Relacionamento OneToOne com JPA

Normalmente, em um relacionamento um-para-um anotado com @OneToOne, a entidade principal e a entidade secundária têm seus próprios identificadores, mas a entidade secundária também armazena uma referência para a chave estrangeira da entidade principal.

Figura 1 – Relacionamento de duas tabelas (note que é um M:N e não necessariamente um 1:1)

  CREATE TABLE TBL_PESSOA(  
    PK_PESSOA NUMBER(10) NOT NULL, 
    NM_PESSOA    VARCHAR2(256) NOT NULL, 
   CONSTRAINT PK_PESSOA PRIMARY KEY (PK_PESSOA)
  );

  CREATE TABLE TBL_PET( 
    PK_PET NUMBER(10) NOT NULL, 
    FK_PESSOA NUMBER(10) NOT NULL, 
    NM_PET    VARCHAR2(256) NOT NULL, 
    CONSTRAINT PK_PET PRIMARY KEY (PK_PET),
    CONSTRAINT FK_PET_PESSOA FOREIGN KEY (FK_PESSOA)
    REFERENCES TBL_PESSOA(PK_PESSOA)
  );

Com o modelo tradicional de mapeamento um-para-um, o banco de dados normalmente indexa tanto a chave primária quanto a chave estrangeira, o que é interessante para diminuir o scanning das tabelas, mas isso tem um custo na aplicação: mesmo anotando com FetchType.LAZY sem optional, a tabela A (pai) se comportará como FetchType.EAGER, o que é ruim para performance e uso de memória.

Se é um relacionamento um-para-um, significa que uma linha da tabela B estará relacionada à apenas uma linha da tabela A. Sendo assim, faria mais sentido utilizar a chave estrangeira da tabela A como chave primária da tabela B. Para isso, vamos utilizar a anotação @MapsId. Utilizando essa anotação, você não precisa de um relacionamento bidirecional, mas esse não é nosso propósito: vamos apenas utilizar a chave primária de uma tabela Pessoa em uma tabela Pet – nesse modelo, uma Pessoa só pode ter um Pet:

Figura 2 – Relacionamento entre Pessoa e Pet

A implementação simplificada abaixo desdobra esse relacionamento:

@Entity
@Table(name = "TBL_PESSOA")
public class Pessoa {
   @Id
   @GeneratedValue(strategy = GenerationType.IDENTITY)
   @Column(name = "PK_PESSOA")
   private Long id;

   @OneToOne(mappedBy = "pessoa", cascade = CascadeType.ALL, 
       fetch = FetchType.LAZY, optional = true)
   private Pet pet;
}

@Entity
@Table(name = "TBL_PET")
public class Pet {
   @Id
   private Long id;

   @MapsId
   @OneToOne(fetch = FetchType.LAZY)
   @JoinColumn(name = "PK_PESSOA")
   private Pessoa pessoa;

}

No banco de dados, o relacionamento é armazenado assim:

  CREATE TABLE TBL_PESSOA(	
    PK_PESSOA NUMBER(10) NOT NULL, 
    NM_PESSOA    VARCHAR2(256) NOT NULL, 
	  CONSTRAINT PK_PESSOA PRIMARY KEY (PK_PESSOA)
  );

  CREATE TABLE TBL_PET( 
    PK_PESSOA NUMBER(10) NOT NULL, 
    NM_PET    VARCHAR2(256) NOT NULL, 
    CONSTRAINT PK_PET PRIMARY KEY (PK_PESSOA),
    CONSTRAINT FK_PET_PESSOA FOREIGN KEY (PK_PESSOA)
    REFERENCES TBL_PESSOA(PK_PESSOA)
  );

Figura 3 – Relacionamento 1:1 com PK compartilhada

  1. 02/10/2018 às 6:54 PM

    Olá! Voce sabe se faz alguma diferença em qual das duas classes eu coloco o @MapsId ou se posso colocar nas duas? E o que acontece se esquecer dessa anotação? Tem alguma ideia?

    Eu usei por muito tempo chave primária e estrangeira na mesma coluna dessa forma sem nenhum @MapsId (apenas o @Column(name = …) e o @JoinColumn(name = …) para avisar o JPA que a coluna é a mesma) e em geral funciona, mas fico me perguntando se alguns erros esporádicos poderiam ser causados pela falta dessa anotação.

    Vou começar a usar o @MapsId agora, mas se eu não vir nenhuma diferença, vou ficar realmente encucado sobre o real funcionamento dessa anotação!

  2. 02/10/2018 às 7:20 PM

    Olá, Marcus. A anotação @MapsId tem que ficar na classe filha, pois ela avisa à persistência que o ID do pai também será o ID do filho.

    O @JoinColumn na verdade diz ao JPA que a coluna anotada é uma chave estrangeira e o objeto correspondente deve ser carregado na memória. Se na sua aplicação o tempo de resposta não é um problema, pode definir uma chave específica para a tabela filha e mapear do jeito comum. Já trabalhei em aplicações onde foi necessário criar VIEW ou utilizar native query para trazer apenas uma pedacinho da informação. Zoado.

    Abs.

    • 03/10/2018 às 11:13 AM

      Pois é, mas quem é a filha? Pode ser qualquer uma, pois as duas têm a PK_PESSOA da mesma forma. A única diferença que vi ali foi que a TBL_PET tem a foreign key, mas como não estou usando o JPA para gerar as tabelas (elas já foram criadas), do ponto de vista do acesso às tabelas, elas são “irmãs” e não mãe e filha, era essa a minha dúvida.

  3. 03/10/2018 às 12:34 PM

    Olá Marcus. Do ponto de vista do negócio, Pessoa é a entidade forte e Pet é a entidade fraca. Sendo assim, Pessoa (TBL_PESSOA) é a tabela pai. Não faz diferença se você utilizou JPA para criar o modelo relacional – eu criei na mão. Sim, você pode acessar qualquer uma delas diretamente, mas seu foco deve ser no negócio para escolher a melhor estratégia de mapeamento. Você só conseguirá usufruir da dica que compartilhei se focar na tabela TBL_PESSOA.
    Abs.

    • 03/10/2018 às 1:48 PM

      Sim, sim, do ponto de vista do negócio são diferentes, mas o JPA/Hibernate não vê desse ponto de vista, ele só vê as anotações e pra ele qualquer uma poderia ser classe mãe ou filha. Por isso perguntei se fazia diferença em qual delas eu ponho o @MapsId, se o JPA/Hibernate vai se comportar de forma diferente ou não.

  1. No trackbacks yet.

Deixe um comentário