Modo mais eficiente para mapear uma @OneToMany relação com JPA e Hibernate

Eu escolhi abrir este post com esta citação porque sou fã de Linus Torvalds.😉

Este é o meu primeiro artigo de sempre. Neste eu estarei cobrindo todos os casos possíveis em One-to-Many/Many-to-One associação de entidades. Restantes Many-to-Many e One-to-One serão cobertos nos próximos artigos.

Eu espero que isto definitivamente ajude todos os novatos que querem aprender jpa/hibernate, Por favor leia a peça inteira 😛

NOTE:

Aqui eu cobri todos os casos possíveis de One-to-Many/Many-to-One mapeamento. Entre todos , a associação Bidirecional `@OneToMany` é a melhor maneira de mapear uma relação de um para muitos bancos de dados.

A associação hibernar classificada em One-to-One, One-to-Many/Many-to-One e Many-to-Many.

  • A direcção de uma relação pode ser bidireccional ou unidireccional.
  • Uma relação bidireccional tem tanto um lado proprietário como um lado inverso.
  • Uma relação unidireccional tem apenas um lado proprietário. O lado proprietário de uma relação determina como o tempo de execução da Persistência faz atualizações da relação no banco de dados.
  • Unidirecional é uma relação onde um lado não sabe sobre a relação.
  • Em uma relação unidirecional, apenas uma entidade tem um campo de relação ou propriedade que se refere à outra. Por exemplo, o Item do documento teria um campo de relação que identifica o Produto, mas o Produto não teria um campo de relação ou propriedade para o Item do documento. Em outras palavras, o Item sabe sobre o Produto, mas o Produto não sabe quais instâncias de Item se referem a ele.

Relações bidirecionais:

  • Relação bidirecional fornece acesso de navegação em ambas as direções, para que você possa acessar o outro lado sem consultas explícitas.
  • Em uma relação bidirecional, cada entidade tem um campo ou propriedade de relação que se refere à outra entidade. Através do campo ou propriedade de relação, o código de uma classe de entidade pode acessar seu objeto relacionado. Se uma entidade tem um campo relacionado, diz-se que a entidade “conhece” o seu objeto relacionado. Por exemplo, se Order sabe quais instâncias de Line Item tem e se Line Item sabe a que Order pertence, eles têm uma relação bidirecional.

As relações bidirecionais devem seguir estas regras.

  • O lado inverso de uma relação bidirecional deve se referir ao seu próprio lado (Entidade que contém a chave estrangeira) usando o elemento mappedBy do elemento @OneToOne, @OneToMany, ou @ManyToMany anotação. O elemento mappedBy designa a propriedade ou campo na entidade que é a proprietária da relação.
  • Os muitos lados de @ManyToOne relações bidirecionais não devem definir o elemento mappedBy. Os muitos lados são sempre o lado proprietário da relação.
  • Para @OneToOne relações bidirecionais, o lado proprietário corresponde ao lado que contém @JoinColuna, ou seja, a chave estrangeira correspondente.
  • Para @ManyToMany relações bidirecionais, cada lado pode ser o lado de propriedade.

@UmaToMuita relação com JPA e Hibernate

Simplesmente colocado, um-para-muitos mapeamentos significa que uma linha em uma tabela é mapeada para várias linhas em outra tabela.

Quando usar um para muitos mapeamentos

Utilizar um para mapear para criar 1…N relação entre entidades ou objetos.

Temos que escrever duas entidades i.e. Company e Branch de modo que várias filiais possam ser associadas a uma única empresa, mas uma única filial não pode ser compartilhada entre duas ou mais empresas.

Hibernar uma a muitas soluções de mapeamento:

  1. Um a muitos mapeamentos com associação de chaves estrangeiras
  2. Um a muitos mapeamentos com tabela de join

Este problema pode ser resolvido de duas formas diferentes.

Uma é ter uma coluna de chaves estrangeiras na tabela de filiais, ou seja company_id. Esta coluna irá se referir à chave primária da tabela da Empresa. Desta forma não podem ser associadas duas filiais a múltiplas empresas.

Segunda abordagem é ter uma tabela de junção comum, digamos Company_Branch, Esta tabela terá duas colunas i.e. company_id que será chave estrangeira referindo-se a chave primária na tabela de Empresa e de forma similar branch_id que será chave estrangeira referindo-se a chave primária da tabela de Filiais.

Se @OneToMany/@ManyToOne não tiver uma associação @ManyToOne/@OneToMany respectivamente no lado infantil, então a associação @OneToMany/@ManyToOne é unidirecional.

@OneToMany Relação Unidirecional

Nesta abordagem, qualquer entidade será responsável por fazer a relação e mantê-la. Ou a Empresa declara a relação como uma para muitas, ou a Filial declara a relação do seu fim como muitas para uma.

CASO 1: (Mapeamento com associação de chave estrangeira)

Se usarmos apenas @OneToMany então haverá 3 tabelas. Tais como company, branch e company_branch.

>

NOTE:
No exemplo acima usei termos como cascata, orphanRemoval, fetch e targetEntity, que explicarei no meu próximo post.

Tabela company_branch terá duas chaves estrangeiras company_id e branch_id.

Agora, se persistirmos uma Empresa e duas Filiais:

Hibernar vai executar as seguintes instruções SQL:

  • A @OneToMany associação é, por definição, uma associação parent(non-owning), mesmo que seja unidirecional ou bidirecional. Apenas o lado pai de uma associação faz sentido em cascata as transições do estado de sua entidade para as crianças.
  • Quando persistir a entidade Company, a cascata irá propagar a operação persistente para as crianças do ramo subjacente também. Ao remover um Branch da coleção de ramos, a linha de associação é excluída da tabela de links, e o atributo orphanRemoval irá acionar uma remoção de ramos também.

As associações unidirecionais não são muito eficientes quando se trata de remover entidades filhas. Neste exemplo em particular, ao descarregar o contexto de persistência, Hibernate apaga todas as entradas filhas da base de dados e reinsere as que ainda são encontradas no contexto de persistência na memória.

Por outro lado, uma associação bidirecional @OneToMany é muito mais eficiente porque a entidade filha controla a associação.

CASO 2: (Mapeamento com associação de chave estrangeira)

Se usarmos apenas @ManyToOne então haverá 2 tabelas. Como empresa, filial.

Este código acima gerará 2 tabelas Company(company_id,name) e Branch(branch_id,name,company_company_id). Aqui Branch é o lado proprietário já que tem a associação de chave estrangeira.

CASO 3: (Mapeamento com associação de chave estrangeira)

Se usarmos ambos @ManyToOne e @OneToMany então ele criará 3 tabelas Company(id,name), Branch(id,name,company_id), Company_Branch(company_id,branch_id)

Este mapeamento abaixo pode parecer bi-direcional mas não é. Ele define não uma relação bidirecional, mas duas relações unidirecionais separadas.

CASO 4: (Unidirecional @OneToMany com @JoinColumn)(Mapeamento com associação de chave estrangeira)

Se usarmos @OneToMany com @JoinColumn então haverá 2 tabelas. Como empresa, filial

Na entidade acima dentro de @JoinColumn, o nome refere-se ao nome da coluna de chave estrangeira que é companyId i.e company_id e nome da coluna referenciada indica a chave primária i.e id da entidade(Empresa) à qual a chave estrangeira companyId se refere.

CASO 5: (Mapeamento com associação de chave estrangeira)

Se usarmos @ManyToOne com @JoinColumn então haverá 2 tabelas. Tal como empresa,filial.

A@JoinColumn anotação ajuda Hibernate a descobrir que existe uma coluna company_id Chave Estrangeira na tabela da filial que define esta associação.

>

Branch terá a chave estrangeira company_id, portanto é o lado do proprietário.

Agora, se persistirmos 1 Empresa e 2 Ramo(s):

quando remover a primeira entrada da coleção de crianças:

company.getBranches().remove(0);

Hibernate executa duas declarações em vez de uma :

  • Primeiro fará com que o campo chave estrangeira seja nulo(para quebrar associação com o pai) e depois apagará o registro.
Update branch set branch_id = null where where id = 1 delete from branch where id = 1 ;

CASO 6: (Mapping with join table)

Agora, vamos considerar one-to-many relação onde Person(id,name) se associar com múltiplos Veichle(id,name,number) e múltiplos veículos podem pertencer à mesma pessoa.

Um dia, enquanto na estrada o xerife Carlos encontrou poucos veículos abandonados, muito provavelmente roubados. Agora o xerife tem de actualizar os detalhes dos veículos (número, nome) na sua base de dados mas o principal problema é que não há dono para esses veículos, por isso person_id(foreign key ) campo permanecerá nulo.

Agora o sheriff salva dois veículos roubados para db da seguinte forma:

Vehicle vehicle1 = new Vehicle("ford", 1); 
Vehicle vehicle2 = new Vehicle("gmc", 2);
List<Vehicle> vehicles = new ArrayList<>(); vehicles.add(vehicle1);
vehicles.add(vehicle2);
entityManager.persist(veichles);

Hibernate vai executar as seguintes instruções SQL:

insert into vehicle (id, name, person_id) values (1, "ford", null); insert into vehicle (id, name, person_id) values (2, "gmc", null);id |name|person_id|
-----|----|---------|
1 |ford|NULL |
2 |benz|NULL |
---------------------

Esta estratégia acima vai nos forçar a colocar valores nulos na coluna para lidar com relações opcionais.

Tipicamente, pensamos em many-to-many relacionamentos quando consideramos um join table, mas, usando uma tabela de join, neste caso, pode nos ajudar a eliminar estes valores nulos:

Esta abordagem usa uma tabela de join para armazenar as associações entre as entidades da Filial e da Empresa. @JoinTable anotação tem sido usada para fazer esta associação.

Neste exemplo aplicamos @JoinTable no lado do Veículo(Many Side).

Vejamos como será o esquema da base de dados:

Acima do código gerará 3 tabelas como , vehicle(id,name) e vehicle_person(vehicle_id,person_id). Aqui vehicle_person manterá a relação da chave estrangeira para ambas as entidades Pessoa e Veículo.

Então quando o xerife salvar os detalhes do veículo, nenhum valor nulo tem que ser persistido para a tabela do veículo porque estamos mantendo a associação da chave estrangeira em vehicle_person tabelas não na tabela do veículo.

Vehicle vehicle1 = new Vehicle("ford", 1);
Vehicle vehicle2 = new Vehicle("gmc", 2);
List<Vehicle> vehicles = new ArrayList<>(); vehicles.add(vehicle1);
vehicles.add(vehicle2);
entityManager.persist(veichles);

Hibernate vai executar as seguintes instruções SQL:

insert into vehicle (id, name) values (1, "ford"); insert into vehicle (id, name) values (2, "gmc");id |name|
-----|----|
1 |ford|
2 |benz|
-----------

Este exemplo acima mostra, como nos livramos da inserção de valor nulo.

@OneToMany Bi-directional Relationship

  • O bidirecional @OneToMany associação também requer uma @ManyToOne associação do lado da criança. Embora o Modelo de Domínio exponha dois lados para navegar nesta associação, nos bastidores, a base de dados relacional tem apenas uma chave estrangeira para esta relação.
  • Todas as associações bidirecionais devem ter apenas um lado próprio (o lado infantil), sendo o outro referido como o lado inverso (ou omappedBy).
  • Se usarmos o @OneToMany com o conjunto de atributos mappedBy, então temos uma associação bidirecional, significando que precisamos ter uma associação @ManyToOne no lado infantil que o mappedBy referências.
  • O elemento mappedBy define uma relação bidirecional. Este atributo permite referenciar as entidades associadas de ambos os lados.

A melhor maneira de mapear uma associação @OneToMany é contar com o lado @ManyToOne para propagar todas as mudanças de estado das entidades.

Se persistirmos 2 Ramo(s)

Hibernar gera apenas uma instrução SQL para cada entidade de Ramo persistente:

insert into company (name, id) values ("company1",1); 
insert into branch (company_id, name, id) values (1,"branch1",1); insert into branch (company_id, name, id) values (1,"branch2",2);

Se removermos um Ramo:

Company company = entityManager.find( Company.class, 1L ); Branch branch = company.getBranches().get(0); company.removeBranches(branch);

Existe apenas uma instrução SQL de exclusão que é executada:

delete from Branch where id = 1

Então, a associação bidirecional @OneToMany é a melhor maneira de mapear uma relação de um para muitos bancos de dados quando realmente precisamos da coleção do lado pai da associação.

@JoinColumn Especifica uma coluna para unir uma associação ou coleção de elementos de uma entidade. A anotação @JoinColumn indica que esta entidade é a proprietária da relação. Esta é a tabela correspondente tem uma coluna com uma chave estrangeira para a tabela referenciada.

No exemplo acima, a Entidade proprietária Ramo, tem uma Coluna de Junção chamada company_id que tem uma chave estrangeira para a entidade não proprietária Empresa.

Quando uma associação bidirecional é formada, o desenvolvedor da aplicação deve certificar-se de que ambos os lados estão sempre em sincronia. Os addBranches() e removeBranches() são métodos utilitários que sincronizam ambas as extremidades sempre que um elemento criança (ou seja, Ramo) é adicionado ou removido.

Não como o unidirecional @OneToMany, a associação bidirecional é muito mais eficiente ao gerenciar o estado de persistência da coleção.

A remoção de cada elemento só requer uma única atualização (na qual a coluna de chave estrangeira é definida como NULL) e se o ciclo de vida da entidade criança está vinculado ao seu próprio pai ou mãe para que a criança não possa existir sem o seu pai ou mãe, então podemos anotar a associação com o atributo orphan-removal e desassociar a criança irá disparar uma declaração de exclusão na linha da tabela da criança real também.

NOTE:

  • No exemplo bi-direcional acima o termo Pai e Filho na Entidade/OO/Modelo (ou seja, na classe java) se refere ao lado Não Ganancioso/Inverso e Proprietário em SQL respectivamente.

No ponto de vista java Companhia é o Pai e ramo é o filho aqui. Como uma filial não pode existir sem pai.

No ponto de vista de SQL a filial é o lado do Proprietário e a Empresa é o lado do Não remunerado(Inverso). Como há 1 empresa para N filiais, cada filial contém uma chave estrangeira para a empresa a que pertence, o que significa que a filial “possui” (ou literalmente contém) a conexão (informação). Isto é exatamente o oposto do mundo OO/modelo.

@OneToMany Bi-directional Relationship(Mapping with join table)

From CASE 6 we have Unidirectional mapping with @JoinTable, so if we add mappedBy attribute to Person entity then the relationship will become bi-directional.

SUMMARY

Existem várias coisas a notar no mapeamento acima:

  • A associação @ManyToOne utiliza FetchType.LAZY porque, caso contrário, voltaríamos ao EAGER fetching que é ruim para o desempenho.
  • As associações bidirecionais devem ser sempre atualizadas em ambos os lados, portanto o lado dos Pais deve conter a combinação addChild e removeChild(O lado dos Pais contém dois métodos de utilidade addBranches e removeBranches). Estes métodos garantem que sempre sincronizamos ambos os lados da associação, para evitar problemas de corrupção de objetos ou dados relacionais.
  • A entidade criança, Branch, implementa os métodos igual e hashCode. Uma vez que não podemos confiar num identificador natural para a verificação da igualdade, precisamos de utilizar o identificador da entidade em vez disso. Contudo, precisamos de o fazer correctamente para que a igualdade seja consistente em todas as transições de estados de entidade. Como confiamos na igualdade para os removeBranches, é uma boa prática sobrepor iguais e hashCode para a entidade filha em uma associação bidirecional.
  • A associação @OneToMany é por definição uma associação de pais, mesmo que seja unidirecional ou bidirecional. Apenas o lado pai de uma associação faz sentido para cascata as transições de estado da sua entidade para crianças.

Deixe uma resposta

O seu endereço de email não será publicado.