segunda-feira, 19 de novembro de 2012

Log de Auditoria - JPA + Spring

Vira e volta, aparecem sistemas onde é necessário fazer um esquema de log de auditoria para uma tabela ou várias.

Montei um esquema que pode ser facilmente extendido para outras tabelas do sistema, para fazê-lo, basta seguir os passos abaixo:

1- Criar um enum para representar as ações de banco:
public enum TransactionType {
    CREATE, UPDATE, DELETE;
}


2- Criar a entidade de log de auditoria:
@Entity
@Table(name = "log_trace")
@AttributeOverride(name = "id", column = @Column(name = "log_trace_id"))
public class LogTrace extends BaseEntity {

    @Column(name = "transaction_type", nullable = false)
    @Enumerated(EnumType.STRING)
    private TransactionType transactionType;

    @Column(name = "entity_name", nullable = false)
    private String entityName;

    @Column(name = "registry_id", nullable = false)
    private Long registryId;

    @Column(name = "operation_date", nullable = false)
    @Temporal(TemporalType.TIMESTAMP)
    private Date operationDate;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "executed_by", nullable = false)
    private User executedBy;

...getters e setters...
}


3- Criar o repositório (DAO) e o serviço como está sendo feito no restante do sistema;

4- Criar o listener que será disparado pelo hibernate:
public class LogTraceListener {

    @Autowired
    LogTraceService logTraceService;


    @Autowired
    UserService userService;

    @PostRemove
    void postDelete(BaseEntity e) {
        createLog(TransactionType.DELETE, e);
    }

    @PostPersist
    void postPersist(BaseEntity e) {
        createLog(TransactionType.CREATE, e);
    }

    @PostUpdate
    void postUpdate(BaseEntity e) {
        createLog(TransactionType.UPDATE, e);
    }

    private void createLog(TransactionType transactionType, BaseEntity e) {
        /*
         * OBSERVAÇÃO 1.
         */
        if (logTraceService == null) {
            ApplicationContext ctx = ContextLoader.getCurrentWebApplicationContext();

            logTraceService = (LogTraceService) ctx.getBean("logTraceService");
        }

        /*
         * OBSERVAÇÃO 2.
         */
        User user = userService.getAuthenticatedUser();

        LogTrace logTrace = new LogTrace();
        /*
         * OBSERVAÇÃO 3.
         */
        String entityName = e.getClass().getAnnotation(Table.class).name();
        if (entityName == null || entityName.isEmpty()) {
            entityName = e.getClass().getSimpleName();
        }

        logTrace.setTransactionType(transactionType);
        logTrace.setEntityName(entityName);
        logTrace.setRegistryId(e.getId());
        logTrace.setExecutedBy(user);
        logTrace.setOperationDate(new Date());

        logTraceService.save(logTrace);
    }
}


Observações:
1- O listener é disparado pelo mecanismo JPA. Uma vez que a classe não foi instanciada pelo Spring, as dependências não são injetadas e para resolver isso, podemos usar aspéctos (AOP) ou uma abordagem mais simples como a feita acima;
2- A lógica de recuperação do usuário logado (via consulta ou busca na sessão) está no serviço neste caso;
3- Alguns projetos adotam o uso da anotação @Table, neste listener, coloquei uma lógica que abrange esta abordagem (como principal) e se não for o caso, o campo receberá o nome da entidade, por exemplo, "PedidoInterno", ao invés de "pedido_interno" (que é a forma como os bancos normalmente são modelados. Veja que nossa "LogTrace" teria o campo salvo como "log_trace";

5- Anotar entidades que devem ser auditadas:
As entidades que devem ser auditadas recebem a anotação @EntityListeners(value = LogTraceListener.class). Para melhorar a leitura da classe, indico colocar esta anotação logo após a anotação @Entity ou @Table(name = "minha_entidade").

Com isso, sempre que uma das operações monitoradas pelo listener acontecer, são as anotações que estão sobre os métodos: @PostPersist, @PostUpdate e @PostRemove, um registro será gerado na tabela de log de auditoria.

Para que outra tabela/entidade seja autitada, basta repetir o passo 5 (incluir a anotação na entidade).

NOTA: Normalmente as entidades de um sistema extendem uma entidade base ou implementam uma interface para que um padrão "mínimo" seja seguido. Este exemplo foi construído considerando este caso, ou seja, as entidades extendem BaseEntity.

2 comentários:

  1. Marcelo, primeiramente gostaria de parabenizar pelo post. Muito bem explicado.
    Em minha aplicação funcionou tudo certinho, reconheceu os services e tudo mais, porém após executar entityManager.merge em meu serviço que persiste o log ele não comita a transação de fato no banco de dados. Chega até a incrementar o sequence, mas não persiste o log. O que pode estar acontecendo nesse caso?

    ResponderExcluir
    Respostas
    1. Olá Gustavo. de bate pronto me vem um palpite em mente.

      O método que dispara a chamada possui a anotação @Transational?
      Por exemplo:
      Vc tem o serviço PedidoService.save(). Este serviço realiza chamada para o repositório e ai a classe monitorada pelo auditor é persistida. No caso, vc quer gerar o log quando o auditor executa. Já ocorreu comigo casos de eu esquecer de inserir a anotação @Transational sobre o método save do serviço (OBS.: Pode ser colocado no repositório, mas eu costumo colocar no serviço porque de lá posso disparar várias operações de banco e assim, estas ficam todas de baixo da mesma transação) e por isso a persistência não ocorrer.

      Faça um teste e caso não dê certo escreva pra mim, o que eu puder fazer pra ajudar, pode contar.

      Excluir