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