PRAKTYCZNY ENVERS

post_img

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

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *