2010. szeptember 10., péntek

Entitások auditálása Hibernate Envers-szel

A Hibernate Envers egy nagyon egyszerű Hibernate modul arra, hogy az entitásokat auditáljuk, azaz módosításkor a régi értékek is megmaradjanak az adatbázisban, és azokat bármikor előkereshessük.

Gyakori megrendelői igény, hogy látni lehessen, hogy ki, mikor és mit módosított bizonyos entitásokon, rekordokon. Az alkalmazásfejlesztő szeretné ezt minél transzparens módon kezelni, szóval lehetőleg ne kelljen ehhez a kódot módosítani. Egyszerű megoldás, hogy az aktuális és az audit rekordok is ugyanabban a táblában maradnak, és egy flag-et állítunk. Ezt meg lehet oldani alacsonyabb szinten is, transzparensebb módon, pl. adatbázis triggerek alkalmazásával. Azaz a táblára kell tenni egy pre-insert, pre-delete és pre-update trigger-t, mely az adott rekordokat átmásolja egy másik, szerkezetileg hasonló táblába. Persze ehhez a triggert nekünk kell megírnunk. Az Oracle az audit rekordokat tartalmazó táblát Journal Table-nek nevezi, és bizonyos eszközök, pl. az Oracle Designer/2000, képesek ezeket, és a hozzá tartozó triggereket automatikusan legenerálni. Ha nem adatbázis alapú megoldást szeretnénk alkalmazni, használhatjuk pl. JPA esetén annak életciklus metódusait. Ennél azonban magasabb szintű, és szabványosabb megoldást biztosít a Hibernate Envers.

Az Envers gyakorlatilag beépül a Hibernate-be, és akár natív módon, akár JPA-n keresztül használjuk, kihasználhatjuk az előnyeit. Működik különálló alkalmazásban, de alkalmazásszerveren belül is, ahol a Hibernate végzi a perzisztenciát. A Subversion-höz hasonlóan az Envers is bevezeti a revision fogalmát. Gyakorlatilag minden tranzakció, mely auditálandó entitást szúr be, módosít vagy töröl, kap egy revision számot, mely a rendszeren belül egyedi. Minden revision-höz elmenti annak dátumát is. Minden entitáshoz létrehoz egy audit táblát is, melybe módosításkor vagy törléskor elmenti az előző állapotot, és természetesen hozzácsapja ezt a revision számot is. Utána a standard lekérdezésekkel elérjük a normál entitásainkat, de lehetőségünk van akár revision szám, akár dátum alapján visszakeresnünk az entitásaink régebbi állapotait.

Használata rendkívül egyszerű, egy Quickstart mutatja be a lehetőségeit. Én is készítettem egy egyszerű Maven-es projektet (letölthető), mely a tipikus Employee, Phone entitásokból áll, valamint az ezen CRUD műveleteket végző EmployeeService osztályból, mely JPA-t használ. Az EmployeeServiceTest teszt eset mutatja az Envers képességeit. A teszt esetek az mvn test parancs kiadásával futtathatóak. Adatbázis telepítése nem szükséges, memóriában futó HSQLDB-t használ.

Az Envers használatához szükséges, hogy a classpath-ban legyen, ehhez a Maven-ben a következő függőséget kell felvennünk:

<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-envers</artifactId>
<version>3.5.5-Final</version>
</dependency>

Következő lépésként a Hibernate-et kell konfigurálunk, méghozzá a persistence.xml-be kell beírnunk a megfelelő gyártófüggő paramétereket. Látható, hogy az Envers listener-eket használ a működéséhez.

<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" />

Ahhoz, hogy egy entitást az Envers auditáljon, el kell helyezni rajta az @Audited annotációt. Amennyiben nem akarjuk az összes mezőjét auditálni, elhelyezhetjük az annotációt a mezőkön is. A példában az Employee és a Phone entitáson is elhelyeztük az annotációt.

@Entity
@Audited
public class Employee implements Serializable {
...
}

Az Envers használatához semmi több nem szükséges, a standard JPA műveleteket használva automatikusan megtörténik az auditálás. Ez annyit jelent, hogy sémageneráláskor az Employee és Phone tábla mellé létrehoz egy Employee_AUD és egy Phone_AUD táblát is, mely megegyezik az eredeti táblákkal, azzal a különbséggel, hogy kiegészíti egy REV és egy REVTYPE mezővel, valamint létrehoz egy REVINFO táblát, REV és REVTSTMP mezővel. Minden egyes beszúráskor, módosításkor, vagy törléskor, mely auditálandó entitást érint, létrehoz egy új revision-t, azaz beszúr egy új rekordot a REVINFO táblába. Ad neki egy egyedi azonosítót, mely egy automatikusan növekvő szám (REV mező), és a REVTSTMP mezőben letárolja az aktuális dátumot, időt. Az entitás előző értékét az _AUD végű táblába szúrja be, melynek REV mezője tartalmazza a revision egyedi azonosítóját, valamint azt, hogy milyen művelet történt (0: ADD - beszúrás, 1: MOD - módosítás, 2: DEL - törlés).

Természetesen lehetőség van az auditált entitások lekérdezésére is. Erre a teszteset testForRevisionsOfEntity és testForEntitiesAtRevision metódusai mutatnak példákat. A legegyszerűbb lekérdezni egy revision-höz tartozó entitást:

AuditReader auditReader = AuditReaderFactory.get(em);
Employee revision = (Employee) auditReader.createQuery()
.forEntitiesAtRevision(Employee.class, 1).getSingleResult();

Látható, hogy az audit entitások kezelésére az AuditReader való. Ennek is vannak hasznos metódusai, mint a findRevision, getCurrentRevision, getRevisionDate, getRevisionNumberForDate, getRevisions, stb. De ezeknél sokkal rugalmasabb a Criteria API-hoz hasonlatos lekérdezési lehetőség a createQuery metódus használatával. Itt fluent interfész használatával további feltételeket tudunk megadni. Pl. nézzük meg az összes revision lekérdezését az Employee osztályhoz:

List revisions = auditReader.createQuery()
.forRevisionsOfEntity(Employee.class, false, true).getResultList();

Ez egy List példánnyal fog visszatérni. A lista elemei tartalmazzák a revision-öket. Egy elem három objektumot tartalmaz. Az első az audit entitás, a második egy DefaultRevisionEntity példány, mely tartalmazza a revision azonosítóját és dátumát, a harmadik a RevisionType enum egy értéke (ADD, MOD, DEL). Persze az AuditQueryCreator metódusaival ezt a lekérdezést tovább finomíthatjuk, hogy csak a számunkra fontos értékeket adja vissza.

Az Envers-t természetesen tovább tudjuk konfigurálni, pl. globális paraméterek használatával, vagy további annotációkkal. Pl. megadhatjuk a táblák prefix-ét, suffix-ét, mezők neveit, sémát, katalógust. Az @AuditTable, @SecondaryAuditTable(s) annotációkkal entitásonként adhatjuk meg az audit tábla nevét. @AuditOverride(s) annotációval a mezők neveit tudjuk felülírni. Amennyiben egy kapcsolatban a cél entitást nem akarjuk auditálni, használjuk a @Audited(targetAuditMode = RelationTargetAuditMode.NOT_AUDITED) annotációt. Ekkor a betöltött audit entitás mindig az aktuális cél entitásra fog mutatni.

Megtehetjük azt is, hogy minden revision-höz saját adatokat mentünk el. Pl. a módosítást végző felhasználó nevét. Ekkor vagy a DefaultRevisionEntity osztályt kell kiterjeszteni, vagy a @RevisionNumber és @RevisionTimestamp annotációkat használni, és felvenni a megfelelő attribútumokat. Mindkét esetben az osztályt el kell látni a @RevisionEntity annotációval, és meg kell adni egy RevisionListener interfészt megvalósító osztályt, mely newRevision metódusát hívja meg az Envers. Ebben lehet beállítani az előbb említett példa esetén a bejelentkezett felhasználó nevét.

Próbálkozásaim közben kiderült, hogy az @Audited annotációt hiába használom @MappedSuperClass-on, az annotáció nem öröklődik, tehát rá kell tennem az entitásokra is. Valamint akár a @PrePersist, akár @PostPersist annotációval jelölt életciklus metódusokat használom, az Envers az audit rekordba már a módosított értéket írja be, és nem az előtti állapotot.

Itt érdemes megemlékezni a Commons DbUtils projektről is. A teszt esetben ugyanis az audit táblák tartalmát JDBC-n keresztül akartam ellenőrizni. A JDBC túl nehézkes, Connection, Statement, ResultSet építésével és a kivételkezelésével. Nem akartam emiatt bevetni a Spring-et (ágyúval verébre), hogy a JdbcTemplate-et használhassam, így Commons DbUtils-ra esett a választásom, mellyel egyszerűen lehet adatbázis műveleteket futtatni. Nézzünk is néhány példát, melyek magukért beszélnek:

QueryRunner runner = new QueryRunner();
runner.update(conn, "delete from Employee");

Map result = runner.query(conn,
"select count(*) as cnt from revinfo", new MapHandler());
assertEquals(1, result.get("cnt"));

List<Map><String, Object[]> results = runner.query(conn,
"select *  from Employee_AUD order by rev", new MapListHandler());
assertEquals(1, results.size());
assertEquals("name1", results.get(0).get("name"));

3 megjegyzés:

  1. Nagyon jó kis összefoglaló poszt az Enversszel való induláshoz!

    Olvasás közben bennem két dolog merült fel:

    Mi történik, ha már élesben fut az alkalmazás, a *_AUD táblák tele vannak verzióállapotokkal, de egy új követelmény miatt át kell alakítani a sémát? Mondjuk a Phone tábla egy külön körzetszámot leíró oszloppal bővülne és még plussz egy egy készüléktípusokat leíró táblára hivatkozó külső kulccsal? Vajon van-e erre valami eszköze az Enversnek (pl. automatikus séma update)?

    Miért nincs a mai napig a modern adatbáziskezelő rendszerekben ilyen funkcionalitás? Mert a standard SQL nem képes ezt kifejezni? Vagy van már ilyen csak én vagyok lemaradva? :)

    VálaszTörlés
  2. Baromi jó leírás.
    Mi persze megcsináltuk ezt magunk (természetesen abszolút projektspecifikusan, nem kiszervezhető formában), mert deklarált idő az nem volt a körbenézésre, csak a billentyű klampírozásra. Na de majd legközelebb meglessük ezt.
    Steve kérdése is korrekt.

    VálaszTörlés
  3. Sziasztok, köszi a hozzászólásokat!

    Kipróbáltam, ha felvettem egy új mezőt, akkor az Envers által generált táblákba is felkerült az új mező, és a régi adatok érintetlenek maradtak. Feltehetőleg a Hibernate sémagenerálóját, mely update-el is, használja.

    Amúgy a JPA könyvben olvastam, hogy ne használjuk a séma generálást, az csak prototípus építésre való. Igenis használjuk az adatbázis eszközeit, építsük fel mi a sémát, hiszen olyan dolgokat is meg kell adnunk, melyet a JPA nem tud, nem scope-ja. Pl. jogosultsági szintek, indexek, constraint-ek, view-k, triggerek, és sok egyéb dolog.

    Az hogy miért nem támogatják, szerintem üzleti modell. Sok-sok kereskedelmi termék van, ami erre jó, de van ingyenesek is, pl. DdlUtils, Liquibase, DbMaintain, Druid, Carbon Five Database Migration framework. Oracle SQL Developer-je is illeszthető CVS-hez, Subversion-höz. Amúgy van külön megvásárolható termékük is: Oracle Change Management Pack, mely szépen illeszkedik az admin felületbe is. Mondjuk kritikaként fogalmazták meg, hogy ez sem beépül a sémába, adatbázis motorba, hanem attól különálló. Ha az adatbázis-kezelők megjelennének (az árban foglalt) natív megoldásokkal, sokak üzlete megcsappanna.

    De Steve, erről neked kéne írnod!

    VálaszTörlés