Motivação
Em aplicações dos mais diversos tamanhos, a necessidade de rastrear as ações realizadas por um usuário frente às várias funcionalidades existentes é algo comum. Nesses cenários, resguardar a integridade das informações administradas pela aplicação, bem como prover o controle sobre seu uso é algo de extrema valia.
Técnicas clássicas, como a utilização de triggers e stored procedures, acabam criando um grande acoplamento com o banco de dados. Para evitar essa dependência, o ideal é que as ações de auditoria sejam realizadas pela aplicação. Como atualmente grande parte dos projetos Java utilizam frameworks ORM para a camada de persistência, sendo o principal deles o Hibernate, surgiu o subprojeto Envers, um módulo do Hibernate que é capaz de gerir todas as modificações realizadas nas entidades e refleti-las no banco de dados de forma simples.
Nesse artigo veremos como utilizar o Hibernate Envers na construção de um cadastro de funcionários, verificando como ele realiza a auditoria das ações.
Desenvolvendo o cadastro
Nesse exemplo utilizaremos o banco de dados MySQL 5.7.17, assim como o IDE NetBeans 8.2 para criar uma aplicação desktop que conterá apenas uma entidade, Funcionario, com os seguintes atributos: código, nome, CPF, cargo, endereço e salário.
O script para criação do banco de dados, que aqui se chamará empresabd, pode ser visto na Listagem 1.
CREATE database empresabd;
USE empresabd;
CREATE TABLE funcionario (
codigo INT NOT NULL AUTO_INCREMENT,
nome VARCHAR(100),
cargo VARCHAR(30),
salario DOUBLE,
cpf VARCHAR(20),
endereco VARCHAR (100),
PRIMARY KEY (codigo));
Com o banco pronto, criaremos um novo projeto Java no IDE chamado hibernate-envers. Feito isso, é importante adicionar as bibliotecas referentes ao Envers no classpath do projeto. Essas bibliotecas podem ser encontradas junto com o Hibernate, cuja versão utilizada foi a 5.2.6.Final.
Criando, mapeando e auditando
Agora, por meio de anotações no código, vamos sinalizar ao Envers que nossa entidade será alvo de auditoria, e também criar e definir o mapeamento objeto-relacional da classe Funcionario. Para isso, crie uma nova classe no projeto e defina seu código conforme a Listagem 2.
//imports omitidos
@Entity
@Audited
@Table
public class Funcionario implements Serializable {
@Id
@Column
@GeneratedValue(strategy = GenerationType.AUTO)
private int codigo;
@Column
private String nome;
@Column
private String cargo;
@Column
private double salario;
@Column
private String cpf;
@Column
private String endereco;
//construtores e métodos gets e sets omitidos
}
A anotação que indica que a entidade criada passará por auditoria é a @Audited (linha 4), pertencente ao pacote org.hibernate.envers. Sua presença possibilita que outra tabela seja criada no banco, com a função de armazenar todas as alterações sofridas pela entidade auditada. As demais anotações referem-se às configurações do Hibernate Core.
O nome da tabela criada automaticamente pode ser customizado através da anotação @AuditTable. Quando não se utiliza essa anotação, o Envers compreende que a nomenclatura da tabela de histórico deve ter o nome da classe com o sufixo _AUD. Nesse caso, como não adicionamos essa anotação na Listagem 2, a tabela criada se chamará funcionario_AUD.
Configurando a aplicação
Aqui, a única configuração necessária é informar ao Hibernate onde está o banco de dados e como se conectar a ele, configuração essa que deve ser feita no arquivo hibernate.cfg.xml.
Em versões anteriores do Envers era necessário especificar alguns listeners no arquivo de configuração do Hibernate. Esses listeners permitem ao Envers controlar as ações de auditoria, incluindo registros nas tabelas de histórico de acordo com a ação realizada pelo usuário. A partir da versão 5, no entanto, essa especificação não é mais necessária, sendo realizada de forma automática.
O passo seguinte, então, é criar a classe utilitária HibernateUtil, que fará a ligação entre o arquivo de configuração e o banco de dados, disponibilizando uma instância de SessionFactory para a aplicação. A Listagem 3 mostra a implementação dessa classe.
//imports omitidos
public class HibernateUtil {
private static SessionFactory sessionFactory;
public static SessionFactory getSessionFactory() {
if (sessionFactory == null) {
Configuration configuration = new Configuration().configure();
sessionFactory = configuration.buildSessionFactory();
}
return sessionFactory;
}
}
Com a classe de persistência mapeada com as anotações do Hibernate, e o arquivo de configuração e a conexão com o banco de dados implementados, é necessário agora criar a classe FuncionarioDAO, que será responsável por disponibilizar as operações de persistência. Seu código pode ser visto através da Listagem 4.
//imports omitidos...
public class FuncionarioDAO {
private static SessionFactory factory;
public FuncionarioDAO() {
factory = HibernateUtil.getSessionFactory();
}
public Integer addFuncionario(Funcionario funcionario) {
Session session = factory.openSession();
Transaction tx = null;
Integer cod_func = null;
try {
tx = session.beginTransaction();
cod_func = (Integer) session.save(funcionario);
tx.commit();
} catch (HibernateException e) {
if (tx != null) {
tx.rollback();
}
e.printStackTrace();
} finally {
session.close();
}
return cod_func;
}
}
Testando a aplicação
Após todos os passos realizados, podemos ver o Envers trabalhando. A execução da classe TesteEnvers, apresentada na Listagem 5, permite visualizar as transformações ocorridas no banco de dados. Isso porque, a partir da linha 5, são criadas quatro instâncias de objetos da classe Funcionario, cujos dados serão persistidos no banco de dados através de chamadas ao método addFuncionario(), implementado na classe FuncionarioDAO.
//imports omitidos...
public class TesteEnvers {
public static void main(String[] args) throws ParseException {
Funcionario func1 = new Funcionario("Clenio Rocha", "Analista de TI", 5600, "06493084765", "Rua Rio
Paranaiba");
Funcionario func2 = new Funcionario("Carlos Martins", "Analista de TI", 4300, "06433055765", "Rua Abadia dos
Dourados");
Funcionario func3 = new Funcionario("Daniel Cioqueta", "Analista de TI", 4200, "06493084444", "Rua Rio da
Ostras");
Funcionario func4 = new Funcionario("Yujo Rodrigues", "Gerente de Projetos", 4110, "07777775765", "Rua
Araxa");
FuncionarioDAO funcDao = new FuncionarioDAO();
funcDao.addFuncionario(func1);
funcDao.addFuncionario(func2);
funcDao.addFuncionario(func3);
funcDao.addFuncionario(func4);
}
}
A Figura 1 mostra a visão do banco de dados após a execução da classe de teste. Além da tabela funcionario, foram criadas automaticamente outras duas tabelas: revinfo e funcionario_aud. Essa última armazenará o histórico de mudanças sofridas pela entidade funcionário, sendo composta por todos os campos auditados da tabela original, além de outros dois atributos: o código da revisão (REV), que é a chave primária da tabela; e o tipo de revisão (REVTYPE), que definirá a ação executada, como inclusão, alteração ou exclusão de dados. Já a tabela revinfo inclui os seguintes campos: REV, que faz a ligação com as tabelas de auditoria individuais (as de final _aud); e REVSTAMP, que indica o momento exato (timestamp) em que a operação ocorreu.
Logo abaixo, a Figura 2 mostra os registros adicionados à tabela funcionario_aud. Além de todos os campos auditados, podemos verificar as colunas REV e REVTYPE. A primeira armazena o identificador da revisão, e é chave estrangeira da tabela revinfo. Já a segunda coluna indica o tipo de ação executada. Nesse caso, o valor 0 (zero) indica que foi adicionado um novo registro. A Figura 2 mostra, ainda, a partir de uma consulta, como ficou a tabela revinfo, que armazena os identificadores das revisões, bem como o momento em que a operação ocorreu.
Uma das principais características oferecidas pelo Envers, além de sua simplicidade, é o ganho em produtividade e a redução de tempo empregado na manutenção. Enquanto com outras opções perde-se muito tempo e utiliza-se mais linhas de código, com esse módulo do Hibernate temos apenas que inserir algumas configurações e poucas anotações no código.