Dentre todas as funcionalidades que o Hibernate provê, talvez uma das que mais chamem a atenção dos desenvolvedores seja a remoção de valores, motivada principalmente pela grande importância e impacto que a mesma gera em uma aplicação, dada a relevância que as informações (os dados manipulados) tem para a mesma.
Diante disso, este artigo visa atingir exatamente essa lacuna que muitos programadores têm em relação à utilização dos mecanismos que o framework ORM dispõe para efetuar a deleção de valores da base de dados, via famosas annotations. Dentre estes mesmos mecanismos, podemos destacar a função do atributo “orphanRemoval”, presente na maioria das anotações de relacionamento entre entidades, e que serve para definir a forma como uma ação de remoção atribuída a um objeto terá impacto sobre os objetos relacionados.
Para tal, efetuaremos a criação de um projeto de exemplo usando as tecnologias Java, Spring e Hibernate (todas em suas versões mais recentes).
Configurando e Criando o projeto
Inicialmente, efetue o download de todas as dependências que o projeto necessitará para funcionar corretamente (Listagem 1). Considere a utilização da IDE Eclipse (também em sua versão mais recente) para o mesmo.
A mesma lista pode ser baixada através da opção do link de download desse projeto acima do artigo. Note que a Listagem 1 apresenta a lista de libs sem a versão em específico a ser usada, isso porque você pode sempre usar as referências mais recentes das mesmas, ou até mesmo configurar teu projeto para usar um repositório maven, por exemplo.
org.springframework.aop-xxx.RELEASE.jar
org.springframework.asm-xxx.RELEASE.jar
org.springframework.aspects-xxx.RELEASE.jar
org.springframework.beans-xxx.RELEASE.jar
org.springframework.context.support-xxx.RELEASE.jar
org.springframework.context-xxx.RELEASE.jar
org.springframework.core-xxx.RELEASE.jar
org.springframework.jdbc-xxx.RELEASE.jar
org.springframework.orm-xxx.RELEASE.jar
org.springframework.transaction-xxx.RELEASE.jar.
org.springframework.expression-xxx.RELEASE.jar
commons-logging-xxx.jar
log4j.jar
aopalliance-xxx.jar
dom4j-xxx.jar
hibernate-commons-annotations-xxx.Final.jar
hibernate-core-xxx.Final.jar
hibernate-jpa-2.0-api-xxx.Final.jar
javax.persistence-xxx.jar
jta-xxx.jar
javassist-xxx.jar
slf4j-api-xxx.jar
mysql-connector-java-xxx-bin.jar
commons-collections-xxx.jar
Imagine o seguinte cenário: Temos uma aplicação de CRUD básica que foi toda configurada para salvar informações cruzadas de duas entidades simples: Pessoa e Endereco. Ambas entidades estão relacionadas através de um relacionamento Many-to-Many via anotações em modelo Hibernate e usando as tecnologias referidas até o momento.
Um desenvolvedor experiente em Hibernate ORM estaria pronto para fazer o sistema e lidar com a camada de persistência usando o próprio Hibernate. Para simplificar, ele estará usando um aplicativo stand alone para persistir as informações das pessoas. Veja então como a divisão de tarefas se dará para esse modelo em específico:
- Desenho do modelo de dados (tabelas);
- Classes de domínio e mapeamentos do Hibernate;
- Classes DAO & Service ;
- Configuração do Spring para a aplicação;
- Uma classe principal simples para mostrar como tudo funciona.
Desenho do modelo
O design do banco de dados será relativamente simples, contendo apenas de três tabelas simples com um relacionamento muitos-pra-muitos conforme mencionado anteriormente e mostrado na Figura 1.
Como já é de conhecimento do leitor, a tabela intermediária servirá como suporte para salvar todas as informações cruzadas entre as duas entidades principais. Ela será, também, de extrema importância para demonstrarmos o bom uso do atributo orphanRemoval.
Classes de domínio e mapeamento do Hibernate
O primeiro trabalho a ser realizado programaticamente será o de criar e mapear as entidades, mapeando-as corretamente segundo as anotações do Hibernate. Lembrando que não será foco deste artigo tratar conceitos relacionados à finalidade de cada anotação ou como usá-las. Pressupõe-se que o leitor já tenha tais conhecimentos.
Crie a primeira entidade dentro de um pacote chamado “br.com.devmedia.hibernate.removal_example” e acrescente à mesma o código referente à Listagem 2.
package br.com.devmedia.hibernate.removal_example;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.OneToMany;
import javax.persistence.Table;
@Entity
@Table(name = "Pessoa")
public class Pessoa implements Serializable {
/**
* Serial Version UID
*/
private static final long serialVersionUID = -4221733194426887403L;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "idPessoa")
private Long idPessoa;
@Column(name = "nome")
private String nome;
@Column(name = "idade")
private Integer idade;
@Column(name = "data_nasc")
private Date dtNasc;
@OneToMany(cascade = { CascadeType.ALL }, fetch = FetchType.EAGER, orphanRemoval = true)
@JoinTable(name = "PessoaEndereco",
joinColumns = { @JoinColumn(name = "idPessoa") },
inverseJoinColumns = { @JoinColumn(name = "idEndereco") })
)
private List<Endereco> listaEnderecos = new ArrayList<Endereco>(0);
/**
* @return o idPessoa
*/
public Long getIdPessoa() {
return idPessoa;
}
/**
* @param idPessoa
* o idPessoa a ser configurado
*/
public void setIdPessoa(Long idPessoa) {
this.idPessoa = idPessoa;
}
/**
* @return o nome
*/
public String getNome() {
return nome;
}
/**
* @param nome
* o nome a ser configurado
*/
public void setNome(String nome) {
this.nome = nome;
}
/**
* @return o idade
*/
public Integer getIdade() {
return idade;
}
/**
* @param idade
* o idade a ser configurado
*/
public void setIdade(Integer idade) {
this.idade = idade;
}
/**
* @return o dtNasc
*/
public Date getDtNasc() {
return dtNasc;
}
/**
* @param dtNasc
* o dtNasc a ser configurado
*/
public void setDtNasc(Date dtNasc) {
this.dtNasc = dtNasc;
}
/**
* @return o listaEnderecos
*/
public List<Endereco> getListaEnderecos() {
return listaEnderecos;
}
/**
* @param listaEnderecos o listaEnderecos a ser configurado
*/
public void setListaEnderecos(List<Endereco> listaEnderecos) {
this.listaEnderecos = listaEnderecos;
}
}
Como estamos usando o MySQL como banco de dados principal, optamos pela estratégia GeneratedValue como GenerationType.AUTO que irá fazer o incremento automático sempre que uma nova pessoa e/ou endereço forem criados. Todos os outros mapeamentos são familiares.
Atente-se também para os mapeamentos corretos da anotação @Column, uma vez que a mesma definirá o valor correto da coluna na base de dados. Portanto, se estiver usando suas próprias definições tome cuidado no momento de mapeá-las.
Logo após, faça o mesmo procedimento agora para a entidade Endereco. Crie uma nova classe no mesmo pacote e insira na mesma o código referente à Listagem 3.
package br.com.devmedia.hibernate.removal_example;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name = "Endereco")
public class Endereco implements Serializable {
/**
* Serial Version UID
*/
private static final long serialVersionUID = -1314843665707276799L;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "idEndereco")
private Long idEndereco;
@Column(name = "logradouro")
private String logradouro;
@Column(name = "numero")
private Integer numero;
@Column(name = "cep")
private Integer cep;
/**
* @return o idEndereco
*/
public Long getIdEndereco() {
return idEndereco;
}
/**
* @param idEndereco
* o idEndereco a ser configurado
*/
public void setIdEndereco(Long idEndereco) {
this.idEndereco = idEndereco;
}
/**
* @return o logradouro
*/
public String getLogradouro() {
return logradouro;
}
/**
* @param logradouro
* o logradouro a ser configurado
*/
public void setLogradouro(String logradouro) {
this.logradouro = logradouro;
}
/**
* @return o numero
*/
public Integer getNumero() {
return numero;
}
/**
* @param numero
* o numero a ser configurado
*/
public void setNumero(Integer numero) {
this.numero = numero;
}
/**
* @return o cep
*/
public Integer getCep() {
return cep;
}
/**
* @param cep
* o cep a ser configurado
*/
public void setCep(Integer cep) {
this.cep = cep;
}
}
O fato importante a observar aqui é a anotação @OneToMany (cascade = { CascadeType.ALL }, fetch = FetchType.EAGER, orphanRemoval = true). Aqui nós configuramos a propriedade orphanRemoval = true.
E o que isso faz exatamente? Digamos que você tem um grupo de pessoas cadastradas na base. E dizem que um super-herói dá errado. E então, precisamos remover uma destas pessoas da mesma. Com as configurações de Cascade simples do JPA isso não é possível, uma vez que elas não detectam registros filhos e você teria de acabar com o banco de dados apagando-as enquanto sua coleção ainda tivesse uma referência a algum deles.
Antes da JPA 2.0 você não teria suporte ao orphanRemoval e a única maneira de apagar registros órfãos (filhos) seria usar a anotação específica do Hibernate abaixo, que agora está obsoleta, por sinal:
@org.hibernate.annotations.Cascade(org.hibernate.annotations.CascadeType.DELETE_ORPHAN)
Agora que fizemos todo esse processo, vejamos como fica toda a parte de banco e classes de negócio.
Classes de Negócio e DAO
Para manter os bons padrões de design vamos separar a camada DAO (Data Access Object) da camada de serviço. Para isso, vamos precisar de uma interface DAO e sua respectiva implementação. Note que estamos usando o HibernateTemplate através do HibernateDaoSupport, de modo a manter-se longe de qualquer detalhe específico do Hibernate e acessar tudo de forma unificada usando o Spring.
Veja então nas Listagens 4 e 5 o código necessário para configurar a interface e classe de implementação do nosso DAO, com duas operações bem básicas: a criação ou atualização de uma entidade Pessoa, e um método de recuperação da mesma.
package br.com.devmedia.hibernate.dao;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import br.com.devmedia.hibernate.removal_example.Pessoa;
@Transactional(propagation = Propagation.REQUIRED, readOnly = false)
public interface PessoaDAO {
public void criaOuAtualizaPessoa(Pessoa pessoa);
public Pessoa recuperaPessoaPorId(Long id);
}
package br.com.devmedia.hibernate.dao;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.orm.hibernate3.support.HibernateDaoSupport;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import br.com.devmedia.hibernate.removal_example.Pessoa;
@Qualifier(value="pessoaHibernateDAO")
public class PessoaHibernateDAOImpl extends HibernateDaoSupport implements PessoaDAO {
@Override
public void criaOuAtualizaPessoa(Pessoa pessoa) {
if (pessoa.getIdPessoa() == null) {
getHibernateTemplate().persist(pessoa);
} else {
getHibernateTemplate().update(pessoa);
}
}
@Transactional(propagation = Propagation.NOT_SUPPORTED, readOnly = false)
public Pessoa recuperaPessoaPorId(Long id){
return getHibernateTemplate().get(Pessoa.class, id);
}
}
Observe que é na camada de interface onde definimos o tratamento das transações, conforme necessário. Isto é feito para que sempre que você não precisar de uma transação, você possa definir isso no nível do método do método específico, além disso em mais situações você vai precisar de uma transação com exceção dos métodos de recuperação de dados.
Na Listagem 5 definimos o @Qualifier para deixar o Spring saber que esta é a implementação Hibernate da classe DAO. Isso a meu ver é um bom conceito de design de se acompanhar onde você separa sua implementação em pacotes separados para manter o design limpo.
Perceba também que toda a implementação de ambos recursos se dá de forma rápida e totalmente integrada às funcionalidades dos recursos usados, fazendo uso de todo o poder que as anotações proveem.
Ok, vamos passar para a implementação da camada de serviço. A camada de serviço, neste caso, está apenas agindo como uma camada de mediação para chamar os métodos DAO. Mas em um aplicativo real, você provavelmente terá outras validações, os procedimentos relacionados à segurança etc tratadas na camada de serviço. Veja o código agora das Listagens 6 e 7.
package br.com.devmedia.service;
import br.com.devmedia.hibernate.removal_example.Pessoa;
public interface PessoaService {
public void handlePessoaCriarAtualizar(Pessoa pessoa);
public Pessoa recuperarPessoaPorId(Long id);
}
package br.com.devmedia.service.impl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import br.com.devmedia.hibernate.dao.PessoaDAO;
import br.com.devmedia.hibernate.removal_example.Pessoa;
import br.com.devmedia.service.PessoaService;
@Component("pessoaService")
public class PessoaServiceImpl implements PessoaService {
@Autowired
@Qualifier(value = "pessoaHibernateDAO")
private PessoaDAO pessoaDAO;
@Override
public void handlePessoaCriarAtualizar(Pessoa pessoa) {
pessoaDAO.criaOuAtualizaPessoa(pessoa);
}
public Pessoa retrieveJusticeLeagueById(Long id) {
return pessoaDAO.recuperaPessoaPorId(id);
}
}
Essa implementação também dispensa muitos comentários. Primeiramente, o @Component liga esta implementação do serviço com o nome pessoaService dentro do contexto do Spring para que possamos referir ao bean como um bean com um id de nome pessoaService.
Além disso, veja que demos um autowired na pessoaDAO e definimos uma @Qualifier para que ele se vincule à implementação do Hibernate.
O valor do Qualifier deve ser o mesmo nome que demos em nível de classe do Qualifier dentro da classe DAOImpl. E por último, veja a configuração do Spring que faz um wire em todos estes juntos. Veja, então na Listagem 8 a presença da configuração padrão para o Spring, em relação ao seu arquivo spring-context.xml.
<context:component-scan base-package="br.com.devmedia" />
<context:annotation-config />
<tx:annotation-driven />
<bean id="sessionFactory"
class="org.springframework.orm.hibernate3.annotation
.AnnotationSessionFactoryBean">
<property name="packagesToScan">
<list>
<value>br.com.devmedia.**.*</value>
</list>
</property>
<property name="hibernateProperties">
<props>
<prop key="hibernate.dialect"
>org.hibernate.dialect.MySQLDialect</prop>
<prop key="hibernate.connection.driver_class"
>com.mysql.jdbc.Driver</prop>
<prop key="hibernate.connection.url">
jdbc:mysql://localhost:3306/devmedia_test_removal
</prop>
<prop key="hibernate.connection.username"
>root</prop>
<prop key="hibernate.connection.password"
>sua_senha_aqui</prop>
<prop key="hibernate.show_sql"
>true</prop>
<prop key="hibernate.dialect"
>org.hibernate.dialect.MySQLDialect</prop>
</props>
</property>
</bean>
<bean id="pessoaDAO"
class="br.com.devmedia.hibernate.dao.PessoaDAOImpl">
<property name="sessionFactory" ref="sessionFactory" />
</bean>
<bean id="transactionManager"
class="org.springframework.orm.hibernate3.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory" />
</bean>
Note que aqui nós escondemos os detalhes da criação e cabeçalho do arquivo XML para simplificar a listagem. Na mesma listagem estamos usando o HibernateTransactionManager como uma instância para rodar o exemplo em stand alone. Se você estiver executando o mesmo exemplo em uma aplicação de servidor você precisará usar o gerenciador de transações JTA.
Nós também estamos usando o recurso de autocriação das tabelas na base de dados através do Hibernate (recurso padrão) para simplificar a implementação. A propriedade “packaagesToScan” instrui como scanear todos os subpacotes dentro do pacote raiz da aplicação, neste caso as classes anotadas com @Entity especificamente.
Finalmente, é possível observar também a presença da associação do session factory ao PessoaDAO em específico, o que nos permite trabalhar com o Hibernate Template.
Agora que tudo está praticamente configurado, temos de criar os objetos em si para verificar o funcionamento de toda essa programação.
Crie então uma nova classe de teste chamada TestesHibernate, com o método main criado para efeito de teste. E adicione na mesma o código presente na Listagem 9.
package br.com.devmedia.teste;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import br.com.devmedia.hibernate.removal_example.Endereco;
import br.com.devmedia.hibernate.removal_example.Pessoa;
import br.com.devmedia.service.PessoaService;
public class TesteHibernate {
/**
* @param args
*/
public static void main(String[] args) {
ApplicationContext ctx =
new ClassPathXmlApplicationContext("spring-context.xml");
PessoaService service =
(PessoaService) ctx.getBean("pessoaService");
Pessoa pessoa = new Pessoa();
List<Endereco> enderecos = getEnderecos();
pessoa.setListaEnderecos(enderecos);
pessoa.setDtNasc(new Date());
pessoa.setIdade(30);
pessoa.setNome("Devmedia");
service.handlePessoaCriarAtualizar(pessoa);
}
private static List<Endereco> getEnderecos() {
List<Endereco> enderecos = new ArrayList<Endereco>();
Endereco endereco1 = new Endereco();
endereco1.setCep(12345678);
endereco1.setLogradouro("Rua Marechal Deodoro");
endereco1.setNumero(123);
enderecos.add(endereco1);
Endereco endereco2 = new Endereco();
endereco2.setCep(87654321);
endereco2.setLogradouro("Rua Floriano Peixoto");
endereco2.setNumero(321);
enderecos.add(endereco2);
return enderecos;
}
}
Ao executar o código dessa listagem é possível observar na base a presença de duas novas informações persistidas. O restante do processo agora é mais simples, basta adicionar o método presente na Listagem 10 e executá-lo a partir do método main, ele será responsável por fazer a remoção dos dados e das dependências usando o orphanRemoval configurado anteriormente.
private void removerPessoa() {
ApplicationContext ctx =
new ClassPathXmlApplicationContext("spring-context.xml");
JusticeLeagureService service =
(JusticeLeagureService) ctx.getBean("pessoaService");
Pessoa pessoa = service.recuperarPessoaPorId(1l);
List<Endereco> enderecos = pessoa.getListaEnderecos();
for (int i = 0; i < enderecos.size(); i++) {
Endereco endereco = enderecos.get(i);
if (endereco.getLogradouro().equalsIgnoreCase("Rua Marechal Deodoro")) {
enderecos.remove(i);
break;
}
}
service.handlePessoaCriarAtualizar(pessoa);
}
E é isso. Se a execução funcionar corretamente você verá ao final da execução a remoção da primeira pessoa cadastrada com sucesso. Isso é apenas uma breve apresentação do que o Hibernate é capaz de fazer ante a sua especificação. Se algo não funcionar, comente no artigo que estarei pronto para ajudá-los. Valeu e até a próxima!