Independentemente do tamanho da aplicação, a necessidade de monitorar as ações realizadas por um usuário frente às diversas funcionalidades existentes são reais. Em um sistema de uma instituição financeira, por exemplo, existe inclusive uma motivação legal para que sejam registradas todas as alterações realizadas nos saldos das contas para uma eventual análise posterior.
Resguardar a integridade das informações administradas por um sistema geralmente é uma exigência da área de Segurança da Informação da corporação que é responsável por especificar, por exemplo, quais ações serão rastreadas, quais dados serão armazenados, por quanto tempo, quem poderá consultá-los etc. Essa demanda, por sua vez, é repassada à área de Tecnologia da Informação (TI) que geralmente é a detentora das informações e a responsável por proteger e garantir a consistência dos dados que na maioria das vezes, ficam internalizados em banco de dados relacionais. Dessa forma, cabe a equipe de TI decidir qual o mecanismo técnico e procedimento capaz de gerar um registro de histórico das ações executadas.
Existem algumas técnicas clássicas que são empregadas, como por exemplo, utilizar-se de recursos de banco, entre eles Stored Procedure e Triggers. O problema desse tipo de abordagem é o alto acoplamento existente entre a aplicação e o banco de dados.
Outra forma é através de código feito pela equipe de desenvolvimento que, para manter todas alterações realizadas nas tabelas auditadas, cria uma tabela exclusiva de histórico, além da tabela monitorada que mantém o estado vigente dos dados. Porém, este expediente requer ajustes em todo o código que lida com o banco, necessitando que a internalização ocorra primeiro para salvar os dados na tabela de histórico e depois para realizar a persistência na tabela auditada.
Hoje, a grande maioria dos projetos Java lançam mão de frameworks ORM (mapeamento objeto relacional) para a camada de persistência. Certamente um dos mais utilizados pela comunidade Java é o Hibernate, que permite a diminuição da complexidade existente na construção de aplicações que trabalham com banco de dados relacional. Em cima desse cenário, e diante da necessidade de rastrear as ações realizadas sobre as entidades de uma aplicação, surgiu o subprojeto Envers.
O Hibernate Envers oferece a organização do histórico das versões dos dados gerenciados pela aplicação, através das entidades mapeadas para a persistência JPA para auditar as modificações ocorridas em um dado registro. Dessa forma, com sua utilização, uma aplicação é capaz de gerir todas as modificações realizadas no seu banco de dados de forma fácil e não intrusiva.
Um processo de auditoria bem modelado deve fornecer algumas informações básicas sobre as operações realizadas sobre um sistema, como: quem consultou informações, quais informações foram essas, quem excluiu, o que foi excluído, o que foi editado, como estavam antes e como ficaram depois; quando foi feito, etc. De posse dessa fotografia e caso algum dado seja inserido ou alterado de forma equivocada, o analista terá modos para recuperar a versão antiga dos dados e inferir quem foi o autor daquela ação.
Neste artigo serão apresentadas algumas das principais características do Hibernate Envers, detalhando-as e mostrando na prática o controle feito em uma aplicação simples, completa e integrada com JSF, e que tem como um dos seus requisitos a necessidade de rastrear as operações efetuadas pelos seus usuários. As tecnologias utilizadas, além dos frameworks já mencionados, serão a linguagem Java, o Eclipse, o Apache Maven, o MySQL, o Tomcat e o Hibernate que viabilizará o contato entre a aplicação e o banco de dados.
Conhecento o Hibernate Envers
O Hibernate Envers é uma biblioteca que permite auditar classes persistentes através do controle de versões em mapeamentos objetos relacionais feitos através do Hibernate.
Para cada entidade mapeada auditada, uma tabela versionada é criada no banco de dados, contendo todo o histórico das alterações efetuadas sobre aquela entidade. Basicamente, cada transação realizada no banco é classificada como uma revisão (ao menos que essa transação não realize nenhuma modificação), sendo que cada nova revisão gera alimentação automática das tabelas que permitem o versionamento das classes persistentes. Em suma, cada vez que uma tabela sinalizada para auditoria sofre alterações em seus dados (registros), uma nova "versão" dela é gerada pelo Envers e armazenada em uma outra tabela, que contém como chave primária o atributo de revisão, além do tipo de operação e de todos os campos auditáveis da tabela alvo. Pode-se comparar o Envers a sistemas de controle de versão, como Subversion, CVS, Git que mantêm revisões globais para todas as mudanças ocorridas.
Dessa forma, o analista pode recuperar e consultar dados históricos sem muito esforço, sendo possível, por exemplo, verificar quais informações foram alteradas naquela revisão, o dia em que aquela alteração ocorreu, em qual momento, quem realizou tal mudança e até mesmo registrar outras informações que julgar imprescindíveis. Em cima disso, é possível identificar comportamentos indevidos da aplicação por parte dos seus usuários e até mesmo recuperar dados que não deveriam ter sofridos alterações.
Como vantagens da utilização desse framework pode-se citarr: permite auditar todas as entidades mapeadas pelo Hibernate; baseia-se em revisões; independe do fabricante para o banco de dados; agrega valor ao produto; reduz o custo de manutenção e aumenta a produtividade.
Outra vantagem do Envers (Easy Entity Versioning) é a sua fácil implementação, uma vez que sua utilização se resume a incluir no código da classe mapeada a anotação @Audited e inserir algumas poucas classes de listener na configuração do projeto. Feito isso, a aplicação está pronta para registrar as alterações ocorridas através da tabela de históricos referente a entidade que será criada automaticamente no banco de dados. Caso algum atributo da entidade não necessite ser auditado, pode-se lançar mão de outra anotação que é a @NotAudited.
Para que o Envers trabalhe de forma adequada, todas as entidades que passarão por processo de auditoria devem ter chaves primárias (identificadores exclusivos) imutáveis.
Configuração do Ambiente de Desenvolvimento
Após abordado o contexto do artigo e mencionado os principais objetivos deste trabalho, será dada ênfase a parte prática, com o desenvolvimento de uma aplicação web que armazenará além das informações default do framework, outras, como o usuário que fez a mudança assim como o IP de sua máquina.
O primeiro passo nesse processo é ter o ambiente de desenvolvimento corretamente configurado. O utilizado neste material foi o Eclipse Luna Java EE, por ser open-source, propiciar maior produtividade durante a construção e por ser amplamente utilizado pela comunidade Java. Trabalhando com o Eclipse, de forma integrada, será empregado o Apache Maven 3.1.1, que oferece maior simplicidade no gerenciamento do projeto e das bibliotecas.
Para a persistência dos dados será utilizado o SGBD (Sistema Gerenciador de Banco de dados) MySQL 6.3. Além disso, é necessário ter acesso, de forma local ou remota, a um servidor web que implemente as bibliotecas do Java EE para execução da aplicação.
Desenvolvendo o cadastro de alunos
O objetivo deste sistema será gerenciar o cadastro de alunos de uma instituição educacional e para isso será construído um formulário simples para internalizar as informações na base de dados.
Como o intuito maior desse artigo é abordar o Hibernate Envers, por questão de simplificação, o sistema conterá apenas duas entidades de domínio, chamadas Aluno e Usuário que não possuem relacionamento, e a partir daí serão construídas as ações do CRUD (Create-Read-Update-Delete).
As propriedades de um aluno serão: código, nome, matricula, CPF, email e cidade. Já o usuário possuirá nome e senha.
A Figura 1 mostra a representação gráfica da entidade de domínio do sistema.
Figura 1. Representação gráfica da tabela Aluno.
Criando a aplicação
Para iniciar a construção, crie um novo projeto Java, a partir do Maven no IDE e coloque as informações de identificação do projeto. Os campos Group ID, Artifact ID (nome do projeto), Packaging e Version devem ser preenchidos, respectivamente, com os seguintes valores: br.com.infoq, hibernate-envers-web, war e 0.0.1-SNAPSHOT.
Feito isso, tem-se o projeto criado. Porém, ainda é necessário atualizar o projeto para que sua estrutura seja construída e as configurações do Maven definidas. Portanto, siga até o menu Maven > Update Project, selecione o projeto recém-criado e clique em OK.
Adicionando as dependências necessárias
Com a estrutura do projeto Maven pronta, o próximo passo é inserir as bibliotecas que serão empregadas no projeto. Isso é feito diretamente configurando o arquivo pom.xml, local onde está contida a configuração principal do Maven, através da adição de dependências. As seguintes serão adicionadas:
- Hibernate Core (5.2.6);
- Hibernate Envers (5.2.6);
- Driver do MySQL (5.1.34);
- JSF API (2.2.13);
- JSF IMPL (2.2.13);
- Java EE API (7.0);
- Javax Servlet API (3.1.0).
Após adicionadas as dependências, é necessário atualizar o projeto para que as bibliotecas sejam baixadas e incorporadas ao Build Path do projeto. Dessa forma, clique em Maven > Update Project.
A Listagem 1 mostra trecho do arquivo pom.xml contendo todas as dependências adicionadas.
Listagem 1. Configuração do arquivo pom.xml.
<dependencies> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.34</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-core</artifactId> <version>5.2.6.Final</version> </dependency> <dependency> <groupId>com.sun.faces</groupId> <artifactId>jsf-api</artifactId> <version>2.2.13</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-envers</artifactId> <version>5.2.6.Final</version> </dependency> <dependency> <groupId>com.sun.faces</groupId> <artifactId>jsf-impl</artifactId> <version>2.2.13</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax</groupId> <artifactId>javaee-api</artifactId> <version>7.0</version> </dependency> </dependencies>
Criando, mapeando e auditando
Através de anotações no código, a partir de agora, será criado e definido o mapeamento objeto/relacional da entidade, e também sinalizado ao Envers qual entidade sofrerá auditoria.
Na organização do projeto, cada tipo de classe será colocado em um pacote diferente. Sendo assim, a entidade Aluno ficará no pacote br.com.infoq.entidade, devendo o mesmo ser criado. O código da classe Aluno é apresentado na Listagem 2.
Listagem 2. Código da classe Aluno.
package br.com.infoq.entidade; //imports omitidos @Audited @Table @AuditTable(value="aluno_auditoria") @Entity public class Aluno implements Serializable { private static final long serialVersionUID = 1L; public Aluno(){} @Id @Column @GeneratedValue(strategy = GenerationType.AUTO) private int codigo; @Column public String nome; @Column public int matricula; @Column public String cpf; @Column public String cidade; @Column public String email; // construtor e métodos gets e sets omitidos }
A anotação principal referente ao Hibernate Envers e que indica que a entidade e suas propriedades passarão por auditoria é a @Audited e pode ser vista na linha 5. Outra anotação da API do Envers aparece na linha 7, @AuditTable, que permite customizar o nome da tabela que será criada para armazenar todas as alterações sofridas pela entidade auditada. Quando não se utiliza essa marcação, o framework utiliza a nomenclatura padrão que consiste do nome da entidade auditada com o sufixo _AUD. As demais anotações são referentes ao Hibernate Core.
Outra classe do domínio da aplicação é a Usuario que será criada dentro do mesmo pacote. Por questão de simplicidade, essa classe não será persistida, nem mesmo enfrentará o processo de auditoria. O motivo da sua criação é para permitir identificar o usuário que estará logado na aplicação e assim poder associar o identificador do usuário as alterações feitas na aplicação. O código da classe Usuário pode ser visualizado na Listagem 3.
Listagem 3. Código da classe Usuário.
package br.com.jm.entidade; public class Usuario { private String user; private String pwd; // construtor e métodos gets e sets omitidos }
Criando o Controller da aplicação
A camada de controle é composta por elementos (Managed Beans) cuja função é controlar o fluxo de processamento e estabelecer a ligação entre a camada de visão e o modelo.
Os Managed Beans serão criados dentro do pacote br.com.infoq.mb. Uma das classes desse pacote é a UsuarioBean que tem como função controlar as ações do login e logoff da aplicação. Seu código pode aparece na Listagem 4.
Listagem 4. Código da classe UsuarioBean.
package br.com.infoq.mb; //imports omitidos @ManagedBean @SessionScoped public class UsuarioMB implements Serializable { private static final long serialVersionUID = 1L; private Usuario usuario = new Usuario(); public String validateUsernamePassword() { String login = "matheus"; String senha = "1234"; if (login.equals(usuario.getUser()) && senha.equals(usuario.getPwd())) { HttpSession session = SessionBean.getSession(); session.setAttribute("username", usuario.getUser()); return "menu"; } else { return "login"; } } public String logout() { HttpSession session = SessionBean.getSession(); session.invalidate(); return "login"; } //métodos gets e sets omitidos }
Na linha 13, o método validateUsernamePassword, verifica se os dados de acesso a aplicação foram inseridos corretamente e, caso isso se confirme, cria uma sessão, através do método estático getSession, da classe SessionBean, definindo uma variável como sendo o nome do usuário logado. Por questão de simplificação, foram definidos dentro da classe, nas linhas 14 e 15, o usuário e a senha da aplicação. Caso os dados de acesso sejam inseridos adequadamente o usuário será direcionado para o objeto view correto.
A Listagem 5 mostra a implementação da classe utilitária SessionBean, utilizada pelo bean.
Listagem 5. Código da classe SessionBean.
package br.com.infoq.util; //imports omitidos public class SessionBean { public static HttpSession getSession() { return (HttpSession) FacesContext.getCurrentInstance() .getExternalContext().getSession(false); } public static String getUserName() { HttpSession session = (HttpSession) FacesContext.getCurrentInstance() .getExternalContext().getSession(false); return session.getAttribute("username").toString(); } }
Outro controlador chama-se AlunoBean que tem como função controlar o módulo do sistema que cadastra os alunos, direcionando o fluxo para as várias operações disponíveis. O código desta classe é o visto na Listagem 6.
Listagem 6. Código da classe AlunoBean.
package br.com.jm.mb; //imports omitidos @ManagedBean @SessionScoped public class AlunoMB implements Serializable { private static final long serialVersionUID = 1L; private Aluno aluno = new Aluno(); private List<Aluno> lista = new ArrayList<Aluno>(); public void buscar(){ AlunoDAO alunoDAO = new AlunoDAO(); aluno = alunoDAO.getAlunoByMatricula(aluno.getMatricula()); } public String salvar() { AlunoDAO alunoDAO = new AlunoDAO(); alunoDAO.adicionarAluno(aluno); return "sucesso"; } public String editar(){ AlunoDAO alunoDAO = new AlunoDAO(); alunoDAO.atualizarAluno(aluno); return "sucesso_edi"; } public String remover() { AlunoDAO alunoDAO = new AlunoDAO(); alunoDAO.apagarAluno(aluno.getMatricula()); return "sucesso_del"; } //métodos gets e sets omitidos. }
Criando a camada view
O próximo passo será criar as páginas web por onde as informações do cadastro serão manipuladas, assim como uma tela de autenticação do usuário, que será a primeira criada.
Trata-se de um formulário simples que se comunica com o bean gerenciado UsuarioMB e que recebe os dados de acesso do usuário. A página deve ficar com o código semelhante ao apresentado na Listagem 7.
Listagem 7. Formulário de login.
<h:body> <h:form> <h3>Informe os dados de acesso:</h3> <h:outputText value="Username:" /> <h:inputText id="username" value="#{usuarioMB.usuario.user}"></h:inputText> <h:message for="username"></h:message> <br /><br /> <h:outputText value="Password:" /> <h:inputSecret id="password" value="#{usuarioMB.usuario.pwd}"></h:inputSecret> <h:message for="password"></h:message> <br /><br /> <h:commandButton action="#{usuarioMB.validateUsernamePassword}" value="Login" /> <h:commandButton value="Limpar" action="#{usuarioMB.limpar}" /> </h:form> </h:body>
A tela seguinte é o menu principal da aplicação e contém uma lista com todas as funcionalidades disponibilizadas pela aplicação. Esse arquivo é chamado de menu.xhtml. Um trecho do código pode ser visto através da Listagem 8.
Listagem 8. Menu Principal.
<h3>Bem vindo ao AcadêmicoNET</h3> <ul id="nav"> <li><h4>Aluno</h4> <ul> <li><a href="cadastro_aluno.xhtml">Cadastrar</a></li> <li><a href="remover_aluno.xhtml">Remover</a></li> <li><a href="listar_aluno.xhtml">Listar</a></li> <li><a href="editar_aluno.xhtml">Editar</a></li> </ul> </li> </ul>
O formulário de cadastro aparece no arquivo cadastro_aluno.xhtml e está representado através da Listagem 9.
Listagem 9. Formulário de cadastro de aluno.
<h:body> <h2>Preencha o formulário abaixo</h2> <h:form id="frmAluno" method="post"> <h:panelGrid columns="2" style="horizontal-align:center"> <h:outputText value="Nome:" /> <h:inputText value="#{alunoMB.aluno.nome}" /> <h:outputText value="Matrícula:" /> <h:inputText value="#{alunoMB.aluno.matricula}" /> <h:outputText value="CPF:" /> <h:inputText value="#{alunoMB.aluno.cpf}" /> <h:outputText value="Email:" /> <h:inputText value="#{alunoMB.aluno.email}" /> <h:outputText value="Cidade:" /> <h:inputText value="#{alunoMB.aluno.cidade}"/> </h:panelGrid> <h:commandButton action="#{alunoMB.salvar}" value="Enviar" /> <h:commandButton action="#{alunoMB.limpar}" value="Limpar" /> <input type='button' onclick='javascript:history.back()' value='Voltar' name='Voltar'/> </h:form> </h:body>
Configurando a aplicação
Após a implementação das camadas view, model e controller, o passo agora é informar ao Hibernate todas as informações do banco de dados ao qual ele se conectará. Essa configuração ficará acessível no arquivo hibernate.cfg.xml que deverá ser criado dentro da pasta src/main/resources. Seu conteúdo pode ser visto através da Listagem 10.
Listagem 10. Conteúdo do arquivo hibernate.cfg.xml.
<hibernate-configuration> <session-factory> <property name="hibernate.dialect">org.hibernate.dialect.MySQLDialect</property> <property name="hibernate.connection.driver_class">com.mysql.jdbc.Driver</property> <property name="hibernate.connection.url">jdbc:mysql://localhost:3306/escolabd?zeroDateTimeBehavior=convertToNull</property> <property name="hibernate.connection.username">root</property> <property name="hibernate.connection.password">1234</property> <property name="hibernate.show_sql">true</property> <property name="hibernate.hbm2ddl.auto">create</property> <mapping class="br.com.jm.entidade.Aluno"/> </session-factory> </hibernate-configuration>
Em versões anteriores do Hibernate Envers, neste ponto, era preciso definir alguns listeners que permitiam ao Envers controlar o processo de auditoria, inserindo registros nas tabelas versionadas de acordo com a conduta do usuário. No entanto, a partir da versão 5, essa configuração não é mais necessária, sendo realizada de forma automática.
Criando a conexão com o banco
Após criado o arquivo de configuração, nesse ponto será implementada a classe utilitária HibernateUtil que fará a ligação entre o hibernate.cfg.xml e o banco de dados, disponibilizando uma instância de SessionFactory para a aplicação. Essa classe ficará dentro do pacote br.com.infoq.util e seu código pode ser visto na Listagem 11.
Listagem 11. Código da classe HibernateUtil.
package br.com.infoq.util; //imports omitidos public class HibernateUtil { private static SessionFactory sessionFactory; public static SessionFactory getSessionFactory() { if (sessionFactory == null) { Configuration configuration = new Configuration().configure(); ServiceRegistry serviceRegistry = new StandardServiceRegistryBuilder() .applySettings(configuration.getProperties()).build(); sessionFactory = configuration.buildSessionFactory(serviceRegistry); SchemaUpdate se = new SchemaUpdate(configuration); se.execute(true, true); } return sessionFactory; } }
Persistindo a entidade Aluno
Agora que as classes mapeadas com as anotações de auditoria, o arquivo de configuração e a conexão com o banco foram implementadas, o passo agora é viabilizar as operações de persistência. O pacote em questão é o br.com.infoq.acessobd e a classe é a AlunoDAO, tendo seu código mostrado através da Listagem 12.
Listagem 12. Código da classe AlunoDAO.
package br.com.infoq.acessobd; //imports omitidos public class AlunoDAO { private static SessionFactory factory; public AlunoDAO() { factory = HibernateUtil.getSessionFactory(); } public Integer adicionarAluno(Aluno aluno) { Session session = factory.openSession(); Transaction tx = null; Integer cod_aluno = null; try { tx = session.beginTransaction(); cod_aluno = (Integer) session.save(aluno); tx.commit(); } catch (HibernateException e) { if (tx != null) { tx.rollback(); } e.printStackTrace(); } finally { session.close(); } return cod_aluno; } public List<Aluno> listarAlunos() { Session session = factory.openSession(); Transaction tx = null; List<Aluno> alunos = null; try { tx = session.beginTransaction(); alunos = session.createCriteria(Aluno.class).list(); tx.commit(); } catch (HibernateException e) { if (tx != null) { tx.rollback(); } e.printStackTrace(); } finally { session.close(); } return alunos; } public void atualizarAluno(Aluno aluno) { Session session = factory.openSession(); Transaction tx = null; Aluno aluno_upd = new Aluno(); try { tx = session.beginTransaction(); aluno_upd.setCidade(aluno.getCidade()); aluno_upd.setCodigo(aluno.getCodigo()); aluno_upd.setCpf(aluno.getCpf()); aluno_upd.setEmail(aluno.getEmail()); aluno_upd.setMatricula(aluno.getMatricula()); aluno_upd.setNome(aluno.getNome()); session.update(aluno_upd); tx.commit(); } catch (HibernateException e) { if (tx != null) { tx.rollback(); } e.printStackTrace(); } finally { session.close(); } } public void apagarAluno(Integer matricula) { Session session = factory.openSession(); Transaction tx = null; try { tx = session.beginTransaction(); Criteria criteria = session.createCriteria(Aluno.class); criteria.add(Restrictions.eq("matricula", matricula)); List<Aluno> results = criteria.list(); session.delete((Aluno)results.get(0)); tx.commit(); } catch (HibernateException e) { if (tx != null) { tx.rollback(); } e.printStackTrace(); } finally { session.close(); } } public Aluno getAlunoByMatricula(int matricula) { Session session = factory.openSession(); Transaction tx = null; Aluno aluno = null; try { tx = session.beginTransaction(); Criteria criteria = session.createCriteria(Aluno.class); criteria.add(Restrictions.eq("matricula", matricula)); List<Aluno> results = criteria.list(); aluno = ((Aluno)results.get(0)); tx.commit(); } catch (HibernateException e) { if (tx != null) { tx.rollback(); } e.printStackTrace(); } finally { session.close(); } return aluno; } }
Auditando informações adicionais
Nesse ponto, toda aplicação está configurada e funcional, portanto, pronta para ser executada. Porém, ainda não atende a um dos requisitos propostos no começo desse artigo que é a possibilidade de customizar informações, adicionando atributos extras junto à estrutura de auditoria do Hibernate Envers. Conforme mencionado anteriormente, além das informações default, duas outras informações devem ser registradas: usuário e o endereço IP.
Para tal, será utilizado o recurso de escuta de revisão do framework e criada uma classe que representará a entidade de revisão do projeto, sendo responsável por mapear todos os dados armazenados no momento da criação de uma nova revisão.
Sendo assim, dentro do pacote br.com.infoq.entidade, crie a classe AuditEntity. A Listagem 13 mostra como foi realizada sua implementação.
Na linha 5 a classe foi mapeada como uma entidade JPA, através da anotation @Entity. Além disso, é feita a alteração do nome da tabela central de auditoria do Envers, que, por padrão, leva o nome de "revinfo". Através do atributo "name" foi definido o novo nome como sendo "revinfo_cust".
A anotação @RevisionEntity, presente na linha 6, indica que essa é uma entidade de revisão e que será usada para armazenar o histórico das revisões. Nessa mesma linha é feita menção a classe AuditListener, que vai ser um "interceptor" da classe RevisionListener e implementará o comportamento customizado que será utilizado no momento da criação da revisão.
Os novos campos que serão inseridos na tabela de revisão aparecem definidos nas linhas 11 e 12. Os demais campos, que são default do framework, são incorporados através da classe DefaultRevisionEntity, estendida na linha 7.
Outra classe que deve ser criada é a AuditListener que estende AuditEventListener, como mostra a Listagem 14.
Listagem 13. Código da classe AuditEntity.
package br.com.infoq.entidade; //imports omitidos @Entity(name="revinfo_cust") @RevisionEntity(AuditListener.class) public class AuditEntity extends DefaultRevisionEntity { private static final long serialVersionUID = 1L; public String usuario; public String ip; //gets e sets omitidos }
Nesta classe, o método newRevision - linha 7 - foi sobrescrito. O mesmo recebe como parâmetro a entidade que está sendo auditada, que no caso é a Aluno. Esse método é chamado toda vez que o Envers criar uma nova revisão, assim, pode-se instanciar a classe AuditEntity e definir os atributos desejados. Nas linhas 9 e 10 informe o usuário que está realizando a alteração, e o IP de sua máquina. Os atributos que vêm da DefaultRevisionEntity (id e timestamp), são preenchidos automaticamente pelo Envers.
Listagem 14. Código da classe AuditListener.
package br.com.infoq.util; //imports omitidos public class AuditListener implements RevisionListener { @Override public void newRevision(Object revisionEntity) { AuditEntity revEntity = (AuditEntity) revisionEntity; revEntity.setUsuario(SessionBean.getUserName()); revEntity.setIp(SessionBean.getIP()); } }
Além da criação dessas duas classes, uma outra configuração deve ser incorporada ao arquivo hibernate.cfg.xml que representa a identificação da nova entidade criada. Portanto, adicione o seguinte trecho ao arquivo mencionado.
<mapping class="br.com.jm.entidade.AuditEntity"/>
Testando o funcionamento
Enfim, nesse ponto é possível testar a aplicação e ver o Envers trabalhando. Para execução da aplicação, clique com o botão direito do mouse sobre o projeto, acesse Run As > Run on Server, escolha o Tomcat como servidor e clique em Finish. Feito isso, a primeira tela que se aparecerá é a tela de login, conforme mostra a Figura 2. Portanto, informe os dados de acesso. Caso estejam corretos, uma sessão será criada para o usuário e o mesmo será direcionado para o menu da aplicação, mostrado pela Figura 3.
Figura 2. Tela de Login
Figura 3. Menu da Aplicação
A primeira ação executada frente ao sistema é o cadastro de um aluno. Após isso, será possível ver os efeitos do Hibernate Envers no banco de dados. Clique em Cadastrar na tela em questão e digite as informações do aluno na próxima tela que aparecer. Após clique em enviar, conforme Figura 4.
Figura 4. Tela de cadastro de aluno.
A Figura 5 mostra o resultado da primeira inserção efetuada no banco.
Figura 5. Visão do Banco de Dados.
Ao realizar o cadastro, além de ser inserido o registro na tabela aluno, duas novas tabelas foram criadas, sendo elas: aluno_auditoria que contém o histórico (modificação, criação e remoção) referente a entidade Aluno e a tabela revinfo_cust que indica em que momento foi criada a revisão e também armazena as informações personalizadas. Após o primeiro cadastro, veja a visão das duas tabelas, através da Figura 6.
Figura 6. Consulta Banco de Dados.
Na tabela aluno_auditoria, além de todos os atributos auditáveis da tabela aluno, duas novas colunas aparecem: REV e REVTYPE. A primeira refere-se à identificação da revisão e a segunda diz respeito ao tipo de operação realizada que pode ser 0, 1 ou 2, indicando, respectivamente, inserção, edição ou remoção. Veja que nesse caso, o número 0 indica que foi realizada a inclusão de um registro na tabela auditada.
Já a tabela revinfo_cust possui, por padrão, dois campos: id e timestamp. O primeiro simboliza a identificação da revisão, que também aparece na tabela aluno_auditoria, enquanto que o segundo indica o momento em que foi realizada a alteração. As demais colunas que aparecem nessa tabela foram customizadas.
Nesse ponto mais algumas ações sobre o banco de dados serão realizadas e os efeitos sobre as tabelas serão vistos. Será cadastrado mais três alunos. Veja o resultado na Figura 7.
Figura 7. Consulta Banco de Dados.
Perceba que os valores da coluna REV na tabela aluno_auditoria mudaram, pois são revisões diferentes. Lembrando que uma revisão no Envers representa uma transação. Já o valor da coluna REVTYPE se manteve o mesmo, pois em todos os 4 registros, a operação realizada sobre eles foi a de inserção.
Aqui será editado um dos alunos cadastrados e o resultado poderá ser visto.
Figura 8. Conteúdo da tabela aluno_auditoria.
A Figura 8 mostra o conteúdo da tabela aluno_auditoria. Foi editado o aluno de código 39301 sendo alterado a sua cidade de Uberlândia para Uberaba. Em virtude dessa ação, foi adicionado um novo registro nessa tabela. Note que agora na coluna REVTYPE aparece o valor 1 pois trata-se de uma alteração de registro.
Neste ponto, um registro será deletado. Analisando a consulta da Figura 9 veja que o aluno de código 2 foi deletado. O valor 2 para o campo REVTYPE indica que houve uma exclusão.
Figura 9. Consulta Banco de Dados.
Consulta e recuperação dos dados de auditoria
O Envers oferece um mecanismo que permite recuperar o histórico de mudanças de uma entidade através de queries que são semelhantes ao Hibernate Criteria. Isso pode ser feito através da interface AuditReader, que contém operações de busca, baseada nas entidades mapeadas.
Por exemplo, a Listagem 15 demostra como recuperar o número máximo de revisões produzidas pelas alterações feitas no banco. Para se ter acesso as revisões produzidas, primeiro deve-se criar uma instância de AuditReader, o que pode ser feito através do método getAuditReader, visível na linha 3.
Listagem 15. Recuperação das revisões.
public int getRevisions(){ Number revision = (Number) getAuditReader().createQuery() .forRevisionsOfEntity(Aluno.class, false, true) .setProjection(AuditEntity.revisionNumber().min()) .add(AuditEntity.id().eq(entityId)) .add(AuditEntity.revisionNumber().gt(42)) .getSingleResult(); return revision; }
Conclusão
Uma das principais características oferecidas pelo Hibernate Envers, além de sua simplicidade, é o ganho em produtividade e a redução de tempo empregado na manutenção. Enquanto, com outros recursos, perde-se muito tempo e utiliza-se muitas linhas de código, com esse framework apenas poucas anotações no código precisam ser inseridas.
Além disso, não é necessário ficar preso aos recursos default que a biblioteca oferece podendo adicionar metadados a uma revisão e customizar as informações, identificando por exemplo, o usuário que fez determinada alteração, ou indo além, determinando o IP da máquina do usuário, seu Sistema Operacional ou mesmo outras informações.
Referências
- Site oficial do Hibernate Envers.
Artigo originalmente publicado em Oracle Technology Network.
Carlos Alberto Silva (casilvamg@hotmail.com) é Formado em Ciência da Computação pelo Universidade Federal de Uberlândia (UFU), com especialização em Desenvolvimento Java pelo Centro Universitário do Triângulo (UNITRI). e em Análise e Desenvolvimento de Sistemas Aplicados a Gestão Empresarial no Instituto Federal do Triângulo Mineiro (IFTM). Trabalha na empresa Algar Telecom como Analista de Sistemas. Possui as seguintes certificações: OCJP, OCWCD e ITIL.