JPA i audyt encji
Dość częstym zadaniem jakie musimy wykonać zapisując dane naszej aplikacji jest również dołączenie informacji o tym kto i kiedy stworzył, a później aktualizował dany rekord. Zazwyczaj informacje takie trafiają do czterech kolumn: data stworzenia, data aktualizacji, użytkownik, który stworzył oraz zaktualizował rekord. Są to tzw. kolumny audytowe. W momencie, gdy wykorzystujemy do zapisu naszych danych implementację JPA, mamy sprawę bardzo ułatwioną.
Stwórzmy interfejs Auditable, który powinna implementować każda „audytowalna” klasa encji.
public interface Auditable { Date getCreationDate(); void setCreationDate(Date date); Date getModificationDate(); void setModificationDate(Date date); T getCreationUser(); void setCreationUser(T user); T getModificationUser(); void setModificationUser(T user); T getCurrentUser(); }
Interfejs Auditable jest parametryzowany, tak aby można było określić jakiego typu obiekty reprezentują użytkownika w naszej aplikacji. Wspólna implementacja tego interfejsu została umieszczona w klasie abstrakcyjnej AuditEntity. To tutaj zdefiniowana jest cała logika związana z aktualizowaniem danych audytowych przed zapisem.
@MappedSuperclass public abstract class AuditEntity implements Auditable<User> { protected Date creationDate; protected Date modificationDate; @ManyToOne @JoinColumn(name = "creation_user_id") protected User creationUser; @ManyToOne @JoinColumn(name = "modification_user_id") protected User modificationUser; //tutaj gettery i settery dla creationDate, //modificationDate, creationUser, modificationUser @Override public User getCurrentUser() { return Application.getCurrentUser(); } @PrePersist public void prePersist() { setCreationDate(new Date()); setCreationUser(getCurrentUser()); preUpdate(); } @PreUpdate public void preUpdate() { setModificationDate(new Date()); setModificationUser(getCurrentUser()); } }
Klasa bazowa została oznaczona adnotacją @MappedSuperclass. Dzięki temu dziedziczone atrybuty są trwałe wg. mapowania zdefiniowanego w tej klasie (istnieje możliwość nadpisania tych mapowań poprzez użycie adnotacji @AttributeOverride(s)). Najważniejszą częścią tej klasy są metody prePersist() oraz preUpdate(). Dzięki oznaczeniu metody prePersist() odpowiedną adnotacją @PrePersist wykonuje się ona przed utrwaleniem danej encji. Podobnie wykonywana jest metoda preUpdate() (oznaczona adnotacją @PreUpdate) przed każdą aktualizacją do bazy danych.
Teraz wystarczy tylko dodać dziedziczenie z AuditEntity dla każdej encji, dla której chcemy zapisywać dane audytowe np.:
@Entity @Table(name = "customers") public class Customer extends AuditEntity
Jak widać dość szybko i prosto można osiągnąć funkcjonalność „audytowalnych” encji.
A już myślałem, że zapadłeś w zimowy sen i kolejnego wpisu można było spodziewać się dopiero na wiosnę 🙂
Audyt encji to przydatna sprawa, w większości projektów wymagana przez klienta. Ciekaw jestem jednak jakiegoś sposobu w czystym JPA, który pozwoli na wydelegowanie zbierania audytowych informacji do osobnego „listenera”. W czystym Hibernate można użyć do tego interceptora, dzięki czemu można pozbyć się zaszywania logiki audytu w klasach encji (czyli bez dziedziczenia po AuditEntity). Wtedy jednak nie mamy dostępu do tych informacji bezpośrednio z encji, jednak nie zawsze są one w aplikacji potrzebne. Pewnie JPA też ma coś takiego.
Do zapisu daty modyfikacji warto też posłużyć się adnotacją @Version na danym atrybucie klasy. Lepsze to niż @PreUpdate, bo pozwala na lock’i takiej encji.