As ações de cascade em mapeamentos de entidades são facilitadores que nos ajudam no dia a dia de desenvolvimento quando utilizamos JPA, uma dessas ações facilitadoras é o cascade REMOVE, que aciona mecanismos de deleção quando removido objetos relacionados, ou seja, quando um objeto é deletado e ele possui mapeamento de cascade remove com outro objetos objeto relacionado também é aplicado a remoção no objeto relacionado, como no exemplo abaixo:
A entidade customer possui um relacionamento de um para um com address.
@Entity public class Customer { @NotNull @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true) private Address address; }
Quando chamarmos o delete em customer o address também será deletado devido o cascade ALL (é assumido todos os tipos de cascade:PERSIST,MERGE, DELETE, REFRESH e DETACH) no mapeamento.
customerRepository.delete(customerId);
Assim, será gerado o seguinte SQL:
Hibernate: delete from address where id=? Hibernate: delete from customer where id=?
Porém, devemos tomar cuidado quando utilizamos essa anotação em mapeamentos *ToMany, ou seja, com lista de objetos, porque ele vai executar uma instrução SQL para cada registro da lista como demonstrado no exemplo abaixo:
Por exemplo, uma Bill possui uma lista de BillItems, que é um mapeamento OneToMany configurado com cascade ALL.
@Entity public class Bill { @NotNull(message = "Bill items is required") @Size(min = 1, message = "The Bill needs at least one item") @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "bill") private Lis<BillItem> items = new ArrayList<>(); }
Devido esse mapeamento, quando removemos uma Bill o JPA sabe que os BillItem também devem ser removidos para manter a integridade dos dados no banco de dados.
billRepository.delete(id);
Porém, quando olhamos o SQL gerado por essa ação, nos deparamos com vários “deletes” sendo executados, isso ocorre, porque o JPA aplica uma instrução de delete para cada item da lista, como o exemplo abaixo a Bill possuía 10 BillItems então foram executados 10 deletes na base, com isso, um mapeamento que nos traz uma grande facilidade pode nos prejudicar em termos de performance, pois conforme a quantidade de registros relacionados, as operações no banco de dados podem ocasionar um gargalo de performance devido a quantidade de instruções.
Hibernate: delete from bill_item where id=? Hibernate: delete from bill_item where id=? Hibernate: delete from bill_item where id=? Hibernate: delete from bill_item where id=? Hibernate: delete from bill_item where id=? Hibernate: delete from bill_item where id=? Hibernate: delete from bill_item where id=? Hibernate: delete from bill_item where id=? Hibernate: delete from bill_item where id=? Hibernate: delete from bill_item where id=? Hibernate: delete from bill where id=?
Uma das alternativas para evitar esse gargalo de performance é deixa essa ação no próprio banco de dados, ou seja, não deixar a cargo do JPA realizar essa operação em cascata.
Para isso, existe a anotação @OnDelete do Hibernate, onde caso o generate ddl estiver ativo no projeto ele gera o “cascade delete” na constraint no banco de dados.
@NotNull(message = "Bill items is required") @Size(min = 1, message = "The Bill needs at least one item") @OneToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}, mappedBy = "bill") @OnDelete(action = OnDeleteAction.CASCADE) private Lis<BillItem> items = new ArrayList<>();
Obs: Caso o generate dll não estiver ativo, essa anotação não terá nenhum efeito, porém para que o mapeamento reflita exatamente como está o modelo de dados, eu particularmente acho válido deixar essa anotação no mapeamento, pois fica com mais clareza que tem uma ação de deleção sendo aplicada nesse relacionamento.
Com isso, como é uma alteração a nível de banco de dados deve ser feito um script para remover a constraint atual do relacionamento e criar uma nova com a instrução de “delete cascade” como demonstrado abaixo.
ALTER TABLE bill_item DROP CONSTRAINT bill_item_bill_id_fk; ALTER TABLE bill_item ADD CONSTRAINT bill_item_bill_id_fk FOREIGN KEY (bill_id) REFERENCES bill(id) ON DELETE CASCADE;
Obs: O exemplo acima é com a sintaxe do PostrgresSQL.
Após aplicado o “ON DELETE CASCADE” no relacionamento, quando executado um delete em bill via JPA, só será impresso um delete em bill, pois a deleção dos billItems fica a cargo do banco de dados e não da aplicação, dessa forma o banco de dados sabe a forma mais performática de executar essa instrução e evita o gargalo de performance na aplicação.
billRepository.delete(id); Hibernate: delete from bill where id=?
Uma outra alternativa em termos de performance seria não utilizar os recursos de cascade do JPA ou do banco de dados e implementar a remoção em duas ações como no exemplo abaixo:
@Transactional public void delete(Long id) { billItemRepository.deleteByBillItem(id); billRepository.delete(id); }
Até que enfim uma excelente explicação lógica com exemplo prático e funcional!
Muito bom, não só ajudou a resolver meu problema como clareou as ideias dando várias opções.
CurtirCurtir
Olá, como configurar o ddl generation para o @OnDelete ter efeito?
CurtirCurtir
Olá, é necessário adicionar ON DELETE CASCADE na foreign key
CurtirCurtir
Olá, Excelente artigo! Gostaria de saber que propriedade “generate ddl” é esta que tenho que adicionar ao hibernate para que a annotation @OnDelete funcione.
CurtirCurtir
Olá Pedro Augusto, obrigado pelo feedback. A annotation @OnDelete especifica que a operação de delete será executada pelo banco de dados com base no relacionamento, com isso é necesssário adicionar a annotation @OnDelete(action = OnDeleteAction.CASCADE) para ser gerado o cascade no relacionamento ou adicionar via script o cascade no relacionamento. Baseado nisso, para o @OnDelete funcionar o relacionamento deve possuir o ON DELETE CASCADE como no exemplo abaixo:
CONSTRAINT bill_item_bill_id_fk FOREIGN KEY (bill_id) REFERENCES bill(id) ON DELETE CASCADE
CurtirCurtir