Hallo zusammen,
JPA und Hibernate machen uns das Leben seit Jahres sehr einfach. Persistenz ist mittlerweile keine Schwierigkeit mehr, sondern man macht es einfach. Probleme treten maximal in Randbezirken auf, im Großen und Ganzen ist sowohl die Verwendung als auch die Performance der ORM Mapper (sei es nun Hibernate, EclipseLink oder ein beliebig anderer) mehr als gut.
Aber Persistenz ist nur ein Use-Case einer Enterprise Anwendung. Ein weiterer interessanter Use-Case ist das Auditing von Entities. Auditing beschreibt hierbei quasi eine Historisierung von Entities und Aktionen darauf.
Man könnte diese Funktionalität jetzt quasi selber implementieren. Das gestaltet sich aber gar nicht so einfach, da ein EntityManager zu jeder Zeit nur eine Version einer Entity laden kann. Eine manuelle Implementierung müsste mit Hilfe eines EntityListeners an eine alte Version der Entity kommen, berechnen, welche Daten verändert worden sind und eine weitere Revision speichern. Klingt kompliziert, ist aber tatsächlich noch schwieriger als es sich anhört.
Lassen wir das…
Das Erste was wir machen ist uns ein einfaches Projekt zu erzeugen mit Hibernate als Persistenzprovider. Hierfür definiere ich folgende POM, in der u.a. alle notwendigen Versionen für Spring, Spring-Data, Hibernate und JPA deklariert sind.
Für diesen Use-Case implementieren wir kein Frontend, sondern wir werden uns allein auf Unit-Tests verlassen (die ja letztendlich sowieso die Einzige Wahrheit liefern).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 | <?xml version="1.0" encoding="UTF-8"?> <!-- Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. --> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>de.effectivetrainings</groupId> <artifactId>hibernate-envers</artifactId> <packaging>jar</packaging> <version>1.0-SNAPSHOT</version> <name>Hibernate Envers</name> <description></description> <properties> <jetty.version>7.6.3.v20120416</jetty.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <spring.version>3.1.3.RELEASE</spring.version> <spring.security.version>3.1.3.RELEASE</spring.security.version> <hibernate.version>4.1.2.Final</hibernate.version> </properties> <dependencies> <!-- SPRING AND SPRING-DATA --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-asm</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-orm</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-jpa</artifactId> <version>1.3.0.RELEASE</version> </dependency> <!-- HIBERNATE --> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-core</artifactId> <version>${hibernate.version}</version> </dependency> <dependency> <groupId>org.hibernate.javax.persistence</groupId> <artifactId>hibernate-jpa-2.0-api</artifactId> <version>1.0.1.Final</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-entitymanager</artifactId> <version>${hibernate.version}</version> </dependency> <!-- LOGGING DEPENDENCIES - LOG4J --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.6.4</version> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.16</version> </dependency> <!-- JUNIT DEPENDENCY FOR TESTING --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.10</version> <scope>test</scope> </dependency> <dependency> <groupId>javax.inject</groupId> <artifactId>javax.inject</artifactId> <version>1</version> </dependency> <dependency> <groupId>org.hsqldb</groupId> <artifactId>hsqldb</artifactId> <version>2.2.9</version> </dependency> </dependencies> <build> <resources> <resource> <filtering>false</filtering> <directory>src/main/resources</directory> </resource> <resource> <filtering>false</filtering> <directory>src/main/java</directory> <includes> <include>**</include> </includes> <excludes> <exclude>**/*.java</exclude> </excludes> </resource> </resources> <testResources> <testResource> <filtering>false</filtering> <directory>src/test/resources</directory> </testResource> <testResource> <filtering>false</filtering> <directory>src/test/java</directory> <includes> <include>**</include> </includes> <excludes> <exclude>**/*.java</exclude> </excludes> </testResource> </testResources> <plugins> <plugin> <inherited>true</inherited> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>2.5.1</version> <configuration> <source>1.6</source> <target>1.6</target> <encoding>UTF-8</encoding> <showWarnings>true</showWarnings> <showDeprecation>true</showDeprecation> </configuration> </plugin> <plugin> <groupId>org.mortbay.jetty</groupId> <artifactId>jetty-maven-plugin</artifactId> <version>${jetty.version}</version> <configuration> <connectors> <connector implementation="org.eclipse.jetty.server.nio.SelectChannelConnector"> <port>8080</port> <maxIdleTime>3600000</maxIdleTime> </connector> <connector implementation="org.eclipse.jetty.server.ssl.SslSocketConnector"> <port>8443</port> <maxIdleTime>3600000</maxIdleTime> <keystore>${project.build.directory}/test-classes/keystore</keystore> <password>wicket</password> <keyPassword>wicket</keyPassword> </connector> </connectors> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-eclipse-plugin</artifactId> <version>2.9</version> <configuration> <downloadSources>true</downloadSources> </configuration> </plugin> </plugins> </build> <repositories> <repository> <id>Apache Nexus</id> <url>https://repository.apache.org/content/repositories/snapshots/</url> <releases> <enabled>false</enabled> </releases> <snapshots> <enabled>true</enabled> </snapshots> </repository> </repositories> </project> |
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>de.effectivetrainings</groupId>
<artifactId>hibernate-envers</artifactId>
<packaging>jar</packaging>
<version>1.0-SNAPSHOT</version>
<name>Hibernate Envers</name>
<description></description>
<properties>
<jetty.version>7.6.3.v20120416</jetty.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring.version>3.1.3.RELEASE</spring.version>
<spring.security.version>3.1.3.RELEASE</spring.security.version>
<hibernate.version>4.1.2.Final</hibernate.version>
</properties>
<dependencies>
<!-- SPRING AND SPRING-DATA -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-asm</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>1.3.0.RELEASE</version>
</dependency>
<!-- HIBERNATE -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>${hibernate.version}</version>
</dependency>
<dependency>
<groupId>org.hibernate.javax.persistence</groupId>
<artifactId>hibernate-jpa-2.0-api</artifactId>
<version>1.0.1.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>${hibernate.version}</version>
</dependency>
<!-- LOGGING DEPENDENCIES - LOG4J -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.6.4</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.16</version>
</dependency>
<!-- JUNIT DEPENDENCY FOR TESTING -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.10</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
</dependency>
<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<version>2.2.9</version>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<filtering>false</filtering>
<directory>src/main/resources</directory>
</resource>
<resource>
<filtering>false</filtering>
<directory>src/main/java</directory>
<includes>
<include>**</include>
</includes>
<excludes>
<exclude>**/*.java</exclude>
</excludes>
</resource>
</resources>
<testResources>
<testResource>
<filtering>false</filtering>
<directory>src/test/resources</directory>
</testResource>
<testResource>
<filtering>false</filtering>
<directory>src/test/java</directory>
<includes>
<include>**</include>
</includes>
<excludes>
<exclude>**/*.java</exclude>
</excludes>
</testResource>
</testResources>
<plugins>
<plugin>
<inherited>true</inherited>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.5.1</version>
<configuration>
<source>1.6</source>
<target>1.6</target>
<encoding>UTF-8</encoding>
<showWarnings>true</showWarnings>
<showDeprecation>true</showDeprecation>
</configuration>
</plugin>
<plugin>
<groupId>org.mortbay.jetty</groupId>
<artifactId>jetty-maven-plugin</artifactId>
<version>${jetty.version}</version>
<configuration>
<connectors>
<connector implementation="org.eclipse.jetty.server.nio.SelectChannelConnector">
<port>8080</port>
<maxIdleTime>3600000</maxIdleTime>
</connector>
<connector implementation="org.eclipse.jetty.server.ssl.SslSocketConnector">
<port>8443</port>
<maxIdleTime>3600000</maxIdleTime>
<keystore>${project.build.directory}/test-classes/keystore</keystore>
<password>wicket</password>
<keyPassword>wicket</keyPassword>
</connector>
</connectors>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-eclipse-plugin</artifactId>
<version>2.9</version>
<configuration>
<downloadSources>true</downloadSources>
</configuration>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>Apache Nexus</id>
<url>https://repository.apache.org/content/repositories/snapshots/</url>
<releases>
<enabled>false</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
</project>Spring-Konfiguration
Nachdem wir alle notwendigen Abhängigkeiten deklariert haben definieren wir uns einen einfachen Spring-Context (und ja, das machen wir in XML, weil gerade das Bootstrapping von JPA/Hibernate in XML eleganter ist als in Java – meine Meinung).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jdbc="http://www.springframework.org/schema/jdbc" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context" xmlns:util="http://www.springframework.org/schema/util" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"> <tx:jta-transaction-manager/> <tx:annotation-driven/> <context:annotation-config/> <jpa:repositories base-package="de.effectivetrainings"/> <bean name="emfactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"> <property name="jpaDialect" ref="jpaDialect"/> <property name="packagesToScan" value="de.effectivetrainings"/> <property name="jpaVendorAdapter" ref="jpaVendorAdapter"/> <property name="dataSource" ref="db"/> <property name="jpaProperties" ref="jpaProperties"/> </bean> <bean id="jpaDialect" class="org.springframework.orm.jpa.vendor.HibernateJpaDialect"/> <bean id="jpaVendorAdapter" class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"/> <bean name="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager"> <property name="entityManagerFactory" ref="emfactory"/> </bean> <util:properties id="jpaProperties"> <prop key="hibernate.hbm2ddl.auto">create</prop> </util:properties> <jdbc:embedded-database id="db" type="HSQL"/> </beans> |
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">
<tx:jta-transaction-manager/>
<tx:annotation-driven/>
<context:annotation-config/>
<jpa:repositories base-package="de.effectivetrainings"/>
<bean name="emfactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="jpaDialect" ref="jpaDialect"/>
<property name="packagesToScan" value="de.effectivetrainings"/>
<property name="jpaVendorAdapter" ref="jpaVendorAdapter"/>
<property name="dataSource" ref="db"/>
<property name="jpaProperties" ref="jpaProperties"/>
</bean>
<bean id="jpaDialect" class="org.springframework.orm.jpa.vendor.HibernateJpaDialect"/>
<bean id="jpaVendorAdapter" class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"/>
<bean name="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="emfactory"/>
</bean>
<util:properties id="jpaProperties">
<prop key="hibernate.hbm2ddl.auto">create</prop>
</util:properties>
<jdbc:embedded-database id="db" type="HSQL"/>
</beans>Dieser Spring-Context sorgt dafür, dass
- alle JPA-Entities automatisch gewired sind
- das Tabellen automatisch generiert werden
- das wir eine Embedded-HSQL Datenbank haben, gegen die wir uns verbinden können
- das alle Spring-Data Repositories und auch alle Spring-Sevices automatisch gewired werden
Bootstrapping mit JUnit
Höchste Zeit, das wir den Spring-Container zum ersten mal hochfahren. Hierfür definieren wir den folgenden Test.
1 2 3 4 5 6 7 8 | @ContextConfiguration(value = "classpath:spring-context.xml") public class SpringBootstrapTest extends AbstractJUnit4SpringContextTests { @Test public void configure(){ //nothing to do, just bootstrap } } |
@ContextConfiguration(value = "classpath:spring-context.xml")
public class SpringBootstrapTest extends AbstractJUnit4SpringContextTests {
@Test
public void configure(){
//nothing to do, just bootstrap
}
}Ein einfaches Entity-Modell
Wir definieren uns ein einfaches Entity-Modell.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | @Entity public class Customer implements Serializable { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String name; private String email; @OneToOne(cascade = CascadeType.ALL) private Address address; public Customer() { } public Customer(String name, String email, Address address) { this.name = name; this.email = email; this.address = address; } public Long getId() { return id; } public String getName() { return name; } public String getEmail() { return email; } public Address getAddress() { return address; } } |
@Entity
public class Customer implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String name;
private String email;
@OneToOne(cascade = CascadeType.ALL)
private Address address;
public Customer() {
}
public Customer(String name, String email, Address address) {
this.name = name;
this.email = email;
this.address = address;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public String getEmail() {
return email;
}
public Address getAddress() {
return address;
}
}1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | @Entity public class Address { @Id @GeneratedValue private Long id; private String street; private String zip; private String city; public Address() { } public Address(String street, String zip, String city) { this.street = street; this.zip = zip; this.city = city; } public String getStreet() { return street; } public String getZip() { return zip; } public String getCity() { return city; } } |
@Entity
public class Address {
@Id
@GeneratedValue
private Long id;
private String street;
private String zip;
private String city;
public Address() {
}
public Address(String street, String zip, String city) {
this.street = street;
this.zip = zip;
this.city = city;
}
public String getStreet() {
return street;
}
public String getZip() {
return zip;
}
public String getCity() {
return city;
}
}Ein zugehöriges Repository um Daten zu laden und zu speichern.
1 2 | public interface CustomerRepository extends JpaRepository<Customer, Long> { } |
public interface CustomerRepository extends JpaRepository<Customer, Long> {
}Und folgenden Test, der das Speichern und Laden eines Kunden testet.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | @ContextConfiguration(value = "classpath:spring-context.xml") public class SpringBootstrapTest extends AbstractJUnit4SpringContextTests { @Inject private CustomerRepository customerRepository; @Test public void configure(){ assertNotNull(customerRepository); } @Test public void saveCustomer(){ assertTrue(customerRepository.findAll().isEmpty()); Address address = new Address("test-street","80805","München"); Customer customer = new Customer("Hans","test@test.de", address); Customer persistedCustomer = customerRepository.save(customer); Customer dbCustomer= customerRepository.findOne(persistedCustomer.getId()); assertNotNull(dbCustomer); //some simple sanity checks assertEquals(dbCustomer.getName(),"Hans"); assertEquals(dbCustomer.getAddress().getStreet(),"test-street"); } } |
@ContextConfiguration(value = "classpath:spring-context.xml")
public class SpringBootstrapTest extends AbstractJUnit4SpringContextTests {
@Inject
private CustomerRepository customerRepository;
@Test
public void configure(){
assertNotNull(customerRepository);
}
@Test
public void saveCustomer(){
assertTrue(customerRepository.findAll().isEmpty());
Address address = new Address("test-street","80805","München");
Customer customer = new Customer("Hans","test@test.de", address);
Customer persistedCustomer = customerRepository.save(customer);
Customer dbCustomer= customerRepository.findOne(persistedCustomer.getId());
assertNotNull(dbCustomer);
//some simple sanity checks
assertEquals(dbCustomer.getName(),"Hans");
assertEquals(dbCustomer.getAddress().getStreet(),"test-street");
}
}Wir schreiben Geschichte – Introducing Hibernate Envers
Was wir jetzt gerne möchten ist die Historisierung unserer Änderungen in der Datenbank. Hierfür verwenden wir Hibernate Envers. Zunächst deklarieren wir die Abhängigkeit auf Envers in der POM.
1 2 3 4 5 | <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-envers</artifactId> <version>${hibernate.version}</version> </dependency> |
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-envers</artifactId>
<version>${hibernate.version}</version>
</dependency>Nachdem Envers bereit steht müssen wir nur noch konfigurieren, dass beim Speichern einer Entity auch jeweils eine Revision gespeichert wird.
Seit Envers 4.x werden automatisch die entsprechenden EntityListener registriert, die notwendig sind (mit Envers 3.x musste dies noch manuell gemacht werden).
Wir deklarieren unsere Entities als “auditierbar” mit der Annotation @Audited.
1 2 3 4 5 6 7 | @Entity @Audited public class Customer implements Serializable {} @Entity @Audited public class Address {} |
@Entity
@Audited
public class Customer implements Serializable {}
@Entity
@Audited
public class Address {}Das war es schon, Hibernate wird jedesmal, wenn wir eine Änderung vornehmen und speichern eine neue sogenannte Revision speichern, auf die zu einem späteren Zeitpunkt zugegriffen werden kann. Das SQL zum Speichern eines Customers ist folgendes.
1 2 3 4 5 | Hibernate: insert into Address (id, city, street, zip) values (default, ?, ?, ?) Hibernate: insert into Customer (id, address_id, email, name) values (default, ?, ?, ?) Hibernate: insert into REVINFO (REV, REVTSTMP) values (default, ?) Hibernate: insert into Address_AUD (REVTYPE, city, street, zip, id, REV) values (?, ?, ?, ?, ?, ?) Hibernate: insert into Customer_AUD (REVTYPE, email, name, address_id, id, REV) values (?, ?, ?, ?, ?, ?) |
Hibernate: insert into Address (id, city, street, zip) values (default, ?, ?, ?) Hibernate: insert into Customer (id, address_id, email, name) values (default, ?, ?, ?) Hibernate: insert into REVINFO (REV, REVTSTMP) values (default, ?) Hibernate: insert into Address_AUD (REVTYPE, city, street, zip, id, REV) values (?, ?, ?, ?, ?, ?) Hibernate: insert into Customer_AUD (REVTYPE, email, name, address_id, id, REV) values (?, ?, ?, ?, ?, ?)
Wir sehen, es wurden drei neue Tabellen angelegt
- REVINFO – für jede Änderung wird eine neue Revision angelegt und in der Tabelle REVINFO gespeichert. Eine Revision bezieht sich immer auf eine Änderung an einer Entity zu einem bestimmten Zeitpunkt.
- Address_AUD – Hier werden Änderungen für die Address-Entity gespeichert, aktuell landen alle Attribute in unserem Audit, das heisst für jede Änderung wird quasi eine Kopie der alten Address-Entity gespeichert.
- Customer_AUD – analog Address_AUD werden hier die Audit-Informationen für die Customer-Entity gespeichert.
Die Namen der Audit-Tabellen lässt sich bequem über die Annotation @AuditTable festlegen.
1 2 3 4 5 6 7 8 9 | @Entity @Audited @AuditTable(value = "Address_History") public class Address {} @Entity @Audited @AuditTable(value = "Customer_History") public class Customer implements Serializable {} |
@Entity
@Audited
@AuditTable(value = "Address_History")
public class Address {}
@Entity
@Audited
@AuditTable(value = "Customer_History")
public class Customer implements Serializable {}Geschichtsstunde – Der Auditmanager
Ein einfacher Test zum Auslesen der Customer-Historie sieht so aus
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | @Test public void checkAudit() throws Exception { assertTrue(customerRepository.findAll().isEmpty()); Address address = new Address("test-street", "80805", "München"); Customer customer = new Customer("Hans", "test@test.de", address); Customer persistedCustomer = customerRepository.save(customer); //make some changes persistedCustomer.setName("Georg"); persistedCustomer = customerRepository.save(persistedCustomer); EntityManager entityManager = entityManagerFactory.createEntityManager(); AuditReader auditReader = AuditReaderFactory.get(entityManager); List<Number> revisions = auditReader.getRevisions(Customer.class, persistedCustomer.getId()); assertEquals(revisions.size(), 2); //delete customer customerRepository.delete(persistedCustomer); AuditQuery query=auditReader.createQuery().forRevisionsOfEntity(Customer.class, true, true); List<Customer> result = query.getResultList(); assertEquals(result.size(),3); } |
@Test
public void checkAudit() throws Exception {
assertTrue(customerRepository.findAll().isEmpty());
Address address = new Address("test-street", "80805", "München");
Customer customer = new Customer("Hans", "test@test.de", address);
Customer persistedCustomer = customerRepository.save(customer);
//make some changes
persistedCustomer.setName("Georg");
persistedCustomer = customerRepository.save(persistedCustomer);
EntityManager entityManager = entityManagerFactory.createEntityManager();
AuditReader auditReader = AuditReaderFactory.get(entityManager);
List<Number> revisions = auditReader.getRevisions(Customer.class, persistedCustomer.getId());
assertEquals(revisions.size(), 2);
//delete customer
customerRepository.delete(persistedCustomer);
AuditQuery query=auditReader.createQuery().forRevisionsOfEntity(Customer.class, true, true);
List<Customer> result = query.getResultList();
assertEquals(result.size(),3);
}Zunächst speichern wir einen neuen Kunden, ändern anschließend dessen Namen und löschen den Kunden anschließend.
Zum Auslesen der Kundenhistorie mit Envers kann der mächtige AuditReader verwendet werden. Der AuditReader bringt eine einfache Query-API mit.
Die einfachste Query, die uns alle Revisionen von ALLEN Kunden liefert ist diese
1 2 | AuditQuery query=auditReader.createQuery().forRevisionsOfEntity(Customer.class, true, true); List<Customer> result = query.getResultList(); |
AuditQuery query=auditReader.createQuery().forRevisionsOfEntity(Customer.class, true, true); List<Customer> result = query.getResultList();
Die Methode forRevisionOfEntity erwartet mehrere Parameter.
- Typ der Entität
- Sollen nur die Entitäten (Customer) zurückgeliefert werden oder Array[3]-Elemente mit Entität, Revisionsnummer und RevisionsTyp (DELETE, ADD, MODIFY)
Es ist natürlich wenig performant, sich ständig alle Revisionen für alle Entitäten ausgeben zu lassen.
AuditManager – Finetuning
Wir speichern uns einfach einen zweiten Kunden.
1 2 3 | Address anotherAddres = new Address("test-street", "80805", "München"); Customer anotherCustomer = new Customer("Heinz","test@test.de", anotherAddres); customerRepository.save(anotherCustomer); |
Address anotherAddres = new Address("test-street", "80805", "München");
Customer anotherCustomer = new Customer("Heinz","test@test.de", anotherAddres);
customerRepository.save(anotherCustomer);Um nur die Historie eines bestimmten Kunden zu laden erweitern wir die Query um eine Expression.
1 2 3 4 | AuditQuery query=auditReader.createQuery().forRevisionsOfEntity(Customer.class, true, true); query.add(new IdentifierEqAuditExpression(persistedCustomer.getId(),true)); List<Customer> result = query.getResultList(); assertEquals(result.size(),3); |
AuditQuery query=auditReader.createQuery().forRevisionsOfEntity(Customer.class, true, true); query.add(new IdentifierEqAuditExpression(persistedCustomer.getId(),true)); List<Customer> result = query.getResultList(); assertEquals(result.size(),3);
Envers bringt eine ganze Reihe von Criterions mit.
Wann werden gleich nochmal die Revisions gespeichert?
Nehmen wir folgenden Testcase
1 2 3 4 5 6 7 8 9 10 11 12 | @Test public void saveRevisionIfEmbeddedEntityChanges(){ Address address = new Address("test-street", "80805", "München"); Customer customer = new Customer("Hans", "test@test.de", address); Customer persistedCustomer = customerRepository.save(customer); persistedCustomer.getAddress().setStreet("Teststraße 4"); customerRepository.save(persistedCustomer); AuditReader auditReader = createAuditReader(); assertEquals(2,auditReader.getRevisions(Customer.class,persistedCustomer.getId()).size()); } |
@Test
public void saveRevisionIfEmbeddedEntityChanges(){
Address address = new Address("test-street", "80805", "München");
Customer customer = new Customer("Hans", "test@test.de", address);
Customer persistedCustomer = customerRepository.save(customer);
persistedCustomer.getAddress().setStreet("Teststraße 4");
customerRepository.save(persistedCustomer);
AuditReader auditReader = createAuditReader();
assertEquals(2,auditReader.getRevisions(Customer.class,persistedCustomer.getId()).size());
}Dieser Test schlägt fehl. Wir ändern ein Attribut der eingebetteten Address-Instanz. Hierdurch wird zwar eine neue Revision der Address-Entity gespeichert, aber keine für die “Eltern”-Entität Customer. Normalerweise würde man aber erwarten, dass eine neue Revision gespeichert wird, wann immer sich etwas ändert und sei dies nur ein Attribut einer Instanzvariablen.
Leider ist dies nicht ohne weiteres möglich, wir behelfen uns aber mit einem Workaround. Wir erweitern den Kunden um ein ChangeDate – ein Datum, wann der Kunde zuletzt geändert wurde. Dieses Datum setzen wir jedesmal neu, wenn ein Kunde gespeichert oder geändert wird. Hierdurch erhalten wir jeweils eine neue Revision. Wir brauchen natürlich viel mehr Speicher aber das Handling wird einfacher.
@Column(name = "CHANGE_DATE")
@Temporal(value = TemporalType.TIME)
private Date changeDate;
...
@PrePersist
@PreUpdate
protected void updateChangeDate(){
this.changeDate = new Date();
}
Nach dieser Änderung ist unser Test grün und es ist sichergestellt, dass wir für jede Änderung eine neue Revision bekommen.
Fazit
Ich glaube, es geht kaum einfacher als mit Envers Entitäten zu auditieren. Alleine die Vorstellung dies selbst zu implementieren lässt mich erschauern. Meine Empfehlung – ganz klar einfach verwenden.
Der Code findet sich natürlich wieder in einem GitHub Repository.
Links















