Entitások auditálása Hibernate Envers-szel
Frissítve: 2014. január 4.
Felhasznált technológiák: Hibernate 4.3.0, HSQLDB 2.3.1, SLF4J 1.7.5, JUnit 4.11, Apache Commons DbUtils 1.5, Maven 3.0.3
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 transzparensebb 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, pl. adatbázis triggerek alkalmazásával. Azaz a táblára kell tenni egy pre-insert, pre-delete és pre-update triggert, 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ű, jól
dokumentált
mutatja be a lehetőségeit. Én is készítettem egy egyszerű Maven-es
projektet, mely letölthető a
GitHubról. Ez 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 Mavenben a következő függőséget kell felvennünk:
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-envers</artifactId>
<version>${hibernate.version}</version>
</dependency>Előző verziókban még konfigurálunk kellett a persistence.xml-ben a
listenereket, most már elég, ha a jar a classpath-on van.
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 revisiont, 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 revisionhö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<Object> 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 Enverst 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.
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
Springet (ágyúval verébre), hogy a JdbcTemplate-et használhassam, így
Commons DbUtilsra 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"));