Envers jest dodatkiem do Hibernate’a pozwalającym na automatyczne zapisywanie historii zmian encji (audytu). Envers używa pojęcia rewizji – co oznacza, że każda zmiana dowolnej (audytowalnej) encji tworzy nową rewizję. Nowy rekord jest zapisany do tabeli, a na podstawie starego zostaje utworzona rewizja i zapisana do dodatkowej tabeli wraz z poprzednimi wartościami.
Konfiguracja
W ramach podstawowej niezbędnej konfiguracji należy dodać następujące właściwości do pliku persistence.xml lub hibernate.cfg.xml:
<property name=”hibernate.ejb.event.post-insert” value=”org.hibernate.ejb.event.EJB3PostInsertEventListener,org.hibernate.envers.event.AuditEventListener” />
<property name=”hibernate.ejb.event.post-update” value=”org.hibernate.ejb.event.EJB3PostUpdateEventListener,org.hibernate.envers.event.AuditEventListener” />
<property name=”hibernate.ejb.event.post-delete” value=”org.hibernate.ejb.event.EJB3PostDeleteEventListener,org.hibernate.envers.event.AuditEventListener” />
<property name=”hibernate.ejb.event.pre-collection-update” value=”org.hibernate.envers.event.AuditEventListener” />
<property name=”hibernate.ejb.event.pre-collection-remove” value=”org.hibernate.envers.event.AuditEventListener” />
<property name=”hibernate.ejb.event.post-collection-recreate” value=”org.hibernate.envers.event.AuditEventListener” />
Konfigurujemy w ten sposób listenery , które sprawdzają, czy zmieniły się jakieś audytowalne encje i czy należy zachować historię zmian tych encji.
Kolejnym krokiem jest już oznaczenie encji adnotacją @Audited. Jeśli adnotację umieścimy na poziomie klasy, wszystkie pola będą wersjonowane. Możemy również adnotację umieścić na poziomie pól klasy, aby wersjonować tylko wybrane.
Przykład użycia
Po umieszczeniu konfiguracji opisanej w kroku wyżej użycie Envers’a sprowadza się do dodania odpowiednich adnotacji.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | @Entity @Audited public class Item { @Id private int id; private String name; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } } |
I to tak naprawdę wszystko z rzeczy specyficznych dla Envers. Użycie encji w innym zakresie jest tak standardowe, jak w przypadku JPA lub Hibernate i w zasadzie niczym się od nich nie różni.
Przechowywanie rewizji
Dla każdej adnotowanej encji zostanie utworzona dodatkowa tabela przechowująca audyt. Dla naszej encji z podanego przykładu powstanie podstawowa tabela:
1 2 3 4 5 | CREATE TABLE ITEM ( ID decimal(10) PRIMARY KEY NOT NULL, NAME varchar2(1020) ); |
Oraz dodatkowa tabela do przechowywania audytu:
1 2 3 4 5 6 7 8 | CREATE TABLE ITEM_AUD ( ID decimal(10) NOT NULL, REV decimal(10) NOT NULL, REVTYPE decimal(3), NAME varchar2(1020), CONSTRAINT SYS_C006187 PRIMARY KEY (ID,REV) ); |
Dodatkowo doszły nam następujące pola:
- REV – oznacza numer rewizji;
- REVTYPE – typ zmiany; możliwe wartości: 0 – wstawienie rekordu, 1 – modyfikacja rekordu, 2 – usunięcie rekordu.
Najnowszy rekord w tabeli audytu będzie miał wartości o jedną wersję wstecz względem wartości w aktualnej tabeli. Jeśli natomiast rekord zostanie usunięty, nie znajdziemy go już w tabeli podstawowej, a najświeższy wpis będzie miał wartości ustawione na <null> i typ rewizji 2.
Numery rewizji są wspólne dla wszystkich encji. Oznacza to, że jeśli najpierw zmienimy encję A, otrzyma ona rewizję 1, dopiero potem zmienimy encję B, która otrzyma rewizję 2 (oczywiście obie będą posiadały oddzielne tabele audytu).
Encja rewizji i zapis dodatkowych danych
Oprócz zapisywania historii zmian danej encji możemy dla każdej rewizji zapisać dodatkowe informacje. Służy do tego encja rewizji – jest to standardowa encja z dodatkową adnotacją oraz dodatkowymi polami, jakie chcemy zapisać. Przykładowo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | @Entity @RevisionEntity(MyRevisionListener.class) public class MyRevisionEntity extends DefaultRevisionEntity { private static final long serialVersionUID = -78999006241889798L; private String username; public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } } |
Klasa DefaultRevisionEntity jest dostarczana wraz z Envers i deklaruje dwa podstawowe pola: id oraz timestamp. Do tego możemy dołożyć dodatkowe informacje, które chcemy zapisać, np. nazwę użytkownika. Wykonuje się to w listenerze wskazanym przez adnotację @RevisionEntity:
1 2 3 4 5 6 7 8 | public class MyRevisionListener implements RevisionListener { public void newRevision(Object revisionEntity) { MyRevisionEntity entity = (MyRevisionEntity) revisionEntity; entity.setUsername("NAZWA UŻYTKOWNIKA"); } } |
Dla przykładowej encji rewizji zostanie utworzona następująca tabela:
1 2 3 4 5 6 | CREATE TABLE MYREVISIONENTITY ( ID decimal(10) PRIMARY KEY NOT NULL, TIMESTAMP decimal(19) NOT NULL, USERNAME varchar2(1020) ); |
Teraz przy utworzeniu nowej rewizji dowolnej audytowanej encji zostanie utworzony nowy wpis w tej tabeli z danymi uzupełnionymi według naszego listenera.
Odpytywanie o rewizje
Envers dostarcza klasy użytkowe pozwalające na odpytywanie o stan rewizji poszczególnych encji. Klasa AuditReaderFactory pozwala nam na pobranie instancji AuditReader w zależności od tego, czy operujemy na EntityManager z JPA czy też na Session z Hibernate.
AuditReader pozwala na budowanie zapytań implementujących interfejs AuditReader. Służy on do budowania zapytań w sposób podobny jak w przypadku Hibernate Criteria. Pytać możemy na dwa sposoby:
- o encję w konkretnej rewizji,
- o rewizje, w których encje się zmieniły.
Przykład zapytania o konkretną rewizję:
1 2 3 4 5 6 7 | int revision = 1; AuditQuery query = getAuditReader().createQuery().forEntitiesAtRevision(Item.class, revision); List items = query.addOrder(AuditEntity.property("name").desc()) .setFirstResult(4) .setMaxResults(2) .getResultList(); |
Przykład zapytania o rewizje, w których zmieniała się encja:
1 2 3 4 5 6 | int revision = 1; Number revision = (Number) getAuditReader().createQuery() .forRevisionsOfEntity(Item.class, false, true) .setProjection(AuditEntity.revisionNumber().min()) .add(AuditEntity.id().eq(entityId)) .getSingleResult(); |
Wnioski
Jak widać, Envers pozwala na łatwe wdrożenie historii zmian bez specjalnego nakładu pracy i praktycznie transparentnie dla istniejącego modelu. Warto też wspomnieć, że Envers stał się już częścią dystrybucji Hibernate’a (od wersji 3.5), co dodatkowo świadczy o jego użyteczności i jakości.
Źródła
Strona główna projektu: http://jboss.org/envers
Dokumentacja: http://docs.jboss.org/envers/docs/index.html
Autor: Mateusz Mrozewski
