30 set 2015
blog-digital-transformation

Utilizando o Hibernate Envers com Spring MVC

Muitas pessoas utilizam em suas entidades a anotação @Audited mas nunca precisaram de fato utilizar o Hibernate ORM Envers e recuperar os dados das tabelas de auditoria. Na maioria dos casos, costumam utilizar o Envers só para deixar o registro no banco para futuras necessidades.

Eu também nunca havia precisado usar mas recentemente precisei e foi difícil encontrar um site que continha exemplos de como recuperar esses dados de auditoria/versionamento por meio de código. Neste tutorial vou mostrar a implementação que usei, de uma maneira fácil e rápida para recuperar e listar esses dados que o Envers gera.

Tenho aqui uma entidade chamada Cliente para utilizar no exemplo:

@Entity
@Audited
public class Cliente extends AbstractEntity {

    /**
    *
    */
    private static final long serialVersionUID = 2054949375369790564L;

    @Column(name = "NOME")
    private String nome;

    @Column(name = "ABREVIATURA")
    private String abreviatura;

    @Column(name = "SITE")
    private String site;

    @ManyToOne
    @JoinColumn(name="COMPANY_ID", nullable = false)
    private Company company;

    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = false, fetch = FetchType.LAZY, mappedBy = "cliente")
    private Set projetos;

}

Criei outra entidade chamada CustomRevision para mapear todos os dados que eu quero armazenar quando o Envers criar um revision. Como pode perceber, coloquei a anotação: @RevisionEntity(CustomRevisionListener.class) que vai definir esta minha entidade como a entidade que o Envers usará para armazenar o histórico das revisions. Criei uma classe chamada CustomRevisionListener que vai ser um “interceptor” da classe RevisionListener e vai implementar o comportamento customizado que preciso na hora da criação da revision. Percebe-se que estendo a classe DefaultRevisionEntity, cuja possuí dois atributos: Id e Timestamp.

@Entity
@RevisionEntity(CustomRevisionListener.class)
public class CustomRevision extends DefaultRevisionEntity {

    /**
     * 
     */
    private static final long serialVersionUID = 1L;

    @Column(name = "USUARIO_NOME")
    private String userName;

    @Column(name = "USUARIO_ID")
    private Long userId;

    @Column(name = "DATA_ALTERACAO")
    private LocalDateTime dataAlteracao;

}

Na classe CustomRevisionListener fiz override no método newRevision que recebe um Object, esse parametro é a entidade que está sendo auditada/versionada, que no nosso caso é a entidade Cliente. Esse método é chamado toda vez que o Envers vai criar uma nova revision, assim, podemos instanciar nossa classe CustomRevision e setar os atributos que queremos nela. No exemplo adicionei o Id e o Nome do usuário que está realizando a alteração, você pode adicionar o atributo que quiser. Os atributos que vem da DefaultRevisionEntity (Id e Timestamp), é preenchido automaticamente pelo Envers através das anotações que eles possuem: @RevisionNumber e @RevisionTimestamp.

@Component
public class CustomRevisionListener implements RevisionListener {

    @Override
    public void newRevision(Object arg0) {

        DefaultUser usuarioLogado = UserUtils.getUserLogged();
        usuarioLogado = usuarioLogado != null ? usuarioLogado : new DefaultUser();

        CustomRevision cr = new CustomRevision();
        cr.setUserId(usuarioLogado.getId());
        cr.setUserName(usuarioLogado.getName());
        cr.setDataAlteracao(LocalDateTime.now());
    }
}

Agora que fizemos a parte de customização do armazenamento das revisions, vamos ao método para buscar esses registros. Criei um controller chamado AuditoriaController onde vou implementar o método que vai recuperar todas as revisions da entidade Cliente e passar para uma DTO para retornar os dados que me importam.
Para recuperar utilizaremos a classe AuditReader criada a partir da classe AuditReaderFactory. Com a instancia dela, chamo o método createQuery() e falo que quero as revisions da entidade Cliente com o método forRevisionsOfEntity, passando a entidade, o parametro para retornar somente a entidade e o parametro para retornar os registros excluídos dessa minha entidade. Esse método vai me retornar uma instancia da classe AuditQuery.

/**
 * 
 * @author GSuaki
 *
 */
@Transactional
@RestController
@RequestMapping(value = "/auditoria")
public class AuditoriaController {

    private static final String CLIENTE = "Cliente";

    @Autowired
    private SessionFactory sessionFactory;

    /**
     * @return currentSession
     */
    private Session getSession() {
        return sessionFactory.getCurrentSession();
    }

    @SuppressWarnings({ "unchecked" })
    public Set findAllRevisionsOfCliente() {

        Set dtos = new HashSet();

        AuditReader reader = AuditReaderFactory.get(getSession());

        AuditQuery query = reader.createQuery().forRevisionsOfEntity(Cliente.class, false, true);

        List result = query.getResultList();
    }

} 

A lista que a query retorna, é uma lista de Object[] e por isso faremos um foreach recuperando os Object[]. Dentro dessas instancias, o primeiro índice do vetor, é a entidade que você recuperou as revisios (Cliente), o hibernate não deixa ver os valores do vetor pois ele utiliza proxys. Faremos então um cast no primeiro índice e terá a entidade, o segundo índice, retorna CustomRevision que é a nossa entidade customizada do Envers e no terceiro índice, retorna o tipo da revision (RevisionType), que podem ser três: ADD, DEL e MOD. Com estes dados instanciamos a classe AuditoriaDTO e adiciono na lista que iremos retornar para o front-end.

/**
 * 
 * @author GSuaki
 *
 */
@Transactional
@RestController
@RequestMapping(value = "/auditoria")
public class AuditoriaController {

    private static final String CLIENTE = "Cliente";

    @Autowired
    private SessionFactory sessionFactory;

    /**
     * @return currentSession
     */
    private Session getSession() {
        return sessionFactory.getCurrentSession();
    }

    @SuppressWarnings({ "unchecked" })
    public Set findAllRevisionsOfCliente() {

        Set dtos = new HashSet();

        AuditReader reader = AuditReaderFactory.get(getSession());

        AuditQuery query = reader.createQuery().forRevisionsOfEntity(Cliente.class, false, true);

        List result = query.getResultList();

        for (Object[] o : result) {
            Cliente cliente = (Cliente) o[0];
            CustomRevision revision = (CustomRevision) o[1];
            RevisionType revisionType = (RevisionType) o[2];

            AuditoriaDTO dto = new AuditoriaDTO(cliente.getId(), revisionType, revision.getDataAlteracao(), CLIENTE, revision.getUserName(), revision.getUserId());

            dtos.add(dto);
        }

        return dtos;
    }

} 

Espero que tenham gostado, disponibilizarei os códigos aqui git:blogspot-java. Quaisquer dúvidas, sugestões e/ou reclamações, comentem!


Gabriel Suaki – Desenvolvedor de software na redspark.
GitHub: GSuaki
Twitter: @gsuaki

Gabriel Suaki
About Gabriel Suaki

Software Engineer at redspark Gabriel Suaki - Software Engineer at redspark.

Comments

  • Eduardo Ximendes
    fevereiro 17, 2017 Responder

    Bacana teu tutorial Gabriel, porém não encontrei o projeto no link do GitHub ;/

Leave a Comment