Il est utile dans certains cas de savoir qui a créé, modifié ou supprimé un enregistrement en base et quand. Cet article a pour objectif de mettre en place un audit sur des entités et de mettre en place un historique de toutes les actions effectué sur les entités choisies en utilisant un entity listener.
Commençons sans plus tarder
Prenons l’entité Article par exemple. Nous souhaitons que JPA persiste les informations lié à la création et à la modification d’un article.
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
@Entity
@EntityListeners(AuditingEntityListener.class)
@Table(name = "article")
public class Article implements Serializable{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ID")
private Long id;
@Version
@Column(name = "VERSION")
private int version;
@Column(name = "TITRE")
private String titre;
@Column(name = "CONTENU")
private String contenu;
@CreatedDate
@Column(name = "CREATED_DATE")
@Temporal(TemporalType.TIMESTAMP)
private Date createdDate;
@CreatedBy
@Column(name = "CREATED_BY")
private String createdBy;
@LastModifiedBy
@Column(name = "LAST_MODIFIED_BY")
private String lastModifiedBy;
@LastModifiedDate
@Column(name = "LAST_MODIFIED_DATE")
@Temporal(TemporalType.TIMESTAMP)
private Date lastModifiedDate;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public int getVersion() {
return version;
}
public void setVersion(int version) {
this.version = version;
}
public String getTitre() {
return titre;
}
public void setTitre(String titre) {
this.titre = titre;
}
public String getContenu() {
return contenu;
}
public void setContenu(String contenu) {
this.contenu = contenu;
}
public Date getCreatedDate() {
return createdDate;
}
public void setCreatedDate(Date createdDate) {
this.createdDate = createdDate;
}
public String getCreatedBy() {
return createdBy;
}
public void setCreatedBy(String createdBy) {
this.createdBy = createdBy;
}
public String getLastModifiedBy() {
return lastModifiedBy;
}
public void setLastModifiedBy(String lastModifiedBy) {
this.lastModifiedBy = lastModifiedBy;
}
public Date getLastModifiedDate() {
return lastModifiedDate;
}
public void setLastModifiedDate(Date lastModifiedDate) {
this.lastModifiedDate = lastModifiedDate;
}
@Override
public String toString() {
return "Article{" +
"id=" + id +
", version=" + version +
", titre='" + titre + '\'' +
", contenu='" + contenu + '\'' +
", createdDate=" + createdDate +
", createdBy='" + createdBy + '\'' +
", lastModifiedBy='" + lastModifiedBy + '\'' +
", lastModifiedDate=" + lastModifiedDate +
'}';
}
}
Nous avons créé une entité JPA avec certains champs habituelle comme l’id , titre, et contenu, et d’autre moins habituelle
« @Version » : indique le nombre de fois ou l’enregistremenr a ete modifié
« @CreatedDate, @CreatedBy, @LastModifiedBy, @LastModifiedDate » : utiliser pour l’audit de l’entité.
L’annotation « @EntityListeners(AuditingEntityListener.class) » : signifie qu’un audit de l’entité doit etre effectué en utilisant le listener par defaut « L’AuditingEntityListener qui fourni deux annotation PrePersist pour auditer la création et PreUpdate pour auditer la mise à jours :

Notre entité Article contient plus 100 lignes, ce qui n’est pas très pratique surtout que les champs d’audit devront être réécrits à chaque fois que l’on souhaite auditer une nouvelle entité. Utilisons une class abstraite contenant les champs d’audit et qui sera hérité par toute les entités que nous voulons auditer. Nous allons aussi utiliser le projet lombok pour écrire moins de code. (voir l’article traitant de lombok)
ce qui donne:
import lombok.Getter;
import lombok.Setter;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.*;
import java.util.Date;
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@Getter@Setter
public abstract class Auditable {
@CreatedDate
@Temporal(TemporalType.TIMESTAMP)
private Date createdAt;
@CreatedBy
private U createdBy;
@LastModifiedDate
@Temporal(TemporalType.TIMESTAMP)
private Date lastUpdateAt;
@LastModifiedBy
private U lastUpdateBy;
}
Et reduit le taille de notre class :
import akl.auditing.auditing.ArticleEntityListener;
import akl.auditing.auditing.Auditable;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import javax.persistence.*;
@Entity
@EntityListeners(ArticleEntityListener.class)
@Data@NoArgsConstructor@ToString
public class Article extends Auditable {
@Id
@GeneratedValue
private Long id;
@Version
@Column(name = "VERSION")
private int version;
private String titre;
private String contenu;
public Article(String titre, String contenu) {
this.titre = titre;
this.contenu = contenu;
}
}
Remarque: nous avons modifier le listener dans l’annotation « EntityListener » car nous voulons auditer l’ajout la modification mais aussi la suppression d’un enregistrement il nous faut donc creer notre propre auditingEntityListener qui prendra en charge la supression et qu’on appellera « ArticleEntityListener »:
import akl.auditing.model.Article;
import javax.persistence.EntityManager;
import javax.persistence.PrePersist;
import javax.persistence.PreRemove;
import javax.persistence.PreUpdate;
import javax.transaction.Transactional;
import static akl.auditing.auditing.Action.DELETED;
import static akl.auditing.auditing.Action.INSERTED;
import static akl.auditing.auditing.Action.UPDATED;
public class ArticleEntityListener {
@PrePersist
public void prePersist(Article article){
perform(article,INSERTED);
}
@PreUpdate
public void preUpdate(Article article){
perform(article,UPDATED);
}
@PreRemove
public void preRemove(Article article){
perform(article,DELETED);
}
//Persister dans la table article_history
@Transactional(Transactional.TxType.MANDATORY)
private void perform(Article article, Action action){
EntityManager entityManager = BeanUtil.getBean(EntityManager.class);
entityManager.persist(new ArticleHistory(article.getTitre(),action));
}
}
Ajout de l’interface « JpaRepository » pour les operation CRUD:
import akl.auditing.model.Article;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface ArticleRepository extends JpaRepository<Article,Long>{
}
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface ArticleHistoryRepository extends JpaRepository<ArticleHistory,Long> {
}
Pour les champs createdAt et lastUpdateAt Jpa peut les compléter en utilisant l’heure courante, ce qui n’est pas le cas pour les champs createdBy et lastUpdateBy qui représente l’utilisateur qui a modifié l’entité.
pour cela nous pouvons utiliser Spring Security pour récupérer l’utilisateur en cours. il nous faut une implémentation de la class « AutidorAware ».
import org.springframework.data.domain.AuditorAware;
public class AuditorAwareImpl implements AuditorAware {
@Override
public String getCurrentAuditor() {
//en l'abscence d'une configuration Spring Security dans ce projet nous faisons un simple return.
return "Amin";
}
}
« @EnableJpaAuditing »:
pour que spring data jpa prennent en compte l’audit des entités marquées par les annotations « @EntityListener(…) » il faut une class de configuration à laquelle nous ajouterons l’annotation enableJpaAuditing contenant la référence de l’implémentation de l’auditor aware via un bean.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.domain.AuditorAware;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
@Configuration
@EnableJpaAuditing(auditorAwareRef = "auditorAware")
public class JpaConfig {
@Bean
public AuditorAware auditorAware(){
return new AuditorAwareImpl();
}
}
Comment mettre en place un historique des changement sur l’entité:
La premiere chose à faire est de creer l’entité « ArticleHistory » contenant les informations de modifications et qui utilise un listener par default « AuditingEntityListener » car nous ne voulons auditer que les ajout et modification de la table article_history:
import lombok.Data;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.*;
import java.util.Date;
@Entity
@EntityListeners(AuditingEntityListener.class)
@Data
public class ArticleHistory {
@Id
@GeneratedValue
private Long id;
private String articleName;
private String articleContent;
@CreatedBy
private String modifiedBy;
@CreatedDate
@Temporal(TemporalType.TIMESTAMP)
private Date modifiedDate;
@Enumerated(EnumType.STRING)
private Action action;
public ArticleHistory() {
}
public ArticleHistory(String article, Action action) {
this.articleName = article;
this.action = action;
this.articleContent = article.toString();
}
}
Les actions possible sont :
public enum Action {
INSERTED("INSERTED"),
UPDATED("UPDATED"),
DELETED("DELETED");
private String name;
private Action(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
@Override
public String toString() {
return name;
}
}
Nous pouvons à présent ajouter et supprimer des article et vérifier que tous est OK:
import akl.auditing.auditing.ArticleRepository;
import akl.auditing.model.Article;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import javax.annotation.Resource;
@EnableJpaAuditing
@EnableJpaRepositories
@SpringBootApplication
public class AuditingApplication implements CommandLineRunner{
@Resource ArticleRepository articleRepository;
public static void main(String[] args) {
SpringApplication.run(AuditingApplication.class, args);
}
@Override
public void run(String... strings) throws Exception {
Article article = new Article("Article 1", "Contenu de l'article");
articleRepository.save(article);
article.setTitre("Mise a jour du titre");
article.setContenu("Mise a jour du contenu");
articleRepository.saveAndFlush(article);
articleRepository.delete(1l);
}
}
ce qui nous donne en base les données suivantes:
Ajout d’un nouvel article


apres supression de l’article

Vous Pouvez récupérer l’ensemble du code sur mon repository Github.