Exclusão lógica utilizando Hibernate

Em praticamente todos os projetos de software em que trabalhamos temos as funcionalidades conhecidas como CRUD. Porém nem sempre o delete do CRUD significa que devemos remover a informação do banco. Algumas vezes temos que apenas desativar o registro, porém mantê-lo por motivos de histórico ou auditoria.

Uma solução comum para este problema é utilizar a exclusão lógica, criando uma coluna que indica se um determinado registro está ativo ou não, e alterando a funcionalidade de exclusão para não disparar um delete. Em vez disso, um update alterando o valor desta coluna.

Utilizando o Hibernate em nosso projeto, podemos ter uma classe com um atributo que indica se o registro está ativo ou não, da seguinte maneira:

@Entity
public class Cliente {
    @Id @GeneratedValue
    private Long id;

    private String nome;

    private String cpf;

    private Boolean ativo;

    // getters e setters caso necessarios

}

E então modificamos a nossa lógica de exclusão para apenas atualizar o registro:

public class ClienteDAO {
    private final Session session;

    public ClienteDAO(Session session) {
        this.session = session;
    }

    public void excluir(Cliente cliente) {
        cliente.setAtivo(false);
        session.update(cliente);
    }
}

Esta é uma solução simples e comum para implementar uma funcionalidade de exclusão lógica, mas alguns recursos do hibernate permitem uma abordagem muito interessante. Podemos alterar como o Hibernate trabalhar com a exclusão de registros, através da anotação @SQLDelete :

@Entity
@SQLDelete(sql = "update Cliente set ativo = 0 where id = ?")
public class Cliente {

    @Id @GeneratedValue
    private Long id;

    private String nome;

    private String cpf;

    private Boolean ativo;
}

A anotação @SQLDelete serve para sobrescrevermos a instrução SQL que será enviada para o banco de dados, quando o método delete for invocado em uma Session do Hibernate. Com o uso desta anotação, não precisamos alterar a lógica de exclusão existente.

public void excluir(Cliente cliente) {
    session.delete(cliente);
}

Mas agora temos um novo problema: nossa lógica de pesquisa está retornando os registros que estão inativos caso não sejamos explícitos em relação a essa coluna. Bastaria então alterar a query de consulta para filtrar os registros, trazendo apenas os que estão ativos:

public List ativos() {
    return session.createQuery("from Cliente where ativo = true").list();
}

Mas ainda podem haver dezenas de outras queries de pesquisa, onde também não queremos trazer os registros inativos. Seria muito trabalhoso ter que alterá-las uma a uma e toda nova busca não esquecer de adicionar este parâmetro. Podemos também alterar a forma de buscar registros com a anotação @Where do Hibernate:

@Entity
@SQLDelete(sql = "update Cliente set ativo = 0 where id = ?")
@Where(clause = "ativo = 1")
public class Cliente {
    //resto do código omitido
}

Na anotação @Where temos o atributo clause, onde informamos um filtro que será aplicado em todas as consultas, na entidade Cliente, realizadas via HQL ou Criteria. Desta maneira nossa lógica de pesquisa permanece sem alterações:

public List ativos() {
    return session.createQuery("from Cliente").list();
}

Um último problema: Como visualizar os registros que estão inativos? Bem, como estamos utilizando a anotação @Where, o Hibernate vai adicionar o filtro definido nesta anotação em todas as consultas efetuadas na entidade Cliente. Portanto mesmo com uma query session.createQuery("from Cliente where ativo = false").list(); esta consulta retornará zero registros, pois o código SQL gerado por esta query será algo como:

select
    cliente0_.id as id0_,
    cliente0_.ativo as ativo0_,
    cliente0_.cpf as cpf0_,
    cliente0_.nome as nome0_
from
    Cliente cliente0_
where
(
    cliente0_.ativo = 1
)
and cliente0_.ativo=0

Infelizmente não há como desativar o @Where em uma consulta específica. Para recuperarmos os registros inativos podemos usar SQL nativo:

public List inativos() {
    return session.createSQLQuery("select * from Cliente where ativo = 0").addEntity(Cliente.class).list();
}

Como a query para buscar os registros inativos é bem simples, não há tantos problemas em utilizar SQL nativo, mas certamente não é elegante.

Existem outras maneiras de obter resultados parecidos, e uma delas é aplicando filtros em consultas, utilizando as anotações @FilterDef e @Filter:

@Entity
@FilterDef(name = "clientesAtivos")
@Filter(name = "clientesAtivos", condition = "ativo = 1")
public class Cliente {
    //resto do código omitido
}

Precisamos dessas duas anotações, pois podemos criar um filtro que recebe parâmetros em tempo de execução, e neste caso os parâmetros devem ser definidos no atributo parameters da anotação @FilterDef, e referenciados no atributo condition da anotação @Filter. Um exemplo deste caso seria:

@Entity
@Filter(name = "clientesAtivosOuInativos", condition = "ativo = :status")
@FilterDef(name = "clientesAtivosOuInativos", parameters = {@ParamDef(name = "status", type = "boolean")})
public class Cliente {
    private Boolean ativo;

    //resto do código omitido
}

Para utilizar este filtro em alguma query devemos ativá-lo primeiro, pois por padrão o Hibernate desconsidera todos os filtros em qualquer query. Essa é uma vantagem em relação ao @Where, já que você pode decidir se irá levá-los em consideração ou não. Fazemos isto com session.enableFilter("clientesAtivos");.

No caso de um filtro que recebe parâmetros definidos, passamos estes parâmetros da seguinte maneira: session.enableFilter("clientesAtivosOuInativos").setParameter("status", true);.

Um outro cenário onde a utilização da anotação @Filter se encaixa bem é quando temos uma aplicação que atende a vários clientes, e um cliente não pode ter acesso aos dados dos outros clientes. Neste tipo de aplicação, também conhecida como Multitenancy, podemos criar um filtro que recebe como parâmetro o id do cliente logado, e utilizar este filtro em todas as queries da aplicação.

Mais detalhes sobre Multitenancy pode ser visto neste post: Um produto para muitos clientes: implementando multitenancy. Lembrando que essas anotações usadas aqui são específicas do Hibernate, e não fazem parte da especificação JPA. Quem sabe em alguma próxima versão?

O projeto com os códigos mostrados neste post pode ser visto em: http://github.com/rcaneppele/hibernate-delete-logico

Para casos mais avançados, com possibilidade de auditoria detalhada, o Hibernate Envers (agora JBoss Envers) possibilita as mais diversas configurações.

Para quem quer aprender JPA com o Hibernate, a Editora Casa do Código lançou o livro que mostra como usar as tecnologias em conjunto com o framework JSF.

Você já teve que implementar um delete lógico na sua empresa? O que você usou?

25 Comentários

  1. Caio Ribeiro Pereira 21/08/2012 at 11:37 #

    Cara muito show esse post!!

    Seguindo essas dicas consegue-se deixar mais semântico o DAO da aplicação, parabéns pela dica!

  2. Arthur Moura Carvalho 21/08/2012 at 11:42 #

    Interessante Rodrigo!
    Eu e um colega estavamos definindo uma questão arquitetural dessas, para um SaaS. Se deveríamos fazer separação lógica ou física do banco de dados de cada cliente.
    Acabou que vimos que o servidor PaaS que iremos usar suporta a separação dos dados por namespaces, que acredito ser separação lógica.

    Mandou bem no artigo. Sucesso!

  3. Renato 21/08/2012 at 14:12 #

    Muito legal o post. Já havia visto esta abordagem antes mas não explicando com detalhe a parte da filtragem dos registros ativos. Ainda por cima da uma pincelada em Multitenacy, não tinha pensado que acaba sendo uma forma de se implementar! Valeu!! 🙂

  4. Washington Botelho 22/08/2012 at 10:31 #

    Ótimo post Rodrigo!

    Não faz muito tempo que desenvolvi um projeto com vários `ativo = 1` nas queries. Bem, agora aprendi uma melhor forma de fazer tais consultas.

    Abraço.

  5. Tiago 22/08/2012 at 10:38 #

    É uma abordagem interessante. Já usei muito o @Where, e acho que é importante citar que a cláusula que vai na anotação deve ser escrita em SQL (do banco, não HQL ou JPQL).

  6. Bruno Nascimento 22/08/2012 at 10:46 #

    olha o peter !! haha parabéns !!

  7. Guilherme 22/08/2012 at 10:55 #

    Bom dia! Muito bom o post, parabéns. Eu passei por problema parecido aqui na empresa, tivemos uma solução não muito elegante, mas ainda sim um problema persistiu.

    Quando temos uma entidade que carrega consigo alguns relacionamentos um para muitos, estas listas penduras nesta entidade podem vir eager ou lazy, no meu caso em lazy. Quando faço uma pesquisa nesta entidade, faço um JOIN com estes relacionamentos, tentando colocar a regra de exclusão lógica, mas a lista vem em modo lazy e o filtro não se aplica corretamente. Se eu fizer um fetch, não consigo colocar condições no relacionamento. Em qualquer uma das formas eu tenho o mesmo problema: Trago um objeto e quando acesso os relacionamentos dele, que deveriam estar filtrados, estão todos os registros lá, inclusive os excluidos.

    Como você procederia neste caso ?!
    Muito obrigado.

  8. orlando 22/08/2012 at 11:11 #

    ótimo post! Muito Interessante

  9. Elon 22/08/2012 at 11:17 #

    Muito bom e útil o artigo

    parabéns e obrigado

  10. Rodrigo Ferreira 22/08/2012 at 11:19 #

    Oi Tiago, bem lembrado, na anotação @Where escrevemos o filtro utilizando SQL nativo mesmo. Valeu!

  11. Rodrigo Bispo 22/08/2012 at 11:57 #

    Fala camarada! Muito bacana e construtivo o post! Anotado! 😉 Abraço

  12. Alayr Sobrinho 22/08/2012 at 13:20 #

    Para fins de auditoria teríamos que ter mais dados do momento da exclusão (usuário que excluíu, data, hora etc).
    O que, a meu ver, ficaria complicado guardar na própria entidade.
    Eu usaria o que já saiu na própria MJ que seria capturar o evento pré-exclusão, copiar ou serializar a entidade para outra tabela, junto com os dados da operação.

    Para coisas mais simples a solução demonstrada é ideal, o que nos mostrou o poder do Hibernate e como ele sempre está 5 passos à frente da JPA.

  13. Rodrigo Ferreira 22/08/2012 at 14:43 #

    @Guilherme
    Neste caso, onde sua entidade possui relacionamento com outra entidade que tb possui exclusão lógica, vc pode colocar a Anotação @Where no atributo que indica o relacionamento. Algo como:

    @Where(clause = "ativo = 1")
    @OneToMany(mappedBy = "cliente")
    private List<Telefone> telefones;
    

    Pois desta maneira sempre que a lista de Telefones for carregada, via join ou via EAGER, o Hibernate tb vai aplicar o filtro na hora de carregar os telefones.

    Espero ter ajudado, qualquer dúvida pode perguntar.
    Abraço!

  14. Marcelo Mrack 22/08/2012 at 19:26 #

    To muito velho pra isso… lembro la dos idos de 2001-02 quando eu baixava o source do Hibernate e gerava patch e incrementos (o meu colega K. ainda me xinga nos churrascos do patch de delete N-N – o famoso dissociate em memoria – onde eu esqueci de concatenar o where… e eles ficaram o fim de semana todo recuperando dados da base de planejamento estrategico da universidade…).

    Enfim, o Hibernate evoluiu muito desde entao, e hoje eu nao mexo mais com isso… mas dou parabens a todos voces “gurizada” que ficam trabalhando com isso… realmente as vezes os dedos se coçam pra programar de novo!

    Otimo post.

  15. Alan Diniz 05/09/2012 at 10:01 #

    cara que post show… muito bom mesmo… irei estudar e fazer alguns testes…

  16. Freitas 05/09/2012 at 21:59 #

    irei implementar de agora em diante esta idéia. show de bola mesmo!

  17. Foster 15/09/2012 at 22:50 #

    Ótimo post Rodrigo!

    Vou começar a colocar em prática essas anotações nos projetos aqui da empresa, principalmente em um que estou trabalhando que serve para gerenciar os nossos índios… 😀

    Abraços!

  18. Carlos Victor 22/10/2012 at 11:38 #

    Sensacional o post… quase todos nós já sofremos com entidades que precisam ser deletadas logicamente. Parabéns!

  19. Vanderson assis da silva 30/11/2012 at 18:44 #

    Como sempre a Caelum mostrando programação de alto nível. Keep it up!!

  20. thwess 07/12/2012 at 17:34 #

    Muito bom o artigo, espero utilizar essa técnica o mais breve possível em um projeto que já estou desenvolvendo.

    Parabéns e obrigado.

  21. João Henrique 10/12/2012 at 10:03 #

    Realmente muito interessante esse post sobre hibernate.

  22. jabi 03/10/2014 at 11:16 #

    Olá, pessoal e aplicada no tipo cascade também?

  23. Rodrigo Ferreira 30/10/2014 at 15:30 #

    Olá jabi,
    Desculpe a demora pela resposta.

    Funciona com cascade sim!
    Basta adicionar o parâmetro cascade na annotation de relacionamento, e as classes de relacionamento também deverão ser anotadas com @SQLDelete

    Abraços!

  24. Lucas Cavalcante 10/02/2015 at 10:56 #

    Muito bom esse post! Porém se tratando de uma empresa grande com um grande numero de dados salvos no banco de dados, isso não deixaria o banco mais “pesado” não?

  25. Rodrigo Ferreira 10/02/2015 at 20:14 #

    Oi Lucas,

    Acredito que não fica mais lento, pois apenas uma nova coluna de status foi adicionada, e nas queries sempre trazemos os registros ativos.

    O problema seria se o banco tivesse milhares de registros e fosse executada uma consulta que carregasse todos os dados de uma vez, sem nenhum filtro ou paginação. Ai será muito lento, podendo até causar um estouro de memória.

    Nesse outro post dou algumas dicas de como lidar com esse cenário de volume de dados enorme: http://blog.caelum.com.br/trabalhando-com-batch-processing-de-maneira-eficaz-utilizando-a-jpa/

    Abraços!

Deixe uma resposta