Hibernate_logo_a

Heute schreibst Du Geschichte – Entity Auditing mit Hibernate Envers

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.

  1. Typ der Entität
  2. 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.

Bildschirmfoto 2013-05-20 um 19.26.21

Verfügbare Criterions in Enver

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

Envers Dokumentation

GitHub-Repository

 

 

Going Mobile mit Apache Wicket – Teil 1 – Setup mit Bootstrap

Hallo,

in der folgenden kleinen Artikelserie geht es um einige kleine Tipps und Tricks, um Webanwendungen mit Apache Wicket zu entwickeln, und zwar Responsive. Hierbei gibt es einige Herausforderungen zu meistern.

Was bedeutet Responsive Web / Responsive Design

Um Anwendungen sowohl auf Desktop- als auch auf mobilen Endgeräten nutzbar zu machen gibt es einiges zu beachen.

  • Richtige Skalierung der Anwendung
  • Performance (gerade wichtig für Mobile Endgeräte die im Mobilnetz unterwegs sind)
  • Anzahl der Requests sollte minimiert werden
  • Ladezeiten verkürzen
  • Caching
  • Browser-Inkompatibilitäten (besonders nett bei älteren Android-Devices)
  • Device-Detection (handelt es sich beim aktuellen Kunden um ein Mobile-, oder Tabletdevice oder haben wir einen Desktoprechner)

Fragen die man unbedingt von vorne weg klären sollte

  • Was sind unsere Target-Devices (was nicht gilt ist – ALLE – das ist nicht zu bezahlen), sondern eine valide Antworte wäre IPhone, IPad und Android ab 2.1)
  • Wird eine Variante für alle Devices entwickelt (Responsive!) oder gibt es eine Mobile- und eine Desktopvariante?

Tools der Wahl

Was man um jeden Preis vermeiden sollte ist, das Rad neu zu erfinden, es gibt sehr viele brauchbare Lösungen für viele Probleme, und eins verspreche ich, wer versucht diese selbst zu lösen wird fast garantiert brutal und schnell scheitern.

  • JQuery als Javascript-Basis-Bibliothek (glücklicherweise bereites Bestandteil von Wicket 6)
  • Bootstrap (Bitte, Bitte, Bitte schreiben Sie kein eigenes CSS-Toolkit, sondern verwenden Sie was bereits da ist)
  • Require.js kann verwendet werden, um Abhängigkeiten richtig aufzulösen, ein Teil der Require.js-Funktionalität wird schon durch Wicket und sein Ressourcenmanagement bereitgestellt

Responsive Design auf einen  Blick

Meine Webseite www.effectivetrainings.de ist zwar nicht mit Wicket gemacht sondern Picket (Wicket-PHP Port), aber mit Bootstrap und man sieht sehr schön, was Responsive Design ist und wie es sich anfühlt, wenn man das Browserfenster einfach mal skaliert.

Desktop Medium-Screen Small-Screen
desktop medium small

Die Seite sieht auf allen Browser / Device-Größen zumindest akzeptabel aus.

Fangen wir also an

Zunächst erzeugen wir uns wie immer einen Wicket-Quickstart.

1
mvn archetype:generate -DarchetypeGroupId=org.apache.wicket -DarchetypeArtifactId=wicket-archetype-quickstart -DarchetypeVersion=6.7.0 -DgroupId=de.effectivetrainings -DartifactId=going-mobile -DarchetypeRepository=https://repository.apache.org/ -DinteractiveMode=false
mvn archetype:generate -DarchetypeGroupId=org.apache.wicket -DarchetypeArtifactId=wicket-archetype-quickstart -DarchetypeVersion=6.7.0 -DgroupId=de.effectivetrainings -DartifactId=going-mobile -DarchetypeRepository=https://repository.apache.org/ -DinteractiveMode=false

Wir entscheiden uns natürlich für die Möglichkeit, eine Anwendung für alle Screen-Größen zu entwickeln, um doppelte Arbeit zu sparen und so günstig wie möglich entwickeln zu können.

Als CSS Framework verwenden wir the One-And-Only Bootstrap – für CSS-Laien (wie mich, auch nach Jahren noch…) die erste und beste Wahl.  Wicket bringt hier schon ein kleines Schmankerl mit, denn Bootstrap ist schon an Bord. Um es zu aktivieren deklarieren wir folgende Abhängigkeit in unserer Pom.

1
2
3
4
5
 <dependency>
            <groupId>org.apache.wicket</groupId>
            <artifactId>wicket-bootstrap</artifactId>
            <version>0.8</version>
</dependency>
 <dependency>
            <groupId>org.apache.wicket</groupId>
            <artifactId>wicket-bootstrap</artifactId>
            <version>0.8</version>
</dependency>

Die Bootstrap-Integration ist keine Zauberei und wäre auch in ein paar Minuten selber geschrieben, aber wie ich ja früher im Artikel bereits erwähnt habe – es gilt um jeden Preis zu vermeiden das Rad neu zu erfinden.

Zunächst aber räumen wir unsere generierte Klasse Homepage ein wenig auf und Löschen alles aus HomePage.html was nicht gebraucht wird, bis das Markup so aussieht.

1
2
3
4
5
6
7
8
<!DOCTYPE html>
<html xmlns:wicket="http://wicket.apache.org">
    <head>
    </head>
    <body>
 
    </body>
</html>
<!DOCTYPE html>
<html xmlns:wicket="http://wicket.apache.org">
	<head>
    </head>
	<body>

	</body>
</html>

Sehr klar, oder? Zu beachten ist auf jedenfall die DOCTYPE-Deklaration ganz oben im File. Mit diesem DOCTYPE geben wir den Browsern einen Hinweis, dass wir evtl. mit HTML5 in Berührung kommen.

1
<!DOCTYPE html>
<!DOCTYPE html>

Um die Bootstrap-Integration jetzt zu verwenden machen wir in HomePage.java einfach folgendes.

1
2
3
4
5
6
7
8
9
10
11
12
13
public class HomePage extends WebPage {
    private static final long serialVersionUID = 1L;
 
    public HomePage(final PageParameters parameters) {
        super(parameters);
    }
 
    @Override
    public void renderHead(HtmlHeaderContainer container) {
        super.renderHead(container);
        container.getHeaderResponse().render(JavaScriptHeaderItem.forReference(Bootstrap.responsive()));
    }
}
public class HomePage extends WebPage {
	private static final long serialVersionUID = 1L;

	public HomePage(final PageParameters parameters) {
		super(parameters);
    }

    @Override
    public void renderHead(HtmlHeaderContainer container) {
        super.renderHead(container);
        container.getHeaderResponse().render(JavaScriptHeaderItem.forReference(Bootstrap.responsive()));
    }
}

Wichtig ist in der renderHead()-Methode folgende Zeile.

1
container.getHeaderResponse().render(JavaScriptHeaderItem.forReference(Bootstrap.responsive()));
container.getHeaderResponse().render(JavaScriptHeaderItem.forReference(Bootstrap.responsive()));

Die Seite die uns daraus gerendet wird beinhaltet derzeit zwar noch keinen Inhalt, aber schaut man sich den <head/>-Tag an, sieht man folgendes

1
2
3
4
5
6
7
<head>
    <script type="text/javascript" src="./wicket/resource/org.apache.wicket.resource.JQueryResourceReference/jquery/jquery-ver-1367063998000.js"></script>
<script type="text/javascript" src="./wicket/resource/org.apache.wicket.ajax.AbstractDefaultAjaxBehavior/res/js/wicket-event-jquery-ver-1367063998000.js"></script>
<link rel="stylesheet" type="text/css" href="./wicket/resource/org.apache.wicket.bootstrap.Bootstrap/css/bootstrap-ver-1368356946000.css" />
<link rel="stylesheet" type="text/css" href="./wicket/resource/org.apache.wicket.bootstrap.Bootstrap/css/bootstrap-responsive-ver-1368356946000.css" />
<script type="text/javascript" src="./wicket/resource/org.apache.wicket.bootstrap.Bootstrap/js/bootstrap-ver-1368356946000.js"></script>
</head>
<head>
    <script type="text/javascript" src="./wicket/resource/org.apache.wicket.resource.JQueryResourceReference/jquery/jquery-ver-1367063998000.js"></script>
<script type="text/javascript" src="./wicket/resource/org.apache.wicket.ajax.AbstractDefaultAjaxBehavior/res/js/wicket-event-jquery-ver-1367063998000.js"></script>
<link rel="stylesheet" type="text/css" href="./wicket/resource/org.apache.wicket.bootstrap.Bootstrap/css/bootstrap-ver-1368356946000.css" />
<link rel="stylesheet" type="text/css" href="./wicket/resource/org.apache.wicket.bootstrap.Bootstrap/css/bootstrap-responsive-ver-1368356946000.css" />
<script type="text/javascript" src="./wicket/resource/org.apache.wicket.bootstrap.Bootstrap/js/bootstrap-ver-1368356946000.js"></script>
</head>

Alle notwendigen Bootstrap Ressourcen sind vorhanden und werden gerendert.

Zuletzt definieren wir uns noch ein minimalistisches Basis-Layout, das wir in den folgenden Artikeln weiter ausbauen werden.

Zunächst stellen wir sicher, dass die initiale Skalierung der Seite auf die Breite des Devices angepasst wird. Hierfür wird der Meta-Tag viewPort verwendet. Eine schöne Beschreibung hierzu findet sich in den Links.

1
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="viewport" content="width=device-width, initial-scale=1.0">

Zusätzlich bedienen wir uns zunächst einfach mal an einem Standard-Template von Bootstrap und definieren folgendes Basis-Markup in der HomePage.html

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
<!DOCTYPE html>
<html xmlns:wicket="http://wicket.apache.org">
    <head>
        <!-- Skalierung auf devices, volle Skalierung auf die Weite -->
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
    </head>
    <body>
    <div class="container">
 
        <div class="masthead">
            <ul class="nav nav-pills pull-right">
                <li class="active"><a href="#">Home</a></li>
                <li><a href="#">About</a></li>
                <li><a href="#">Contact</a></li>
            </ul>
            <h3 class="muted">Going Mobile</h3>
        </div>
 
        <hr>
 
        <div class="jumbotron">
            <h1>Jetzt wirds mobil!</h1>
            <p class="lead">Responsive Design mit Bootstrap ist so einfach!</p>
            <a class="btn btn-large btn-success" href="#">Klick me!</a>
        </div>
 
        </div>
    </body>
</html>
<!DOCTYPE html>
<html xmlns:wicket="http://wicket.apache.org">
	<head>
        <!-- Skalierung auf devices, volle Skalierung auf die Weite -->
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
    </head>
	<body>
    <div class="container">

        <div class="masthead">
            <ul class="nav nav-pills pull-right">
                <li class="active"><a href="#">Home</a></li>
                <li><a href="#">About</a></li>
                <li><a href="#">Contact</a></li>
            </ul>
            <h3 class="muted">Going Mobile</h3>
        </div>

        <hr>

        <div class="jumbotron">
            <h1>Jetzt wirds mobil!</h1>
            <p class="lead">Responsive Design mit Bootstrap ist so einfach!</p>
            <a class="btn btn-large btn-success" href="#">Klick me!</a>
        </div>

        </div>
	</body>
</html>

Das sieht gerendert dann so aus (einmal Desktop und einmal als Mobile-Version mit verkleinertem Browser-Fenster).

Desktop Small-Screen
desktop-version small-screen-version

Bis jetzt wars einfach..

Bis jetzt war alles relativ einfach, oder? Im nächsten Artikel werden wir unsere Anwendung mit einigen kleinen Spielereien aufpeppen und werden ziemlich schnell an unsere Grenzen kommen.

Links

Beschreibung des ViewPort MetaTags

Sourcen auf GitHub

 

 

Wicket 6 und CDI – es kann so schön sein

Wicket 6.7.0 ist released, und das ist wie immer ein guter Zeitpunkt um sich einige Features  anzuschauen.

Heute geht es um Wicket in Verbindung mit CDI. Ein äusserst spannendes und praktisches Thema das bisher in diesem Blog sträflich vernachlässigt wurde. Am besten, wir fangen direkt an.

Ein neues Projekt – Wicket Archetype

Zunächst erzeugen wir uns ein neues Projekt mit Hilfe des Wicket-Maven-Archetypes.

mvn archetype:generate -DarchetypeGroupId=org.apache.wicket -DarchetypeArtifactId=wicket-archetype-quickstart -DarchetypeVersion=6.7.0 -DgroupId=de.effectivetrainings -DartifactId=wicket-cdi -DarchetypeRepository=https://repository.apache.org/ -DinteractiveMode=false

Dieses Projekt lässt sich sehr einfach starten.

Um mit CDI arbeiten zu können deklarieren wir zusätzlich eine Depdendency in der pom auf das Wicket-CDI Modul, das mittlerweile nativ mit Wicket mitkommt.

<dependency>
<groupId>org.apache.wicket</groupId>
<artifactId>wicket-cdi</artifactId>
<version>${wicket.version}</version>
</dependency>

Zusätzlich brauchen wir unbedingt eine Implementierung der CDI-Spezifikation. Natürlich verwenden wir hierfür Weld – die Referenzimplementierung.

Hierfür deklarieren wir zusätzlich folgende Abhängigkeiten in unserer Projekt-Pom.

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
   <dependency>
            <groupId>org.jboss.weld</groupId>
            <artifactId>weld-core</artifactId>
            <version>1.1.11.Final</version>
        </dependency>
        <!-- needed for conversation scope propagation -->
        <dependency>
            <groupId>org.jboss.seam.conversation</groupId>
            <artifactId>seam-conversation-weld</artifactId>
            <version>3.0.0.CR2</version>
        </dependency>
        <dependency>
            <groupId>org.jboss.weld.servlet</groupId>
            <artifactId>weld-servlet-core</artifactId>
            <version>1.1.11.Final</version>
        </dependency>
        <dependency>
            <groupId>javax.el</groupId>
            <artifactId>javax.el-api</artifactId>
            <version>2.2.1</version>
        </dependency>
        <dependency>
            <groupId>javax.servlet.jsp</groupId>
            <artifactId>javax.servlet.jsp-api</artifactId>
            <version>2.2.1</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-ext</artifactId>
            <version>1.6.2</version>
        </dependency>
   <dependency>
            <groupId>org.jboss.weld</groupId>
            <artifactId>weld-core</artifactId>
            <version>1.1.11.Final</version>
        </dependency>
        <!-- needed for conversation scope propagation -->
        <dependency>
            <groupId>org.jboss.seam.conversation</groupId>
            <artifactId>seam-conversation-weld</artifactId>
            <version>3.0.0.CR2</version>
        </dependency>
        <dependency>
            <groupId>org.jboss.weld.servlet</groupId>
            <artifactId>weld-servlet-core</artifactId>
            <version>1.1.11.Final</version>
        </dependency>
        <dependency>
            <groupId>javax.el</groupId>
            <artifactId>javax.el-api</artifactId>
            <version>2.2.1</version>
        </dependency>
        <dependency>
            <groupId>javax.servlet.jsp</groupId>
            <artifactId>javax.servlet.jsp-api</artifactId>
            <version>2.2.1</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-ext</artifactId>
            <version>1.6.2</version>
        </dependency>

Zuletzt müssen wir noch das Jboss-Repository (oder müsste das mittlerweile Wildfly-Repository:) heissen?) integrieren, da die Weld-Ressourcen aus einem mir nicht erfindlichen Grund nicht im öffentlichen Maven-Repository hinterlegt sind.

1
2
3
4
<repository>
<id>jboss</id>
<url>https://repository.jboss.org/nexus/content/groups/public/</url>
</repository>
<repository>
<id>jboss</id>
<url>https://repository.jboss.org/nexus/content/groups/public/</url>
</repository>

Damit Weld / CDI mit unseren Container (Jetty) startet deklarieren wir noch einen ServletContextListener in der web.xml

1
2
3
<listener>
<listener-class>org.jboss.weld.environment.servlet.Listener</listener-class>
</listener>
<listener>
<listener-class>org.jboss.weld.environment.servlet.Listener</listener-class>
</listener>
<em>Hinweis - das initiale Projekt-Setup mit allen Abhängigkeiten findet man im Commit 62479a3888ca93dc6d9095bafb4858df014ad8d1</em>

Immer auf den Scope fokussieren – CDI Scopes in Wicket

Beim Start der Anwendung sieht man sofort, ob CDI mit dem Container startet, wenn man folgende Meldung in der Konsole sieht.

INFO  - Version                    - WELD-000900 1.1.11 (Final)

Jetzt sind wir bereit, uns so richtig schön mit Scopes, Conversations und Dependency Injection zu beschäftigen.

Wicket mit CDI verheiraten

Das Projekt ist soweit vorbereitet und Wicket möchte gerne eine gepflegte Konversation mit CDI führen. Hierfür müssen wir noch einige kleine Schritte vornehmen. Zunächst brauchen wir den BeanManager, der uns vom zuvor konfigurierten ServletContextListener bereitgestellt wird.

Hierzu definieren wir eine neue Methode initCDI() in unserer Application-Klasse und rufen diese Methode aus der bekannten init()-Methode auf.

1
2
3
4
5
6
7
8
9
10
11
12
   @Override
   public void init()
   {
        super.init();
        initCDI();
   }
 
    private void initCDI(){
        BeanManager manager = (BeanManager)getServletContext().getAttribute(
            Listener.BEAN_MANAGER_ATTRIBUTE_NAME);
        new CdiConfiguration(manager).configure(this);
    }
   @Override
   public void init()
   {
        super.init();
        initCDI();
   }

    private void initCDI(){
        BeanManager manager = (BeanManager)getServletContext().getAttribute(
            Listener.BEAN_MANAGER_ATTRIBUTE_NAME);
        new CdiConfiguration(manager).configure(this);
    }

Den BeanManager können wir uns direkt aus dem ServletContext laden und übergeben diesen in eine neue Instanz der CdiConfiguration (hier sind wir bereits in der Wicket-Welt). Mit Hilfe der Methode configure(..) binden wir die CdiConfiguration an unsere Application und wir sind bereit für CDI und Wicket.

Mehr ist übrigens nicht zu tun. Die CdiConfiguration kümmert sich bereits um die Registrierung der entsprechenden RequestCycle- und ComponentInstantiationListener. Sehr schön gemacht.

<em>Hinweis - Die Konfiguration für CDI findet man im Commit 6eb7a81549a9138f586fc49b13e434457e77f033</em>

Bitte injezieren Sie..

Um CDI testen zu können bauen wir uns zunächst mal eine kleine Mini-Domäne. Wir bauen einen Miniatur-Online-Shop und brauchen hierfür einige Produkte.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Product implements Serializable{
 
    private String name;
    private Double prize;
 
    public Product(String name, Double prize) {
        this.name = name;
        this.prize = prize;
    }
 
    public String getName() {
        return name;
    }
 
    public Double getPrize() {
        return prize;
    }
}
public class Product implements Serializable{

    private String name;
    private Double prize;

    public Product(String name, Double prize) {
        this.name = name;
        this.prize = prize;
    }

    public String getName() {
        return name;
    }

    public Double getPrize() {
        return prize;
    }
}

Und einen Provider für unsere Schuhe.

1
2
3
4
5
6
7
8
9
10
11
12
13
@ApplicationScoped
public class ProductProvider {
 
    public List<Product> productList(){
 
        Product samba = new Product("Adidas Samba",49.95);
        Product air = new Product("Nike Air",79.95);
        Product balerina = new Product("Balerinas",9.95);
 
        return Arrays.asList(samba, air, balerina);
 
    }
}
@ApplicationScoped
public class ProductProvider {

    public List<Product> productList(){

        Product samba = new Product("Adidas Samba",49.95);
        Product air = new Product("Nike Air",79.95);
        Product balerina = new Product("Balerinas",9.95);

        return Arrays.asList(samba, air, balerina);

    }
}

Der Provider liegt im ApplicationScope, da er von allen Benutzern unserer Webseite verwendet wird.

Injezieren können wir in alles, was von Wicket gemanaged wird und die ComponentInstantiationListeneres notifiziert – also Session, Komponenten, die Application und Behaviors.

Wir löschen zunächst alles aus der Homepage und dem zugehörigen Markup, injezieren uns den Provider und zeigen alle Produkte in einem ListView an.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class HomePage extends WebPage {
    private static final long serialVersionUID = 1L;
 
    @Inject
    ProductProvider provider;
 
    public HomePage(final PageParameters parameters) {
        super(parameters);
                add(new ListView<Product>("productsList", provider.productsList()) {
                     @Override
                     protected void populateItem(ListItem<Product> item) {
                          item.add(new Label("name", new PropertyModel<String>(item.getModel(),"name")));
                          item.add(new Label("prize", new PropertyModel<Double>(item.getModel(),"prize")));
                     }
                });
    }
}
public class HomePage extends WebPage {
	private static final long serialVersionUID = 1L;

    @Inject
    ProductProvider provider;

    public HomePage(final PageParameters parameters) {
		super(parameters);
                add(new ListView<Product>("productsList", provider.productsList()) {
                     @Override
                     protected void populateItem(ListItem<Product> item) {
                          item.add(new Label("name", new PropertyModel<String>(item.getModel(),"name")));
                          item.add(new Label("prize", new PropertyModel<Double>(item.getModel(),"prize")));
                     }
                });
    }
}

und das zugehörige Markup.

1
2
3
4
5
6
7
8
9
<!DOCTYPE html>
<html xmlns:wicket="http://wicket.apache.org">
    <body>
        <div wicket:id="productsList">
            <span wicket:id="name"></span>
            <span wicket:id="prize"></span>
        </div>
    </body>
</html>
<!DOCTYPE html>
<html xmlns:wicket="http://wicket.apache.org">
	<body>
        <div wicket:id="productsList">
            <span wicket:id="name"></span>
            <span wicket:id="prize"></span>
        </div>
	</body>
</html>

Die beans.xml wird immer vergessen…

Beim Zugriff auf die Seite bekommen wir als allererstes eine Unsatisfied-Dependency Exception. CDI ist zwar konfiguriert, aber inaktiv, denn das scannen nach Beans und Abhängigkeiten passiert erst, wenn wir die so gehasste beans.xml im Verzeichnis src/main/resources/META-INF/beans.xml anlegen mit folgendem Inhalt.

1
2
3
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/beans_1_0.xsd">
</beans>
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/beans_1_0.xsd">
</beans>

Endlich ist es soweit

wicket cdi und weld in aktion

Unser ProductProvider wird injeziert und funktioniert. Ideal ist das aber nicht, was wir hier machen. Denn eigentlich sollte die Liste von verfügbaren Produkten nicht direkt beim ProductProvider angefragt werden, sondern über ein entsprechendes LoadableDetachableModel. Machen wir das doch einfach mal.

Injezieren in Non-Managed Klassen

Hierfür bauen wir uns folgendes Model.

1
2
3
4
5
6
7
8
9
10
public class ProductsLoadableDetachableModel extends LoadableDetachableModel<List<Product>> {
 
    @Inject
    ProductProvider provider;
 
    @Override
    protected List<Product> load() {
        return provider.productList();
    }
}
public class ProductsLoadableDetachableModel extends LoadableDetachableModel<List<Product>> {

    @Inject
    ProductProvider provider;

    @Override
    protected List<Product> load() {
        return provider.productList();
    }
}

Funktioniert das? NEIN! Denn Models sind nicht unter Wicket-Kontrolle – hier haben wir per Definition keine Möglichkeit, uns Abhängigkeiten über CDI injezieren zu lassen… Oder?

Das Prinzip ist immer dasselbe, ob wir jetzt mit Spring, Guice oder CDI arbeiten. Für die Kombination mit Spring würde man das allseits bekannte Injector.get().inject(this) verwenden.

Das CDI-Pendant hierfür heisst  CdiContainer.get().getNonContextualManager().inject(this);

Die korrekte Model-Implementierung sieht also so aus.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ProductsLoadableDetachableModel extends LoadableDetachableModel<List<Product>> {
 
    @Inject
    ProductProvider provider;
 
    public ProductsLoadableDetachableModel() {
        CdiContainer.get().getNonContextualManager().inject(this);
    }
 
    @Override
    protected List<Product> load() {
        return provider.productList();
    }
}
public class ProductsLoadableDetachableModel extends LoadableDetachableModel<List<Product>> {

    @Inject
    ProductProvider provider;

    public ProductsLoadableDetachableModel() {
        CdiContainer.get().getNonContextualManager().inject(this);
    }

    @Override
    protected List<Product> load() {
        return provider.productList();
    }
}

Fahren wir die Anwendung hoch, funktioniert das Ganze natürlich.

Einige CDI-Spielereien

Jetzt können wir uns noch einige Spezialitäten von CDI anschauen, wir können beispielsweise statt uns den ProductProvider direkt in das Model zu injezieren mit @Produces arbeiten.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@ApplicationScoped
public class ProductProvider {
 
    public ProductProvider(){}
 
    @Produces
    public List<Product> productList(){
 
        Product samba = new Product("Adidas Samba",49.95);
        Product air = new Product("Nike Air",79.95);
        Product balerina = new Product("Balerinas",9.95);
 
        return Arrays.asList(samba, air, balerina);
 
    }
}
@ApplicationScoped
public class ProductProvider {

    public ProductProvider(){}

    @Produces
    public List<Product> productList(){

        Product samba = new Product("Adidas Samba",49.95);
        Product air = new Product("Nike Air",79.95);
        Product balerina = new Product("Balerinas",9.95);

        return Arrays.asList(samba, air, balerina);

    }
}

Wir deklarieren im ProductProvider dass dieser eine Liste von Produkten bereitstellt. Im Model sieht das Ganze dann so aus.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ProductsLoadableDetachableModel extends AbstractReadOnlyModel<List<Product>> {
 
    @Inject
    List<Product> productList;
 
    public ProductsLoadableDetachableModel() {
        CdiContainer.get().getNonContextualManager().inject(this);
    }
 
    @Override
    public List<Product> getObject() {
        return productList;
    }
}
public class ProductsLoadableDetachableModel extends AbstractReadOnlyModel<List<Product>> {

    @Inject
    List<Product> productList;

    public ProductsLoadableDetachableModel() {
        CdiContainer.get().getNonContextualManager().inject(this);
    }

    @Override
    public List<Product> getObject() {
        return productList;
    }
}

Achtung, durch das injezieren der Liste lösen wir zwar die Abhängigkeit zum ProductProvider auf, machen die Liste aber statisch. Wir würden keine Updates auf unsere Produktliste für diese Model-Instanz bekommen.

Kauf dir nen Schuh..

Zuletzt bauen wir uns noch die Möglichkeit, einen Schuh zu kaufen.

Hierfür bauen wir die Page folgendermaßen um.

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
public class HomePage extends WebPage {
    private static final long serialVersionUID = 1L;
 
    @Inject
    private Event<Product> productEvent;
 
    public HomePage(final PageParameters parameters) {
        super(parameters);
 
        ListView<Product> listView = new ListView<Product>("productsList", new ProductsLoadableDetachableModel()) {
            @Override
            protected void populateItem(ListItem<Product> item) {
                Form<Product> form = new Form<Product>("form", item.getModel()){
                    @Override
                    protected void onSubmit() {
                        super.onSubmit();
                        productEvent.fire(getModelObject());
                    }
                };
                item.add(form);
                form.add(new Label("name", new PropertyModel<String>(item.getModel(), "name")));
                form.add(new Label("prize", new PropertyModel<Double>(item.getModel(), "prize")));
            }
        };
        listView.setReuseItems(true);
        add(listView);
    }
}
public class HomePage extends WebPage {
	private static final long serialVersionUID = 1L;

    @Inject
    private Event<Product> productEvent;

	public HomePage(final PageParameters parameters) {
		super(parameters);

        ListView<Product> listView = new ListView<Product>("productsList", new ProductsLoadableDetachableModel()) {
            @Override
            protected void populateItem(ListItem<Product> item) {
                Form<Product> form = new Form<Product>("form", item.getModel()){
                    @Override
                    protected void onSubmit() {
                        super.onSubmit();
                        productEvent.fire(getModelObject());
                    }
                };
                item.add(form);
                form.add(new Label("name", new PropertyModel<String>(item.getModel(), "name")));
                form.add(new Label("prize", new PropertyModel<Double>(item.getModel(), "prize")));
            }
        };
        listView.setReuseItems(true);
        add(listView);
    }
}

Und das entsprechende Markup.

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html xmlns:wicket="http://wicket.apache.org">
<body>
<div wicket:id="productsList">
    <form wicket:id="form">
        <span wicket:id="name"></span>
        <span wicket:id="prize"></span>
        <input type="submit" value="bestellen">
    </form>
</div>
</body>
</html>
<!DOCTYPE html>
<html xmlns:wicket="http://wicket.apache.org">
<body>
<div wicket:id="productsList">
    <form wicket:id="form">
        <span wicket:id="name"></span>
        <span wicket:id="prize"></span>
        <input type="submit" value="bestellen">
    </form>
</div>
</body>
</html>

cdi-weld-order-by-event

Wir stellen also ein Formular bereit, über das unsere Schuhe bestellt werden können.

Interessant ist die neue Instanzvariable

1
2
 @Inject
 private Event<Product> productEvent;
 @Inject
 private Event<Product> productEvent;

Wir verwenden den CDI-Eventmechanismus um Schuhe zu kaufen. Beim Klick auf den Submit-Button wird die onSubmit-Methode der Form-Klasse aufgerufen und hier feuern wir ein neues Event.

1
2
 super.onSubmit();
 productEvent.fire(getModelObject());
 super.onSubmit();
 productEvent.fire(getModelObject());

Zuletzt bauen wir uns einen Miniatur-Product-Service, der auf dieses Event reagiert.

1
2
3
4
5
6
7
@ApplicationScoped
public class ProductService {
 
    public void order(@Observes Product product){
        System.out.println("Produkt " +   product.getName() + " bestellt");
    }
}
@ApplicationScoped
public class ProductService {

    public void order(@Observes Product product){
        System.out.println("Produkt " +   product.getName() + " bestellt");
    }
}

Und bestellen wir beispielsweise den Nike-Schuh sehen wir in der Konsole wie erwartet.

1
Produkt Nike Air bestellt
Produkt Nike Air bestellt

Fazit

Die Integration von CDI und Wicket ist genauso einfach wie mit Spring oder Guice und sieht sehr vielversprechend aus. Ich hoffe, ich habe demnächst die Gelegenheit, dieses nette Duo mal in einem Projekt verwenden zu dürfen.

Links

Anleitung von Igor Vaynberg zu Wicket und CDI
Schöne Anleitung zu CDI zum Ausprobieren
API-Docs zu Wicket & CD
Sourcen auf Github

 

 

Hibernate – Collection-Size mit Lazy-Initialization

Es gibt einige klassische Probleme, die man mit JPA ständig zu lösen hat, für die aber scheinbar kaum jemand eine Sofort-Lösung parat hat.

Ein Klassiker dieser Gattung ist die Größe einer Collection zu erfragen, ohne diese vollständig zu initialisieren.

Nehmen wir das Standardbeispiel eines Blogs.

Ein Blog besteht aus Einträgen und Kommentaren. Die Entity für einen Blogeintrag kann beispielsweise so aussehen:

@Entity
@Table(name="POST")
public class Post {

    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    @Column(name="POST_ID")
    Integer postId;

    @Column(name="TITLE")
    String title;

    @Column(name="POST_DATE")
    Date postDate;

    @OneToMany(cascade = CascadeType.ALL)
    @JoinColumn(name="POST_ID",referencedColumnName = "POST_ID")
    private List<Comment> comments = new ArrayList<Comment>();

    public Integer getPostId() {
        return postId;
    }

    public void setPostId(Integer postId) {
        this.postId = postId;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public Date getPostDate() {
        return postDate;
    }

    public List<Comment> getComments(){
        return comments;
    }

    public void addComment(Comment comment){
        this.comments.add(comment);
    }

    @PrePersist
    public void setPostDate() {
        this.postDate = new Date();
    }

}</pre>

Das Setup ist ein Standard-Hibernate/Spring/JPA Setup. Beispielsweise können wir mit Spring-Data für Posts folgendes Repository definieren, um auf die Einträge zuzugreifen.

public interface PostRepository extends JpaRepository<Post, Integer> { }

Um das Ganze zu testen definieren wir folgenden Test-Case

<pre>@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="classpath:META-INF/test-context.xml")
public class PostRepositoryTest {

    @Autowired
    PostRepository repository;

    @Test
    public void test() {
        Post post = new Post();
        Comment firstComment = new Comment("Kommentar Text1","Martin");
        post.addComment(firstComment);
        Comment secondComment = new Comment("Kommentar Text1","Martin");
        post.addComment(secondComment);
        post.setTitle("First Post");

        repository.save(post);

        Post dbpost = repository.findOne(post.getPostId());
        assertNotNull(dbpost);
        System.out.println(dbpost.getTitle());
    }

}</pre>

Das generierte DDL-SQL von Hibernate sieht so aus

Hibernate: create table POST (POST_ID integer generated by default as identity, POST_DATE timestamp, TITLE varchar(255), primary key (POST_ID))
Hibernate: create table comments (id bigint generated by default as identity, author varchar(255), comment varchar(255), POST_ID integer, primary key (id))
Hibernate: alter table comments add constraint FKDC17DDF4D3BC1BD8 foreign key (POST_ID) references POST

Für die Inserts generiert Hibernate folgende Queries

Hibernate: insert into POST (POST_ID, POST_DATE, TITLE) values (null, ?, ?)
Hibernate: insert into comments (id, author, comment) values (null, ?, ?)
Hibernate: insert into comments (id, author, comment) values (null, ?, ?)
Hibernate: update comments set POST_ID=? where id=?
Hibernate: update comments set POST_ID=? where id=?

Soweit alles Standard. Jetzt wird es interessant. Wir möchten die Anzahl an Kommentaren ermitteln. Kein Problem oder?

Zunächst definieren wir eine neue Methode getCommentsCount() in der Post-Entity

1
2
3
 public Integer getCommentsCount(){
        return comments.size();
 }
 public Integer getCommentsCount(){
        return comments.size();
 }

Den Test ergänzen wir um einen einfachen Assert.

assertEquals(new Integer(2), post.getCommentsCount());

Das funktioniert nicht, aber nur deswegen, weil Hibernate die Collection nicht direkt beim Laden initialisiert (Lazy-Initialization). Das bedeutet, Kommentare werden nur geladen, wenn auch wirklich auf sie zugegriffen wird. Das Nachladen einer Lazy-Initialisierten Collection funktioniert aber leider nur im Kontext einer Transaktion und nicht mit einer Entity die bereits detached ist.
Der Test spiegelt das auch sofort wieder.

1
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: de.effectivetrainings.entities.Post.comments, could not initialize proxy - no Session
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: de.effectivetrainings.entities.Post.comments, could not initialize proxy - no Session

Welche Möglichkeiten haben wir jetzt?

Eager Initialization

Wir können Hibernate anweisen, die Collection Eager zu initialisieren, das bedeutet, immer wenn eine Post-Entity geladen wird, wird Hibernate auch gleich alle Kommentare laden.

@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@JoinColumn(name="POST_ID",referencedColumnName = "POST_ID")
private List<Comment> comments = new ArrayList<Comment>();

Hibernate generiert uns folgendes SQL.

1
2
Hibernate: select post0_.POST_ID as POST1_0_0_, post0_.POST_DATE as POST2_0_0_, post0_.TITLE as TITLE0_0_ from POST post0_ where post0_.POST_ID=?
Hibernate: select comments0_.POST_ID as POST4_0_1_, comments0_.id as id1_1_, comments0_.id as id1_0_, comments0_.author as author1_0_, comments0_.comment as comment1_0_ from comments comments0_ where comments0_.POST_ID=?
Hibernate: select post0_.POST_ID as POST1_0_0_, post0_.POST_DATE as POST2_0_0_, post0_.TITLE as TITLE0_0_ from POST post0_ where post0_.POST_ID=?
Hibernate: select comments0_.POST_ID as POST4_0_1_, comments0_.id as id1_1_, comments0_.id as id1_0_, comments0_.author as author1_0_, comments0_.comment as comment1_0_ from comments comments0_ where comments0_.POST_ID=?

Aus Performance-Gründen kann das nicht erwünscht sein, beispielsweise wenn wir nur eine Liste von Posts anzeigen möchten aber an den Kommentaren zunächst gar nicht interessiert sind. Jetzt haben wir ein Dilemma, aus dem es scheinbar keinen Ausweg gibt, oder? Dachte ich auch, bis ich auf ein spezielles Feature von Hibernate gestossen bin.

Extra-Lazy Initialization

Hibernate bietet ein sehr nettes Feature das sich Extra-Lazy Initialization nennt. Hier initialisiert Hibernate eine Collection teilweise, aber nur die Elemente, die tatsächlich gebraucht werden, was sogar einzelne Elemente in der Liste sein können.

Probieren wir es aus.

Wir ändern hierfür das Collection-Mapping für die Kommentare folgendermaßen.

 @OneToMany(cascade = CascadeType.ALL)
 @JoinColumn(name="POST_ID",referencedColumnName = "POST_ID")
 @LazyCollection(LazyCollectionOption.EXTRA)
 private List<Comment> comments = new ArrayList<Comment>();

Das allein macht unseren Test nicht grün. Zusätzlich fügen wir folgende private Methode in die Post-Klasse ein.

 @PostLoad
 private void initCommentCount(){
        comments.size();
 }

Jedesmal wenn ein Post geladen wird, fragen wir die Größe der Kommentar-Collection ab. Das SQL das jetzt von Hibernate generiert wird ist folgendes:

Hibernate: select post0_.POST_ID as POST1_1_0_, post0_.POST_DATE as POST2_1_0_, post0_.TITLE as TITLE1_0_ from POST post0_ where post0_.POST_ID=?
Hibernate: select count(id) from comments where POST_ID =?

Hibernate ist wirklich unglaublich klug und erkennt, dass nicht die Collection selbst sondern die die Größe benötigt wird. Deswegen wird auch kein Select auf alle Kommentare sondern nur ein select(count) ausgeführt, was aus Performance-Sicht optimal ist.Das lässt sich auch beweisen, in dem wir einfach mal auf ein Element in der Collection zugreifen.

 dbpost.getComments().get(0);

verursacht nach wie vor unsere

1
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: de.effectivetrainings.entities.Post.comments, could not initialize proxy - no Session
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: de.effectivetrainings.entities.Post.comments, could not initialize proxy - no Session

Fazit

Das ist ein sehr oft diskutiertes Problem, ich finde diese Lösung ziemlich elegant und beeindruckend. Ich hoffe, der/die Eine oder Andere kann damit was anfangen.

Der Code zu diesem Beispiel findet sich übrigens auf GitHub.

 

Enterprise GIT – Automatisches Einfügen der Task-Nummer

Je länger man mit GIT arbeitet, desto einfacher kommt einem die Arbeit vor.

Man arbeitet irgendwann ganz automatisch mit Feature / Hotfix und Release-Branches und die Arbeit geht einfach flüssig von der Hand.

Stellen wir uns aber folgenes Szenario vor:

Ein Business-Owner stellt einen Bug ein mit etwa folgendem Wortlaut:

“Warum ist der Button XY denn wieder grau hinterlegt. Das war doch schon mal anders.”

Prio – Selbstverständlich – BLOCKER.

Um nachvollziehen zu können, wann diese Änderung gemacht wurde ist es fast unumgänglich, die Task-Nummer in der Commit-Message eines Commits zu hinterlegen. Ich selbst mache das (meistens), wenn ich denn dran denke. Eine “perfekte” Commit-Message sieht für mich so aus

[Task-4711] Changed Button Background Color to Gray

Um nicht jedesmal daran denken zu müssen lässt sich das Ganze mit Hilfe von Git-Hooks automatisieren.

In jedem Git-Repository stellt GIT eine Reihe von “brauchbaren” Hooks zur Verfügung.

applypatch-msg.sample
post-update.sample
pre-commit.sample
prepare-commit-msg.sample
commit-msg.sample
pre-applypatch.sample
pre-rebase.sample
update.sample

Um eine Commit-Message anzupassen, noch bevor der Commit gemacht wird kann der prepare-commit-msg-Hook verwendet werden.

Wie könnte das aussehen. Um eine Task-Nummer automatisch parsen zu können muss der Branch-Name ein bestimmtes Format haben.

Wir gehen davon aus, dass ein Feature-Branch in unserem Projekt immer die folgende Form hat

fb_task-4711_eine_beschreibung

Daraus möchten wir gerne die Task-Nummer, also task-4711 extrahieren und vorne an unsere Commit-Message anhängen.

Ein kleines Bash-Skript, dass das bewerkstelligt habe ich hier zusammengestöpselt

#!/bin/sh

#author Martin Dilger - EffectiveTrainings.de
#get the original commit msg
orig_msg=$(cat $1)
#expects branchnames in the form fb_task-4711_some_description
branchName=$(git rev-parse --abbrev-ref HEAD)
#branches may start with fb_task and then have "-1234"
regexpForBranches='^fb_task-[0-9]+_'

branchMatches=$(echo $branchName | grep -E $regexpForBranches)
if [ "$branchMatches" ] ;
then
echo "matched branch";
#split branchname by underscore and take the second chunk
task=$(echo $branchName | cut -f 2  -d '_')
#prepend task name to original msg
msg="[$task] - $orig_msg"
echo "$msg" > "$1"
else
echo "[ATTENTION] - branch name does not match, no task number in branch but committing";
fi

Die Kommentare sprechen hoffentlich für sich.

Im Prinzip holen wir uns den aktuellen Branch-Namen mit git rev-parse –abbrev-ref HEAD und prüfen zunächst mit der Regular Expression ^fb_task-[0-9]+_ ob der Branch-Name überhaupt dem erwarteten Format entspricht. Ist das Pattern korrekt splitten wir den Branchnamen an “_” und kommen so an die Task-Nummer. (cut etc.. und wir nehmen das zweite Element).

Mit Hilfe von msg=”[$task] – $orig_msg” bauen wir uns eine neue Commit Message zusammen, in dem wir die Tasknummer einfach vorne anfügen und in die bereits bestehende Commit-Message einfügen mit echo “$msg” > “$1″

$1 in diesem Fall ist nichts anderes als ein File im .git-Verzeichnis, in dem die Commit-Message zwischengespeichet wird (das File heisst COMMIT_EDITMSG).

Spielen wir das Szenario mal durch:

git init
Initialized empty Git repository in /Users/martindilger/development/git/.git/

Wir benennen die Datei prepare-commit-msg.sample im Verzeichnis .git/hooks in prepare-commit-msg um und aktivieren so den Hook.

Dann löschen wir alles in dieser Datei und kopieren das obige Skript hinein. Ggf. muss die Regular-Expression und das Cut-Skript an die jeweilige Branch-Naming-Struktur angepasst werden.

Wir mache einen ersten Commit den wir brauchen um überhaupt Branches anlegen zu können.

echo "mein commit" > test.txt; git add .; git commit -m "der erste commit"
[master (root-commit) 73ff1a4] der erste commit
1 file changed, 1 insertion(+)
create mode 100644 test.txt

Wir erzeugen uns einen neuen Branch mit einem korrekten Namen

git checkout -b fb_task-4711-ganz-wichtig
Switched to a new branch 'fb_task-4711-ganz-wichtig'

und machen direkt einen Commit, und zwar ohne Task-Nummer

echo "ein neuer commit" > new.txt; git add new.txt; git commit -m "message ohne tasknummer"
matched branch
[fb_task-4711_ganz-wichtig 7e7446f] [task-4711] - message ohne tasknummer
1 file changed, 1 insertion(+)
create mode 100644 new.txt

Mit git log sehen wir , dass die Tasknummer korrekt eingefügt wurde.

git log
Commit:  7e7446f03cd91f70d120aade24a6d33a3caa8abd
Author:  dilgerm <martin@effectivetrainings.de>
Date:    (38 seconds ago) 2013-04-18 21:55:46 +0200
Subject: [task-4711] - message ohne tasknummer

Nun benennen wir den Branch um, und zwar in ein Format das nicht passt.

git branch -m fb-nicht-so-wichtig
~/development/.../test2$ git branch
* fb-nicht-so-wichtig
master

Falls der Branchname nicht passt möchten wir auf keinen Fall, dass irgendwelcher “Blödsinn” in der Commit-Message landet. Deswegen der Check mit RegExp.

Wir machen einen letzten Commit

echo "ein neuerer commit" > new.txt; git add new.txt; git commit -m "message ohne tasknummer"
[ATTENTION] - branch name does not match, no task number in branch but committing
[fb-nicht-so-wichtig d5697df] message ohne tasknummer
1 file changed, 1 insertion(+), 1 deletion(-)

und git log zeigt die Message ohne Änderung.

git log
Commit:  d5697dff5690083fd138d39820ebcdafd8cb29aa
Author:  dilgerm <martin@effectivetrainings.de>
Date:    (23 seconds ago) 2013-04-18 21:58:40 +0200
Subject: message ohne tasknummer

War dieser Blogeintrag für Euch interessant? Vielleicht kann ich noch mehr für Euch tun – gerade im Bereich GIT biete ich interessante Trainings und Consulting an.

Einige interessante Einträge zum Thema GIT:

Besser GITs nicht Slides (Herbstcampus 2012)

Besser GITs nicht – Artikel im Java Magazin

 

Der Weg zum Meister – meine 10.000 Stunden als Entwickler

Ten thousand hours of practice is required to achieve the level of mastery associated with being a world-class expert — in anything.

Daniel Levitin

Die 10.000 Stunden Regel sollte jedem Entwickler (und auch jedem anderen) ein Begriff sein. Die Regel basiert ganz lose auf den Studien von Anders Ericcson und wurde hauptsächlich durch das Buch “Outliers” von Malcolm Gladwell bekannt.

Die Regel ist nicht wissenschaftlich belegt sondern ist eher als Richtlinie zu verstehen und besagt, dass jeder der auf seinem Fachgebiet führend sein oder werden möchte, mindestens 10.000 Stunden “Deep bzw. Delibate Practice” benötigt.

Was bedeutet Deep Practice?

Karl der Entwickler

Erstellt auf sp-studio.de

Nehmen wir zum Beispiel Karl.

Karl ist Entwickler und das seit 10 Jahren. Karl ist aber niemand, der sich intensiv mit seiner Arbeit und seiner Tätigkeit identifiziert, sondern ein 09:00 – 17:00 Entwickler.

Karl ist auch niemand, der sich in seiner Freizeit mit neuen Themen, Programmiersprachen und Problemstellungen auseinandersetzt.

Karl macht seinen Job – gut und zufriedenstellend – aber nicht herausragend.

Man könnte jetzt annehmen, das Karl die 10.000 Stunden schon lange geknackt hat und als Meister seines Faches zählt.

Mal im Kopf überschlagen:

220 (grob Arbeitstage pro Jahr abzgl. Urlaub) * 8 (Stunden pro Tag) * 10 Jahre = 17.600 Stunden

So gerechnet müsste Karl also ein Superstar und ein Held in seinem Metier sein. Das was Karl aber tagtäglich macht ist keine Deliberate Practice sondern sein Tagewerk, und Tagewerk zählt nicht in die 10.000 Stunden Regel.

Was wäre wirkliche Praxis in Karls Job als Entwickler:

  • Auseinandersetzung mit komplexen Problemen, die neue Denkmuster erfordern
  • Neue Programmiersprachen, Konzepte und Ansätze erlernen und meistern
  • komplexe Algorithmen verstehen
  • Pair Programming und Auseinandersetzung mit Kollegen
  • Konzepte lehren und schulen
  • Fokussierung und Konzentration

Was zählt nur in Teilen:

  • Wiederholte und oft gelöste Aufgabenstellungen (sich wiederholende Aufgaben können für Routine sorgen, aber nur wenn Sie bewusst gelöst werden und nicht “automatisch”)

Was zählt gar nicht:

  • Fachfremde Aufgaben (Rechnungsstellung etc.)

Stellen wir die Rechnung also nochmals auf:

220 (grob Arbeitstage pro Jahr abzgl. Urlaub) * 4 (Stunden pro Tag) * 10 Jahre = 8.800 Stunden

Nehmen wir vereinfacht an, Karl verbringt pro Tag 4 Stunden mit echter Praxis für seine Arbeit. An manchen Tagen vielleicht weniger, an manchen Tagen mehr. Im Mittel könnte das stimmen. Das bedeutet, Karl hat bis dato 8.800 Stunden Praxis gesammelt. Im fehlen also noch genau 2.200 Stunden zum Meister.

Da potentiell die Möglichkeit zur Konzentration und Fokussierung abnimmt, je länger man im gleichen Job arbeitet,  wird auch die Praxis, die Karl pro Jahr sammeln kann abnehmen. Das bedeutet, es ist sehr fraglich, ob er jemals den Status eines Meisters erreichen kann (und vielleicht auch will).

Ich stelle die Rechnung mal grob für mich auf. Ich habe im Jahr 2002 angefangen, mich mit Programmiersprachen zu beschäftigen (ungefähr..).

2002 (grob jeden zweiten Tag eine Stunde) – (250 / 2 * 1)
2003 (grob jeden dritten Tag eine Stunde, da ich hier viel gearbeitet habe) – (250 / 3 * 1)
2004 (im Studium jeden Tag ca. eineinhalb Stunden inkl Wochenenden) – (250 * 1.5)
2005 – (im Studium jeden Tag ca. zweieinhalb Stunden inkl Wochenenden)(250 * 2.5)
2006 – (Praxissemester – hier habe ich ein halbes Jahr lang extrem viel gelernt und gearbeitet)
(125 * 6) + (125 * 2.5)
2007 - (Annahme, 3 Stunden Praxis pro Tag + 150 Extrastunden für die Abschlussarbeit) (250 * 3) + 150
2008 – (Studienabschluss und erstes grosses Projekt mit steiler Lernkurve) (125 * 5)  + (125 * 6)
2009 – (extrem viel gearbeitet und gelernt – Annahme 270 Arbeitstage) (270 * 6)
2010 – (einige neue Projekte, viele unterschiedliche Programmiersprachen und Werkzeuge) 270 * 6 + 100
2011 – 280 * 6
2012 – (großes neues Projekt, neues Team, Teamlead und Scrummaster) – 280 * 6
2013 (Selbstständigkeit)- 80 * 10

Die Rechnung ergibt also folgendes:

(250 / 2 * 1) + (250 / 3 * 1) + (250 * 1.5) + (250 * 2.5) + (125 * 6) + (125 * 2.5) + (250 * 3) + 150 + (125 * 5)  + (125 * 6) + (270 * 6) + 270 * 6 + 100 + 280 * 6 + 280 * 6 + 80 * 10 = ~12.000 Stunden Praxis in 10 Jahren.

(Natürlich ist die Rechnung komplett an den Haaren herbeigezogen, aber trotzdem interessant, wenn man mal so grob zusammenrechnet)

Das entspricht in etwa auch den Erfahrungen anderer Menschen, die sich mit dieser Regel befasst haben. Typischerweise braucht man 10 Jahre und ~10.000 Stunden Praxis, um gut in etwas zu werden ( ich behaupte jetzt einfach mal, dass ich in dem was ich mache ganz OK bin).

Es ist extrem interessant, wenn man sich weiter mit dem Thema Deliberate Practice beschäftigt. Hier sind einige Buchempfehlungen zum Thema:

 

 

Malcolm Gladwell – Outliers

 

Robert Greene – Mastery

 

Doug Lemov – Practice Perfect

Weitere Links:

10.000 Stunden Regel

Was ich so mache um auf meine 10.000 Stunden zu kommen

 

 

 

 

Ich bin der Autor des Monats im Java Magazin 04/2013 – vielen Dank!

Das Java Magazin hat mich für die Ausgabe 04/2013 zum Autor des Monats gekürt.Vielen Dank für diese große Ehre.

Martin Dilger - Autor des Monats

Übrigens – wahrscheinlich ist dem Einen oder Anderen aufgefallen, dass in letzter Zeit sehr wenig hier auf dem Blog passiert. Das liegt daran, dass ich mit Hochdruck an dem Buch arbeite, dass in “naher” Zukunft erscheinen wird.

Wicket 6 und JSR-303 – Beanvalidation

Hallo zusammen,

Wicket 6.4.0 ist released und es versteckt sich eine wirkliche Perle hier. Mit WICKET-4883 hat Igor Vaynberg eine Implementierung für BeanValidation (JSR-303) eingefügt. Das bedeutet, Wicket unterstützt BeanValidation nun “Out-of-the-Box”.

Das lasse ich mir natürlich nicht nehmen.

Zunächst erzeugen wir uns wie immer ein Maven-Wicket-Archetype für die Version 6.3.0 (Die Snapshot-Repositories für den 6.4.0-SNAPSHOT Archetype sind leider nicht aktuell).

mvn archetype:generate -DarchetypeGroupId=org.apache.wicket -DarchetypeArtifactId=wicket-archetype-quickstart -DarchetypeVersion=6.4.0 -DgroupId=de.effectivetrainings -DartifactId=jsr303-beanvalidation -DarchetypeRepository=https://repository.apache.org/ -DinteractiveMode=false

Zunächst registrieren wir die BeanValidationConfguration in der init-Methode der Application.

/**
* @see org.apache.wicket.Application#init()
*/
@Override
public void init()
{
super.init();
new BeanValidationConfiguration().configure(this);
// add your configuration here
}

In Wicket-Experimental ist ein neues Modul entstanden. Dieses Modul deklarieren wir ebenfalls als Dependency in der pom.

<groupId>org.apache.wicket<groupId>
<artifactId>wicket-bean-validation</artifactId>
<version>0.5</version>

Zusätzlich brauchen wir eine Implementierung der Bean-Validation-API. Typischerweise Hibernate Validator.

<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>4.3.0.Final</version>
</dependency>

Jetzt fehlt noch ein schön annotiertes Model-Objekt. Wir nehmen hierfür einen….

public class EffectiveTrainer implements Serializable {

@NotNull
private String name;

@Pattern(regexp = "^[_A-Za-z0-9-]+(.[_A-Za-z0-9-]+)*@[A-Za-z0-9-]+(.[A-Za-z0-9-]+)*((.[A-Za-z]{2,}){1}$)")
@NotNull
private String email;

@Pattern(regexp = "[0-9]+")
private String phone;

@Past
@NotNull
private Date birthDay;

public EffectiveTrainer(String name, String email, String phone, Date birthDay) {
this.name = name;
this.email = email;
this.phone = phone;
this.birthDay = birthDay;
}

public String getName() {
return name;
}

public String getEmail() {
return email;
}

public String getPhone() {
return phone;
}

public Date getBirthDay() {
return birthDay;
}
}

Im HomePage-Markup ersetzen wir alles durch folgendes Formular.

<body>
<div wicket:id="feedback"/>
<form wicket:id="form">
<input type="text" wicket:id="name"/> <br/>
<input type="text" wicket:id="email"/> <br/>
<input type="text" wicket:id="birthDay"/> <br/>
<input type="text" wicket:id="phone"/> <br/>
<input type="text" wicket:id="zip"/> <br/>
<input type="text" wicket:id="globalZip"/><br/>
<input type="submit"/>
</form>
</body>

und den passenden Java-Code hierzu:

public HomePage(final PageParameters parameters) {
super(parameters);

IModel effectiveTrainerModel = Model.of(new EffectiveTrainer());
add(new FeedbackPanel("feedback"));
Form form = new Form("form", effectiveTrainerModel);

form.add(new TextField("name", new PropertyModel(effectiveTrainerModel, "name"))
.add(new PropertyValidator()));
form.add(new TextField("email", new PropertyModel(effectiveTrainerModel, "email"))
.add(new PropertyValidator()));
form.add(new TextField("phone", new PropertyModel(effectiveTrainerModel, "phone"))
.add(new PropertyValidator()));
form.add(new TextField("date", new PropertyModel(effectiveTrainerModel, "birthDay"))
.add(new PropertyValidator()));

add(form);

}

Starten wir die Anwendung und schicken das Formular testweise ab ergibt sich folgendes Bild.

Bean Validation

Bean Validation in Action

Jede FormComponent bekommt einen neuen PropertyValidator spendiert. Der PropertyValidator liest die entsprechenden Properties und deren Constraints aus.

Sehr schön, das Prinzip funktioniert. Aber die Fehlermeldungen sind nicht ideal. Wie lässt sich hier etwas machen?

Feedback-Messages

Das erste was einem wahrscheinlich einfällt ist die Fehlermeldungen direkt an den Constraints zu definieren.

@NotNull(message = "Bitte geben Sie Ihren Namen ein.")
private String name;

@Pattern(message = "Die E-Mailadresse ist nicht gültig",
regexp = "^[_A-Za-z0-9-]+(.[_A-Za-z0-9-]+)*@[A-Za-z0-9-]+(.[A-Za-z0-9-]+)*((.[A-Za-z]{2,}){1}$)")
@NotNull(message = "Bitte geben Sie Ihre E-Mailadresse ein")
private String email;

@Pattern(message = "Ihre Telefonnummer sollte aus Ziffern bestehen",regexp = "[0-9]+")
private String phone;

@Past(message = "Sie können nicht in der Zukunft Geburtstag haben")
@NotNull(message = "Bitte geben Sie Ihr Geburtsdatum ein.")
private Date birthDay;

Und das funktioniert auch.

bean-validation - angepasste meldungen

bean-validation – angepasste meldungen

Ideal ist das aber nicht, denn meiner Ansicht nach ist die Wicket-Komponente (also die UI) verantwortlich sein zu definieren, wann in welchem Kontext welche Fehlermeldung angezeigt wird. Das Domain-Objekt “EffectiveTrainer” ist so nicht Kontextübergreifend wiederverwendbar. Das muss besser gehen, oder?

Dynamische FeedbackMessages

Natürlich geht das besser. Wir definieren die Attribute am EffectiveTrainer um.

@NotNull
private String name;

@Pattern(message = "{email.invalid}",
regexp = "^[_A-Za-z0-9-]+(.[_A-Za-z0-9-]+)*@[A-Za-z0-9-]+(.[A-Za-z0-9-]+)*((.[A-Za-z]{2,}){1}$)")
@NotNull
private String email;

@Pattern(message = "{phone.invalid}",regexp = "[0-9]+")
private String phone;

@Past(message = "{birthday.invalid}")
@NotNull
private Date birthDay;

Die JSR-303 Spezifikation definiert, dass Fehlermeldungen mit Platzhaltern befüllt sein dürfen. Platzhalter sind definiert als { value }. Wir verwenden die Platzhalter einfach als Keys in die Property-Datei der jeweiligen Komponente.

Hierfür definieren wir jetzt nur noch diese Property-Datei (HomePage.properties).

email.Required=Bitte geben Sie Ihre E-Mailadresse ein.
email.invalid = Ihre E-Mailadresse ist ungueltig
name.Required=Bitte geben Sie Ihren Namen ein
phone.invalid=Ihre Telefonnummer sollte aus Ziffern bestehen
birthday.invalid=Ihr Geburtsdatum ist ungültig
birthday.Required=Bitte geben Sie Ihr Geburtsdatum ein

Wieso aber email.Required? Wir haben überhaupt keine message für @NotNull definiert? Igor Vaynberg hat uns hier einen Gefallen getan:). Ein Formkomponente, deren zugeordnetes Attribut mit @NotNull annotiert ist wird standardmässig auf Required gesetzt. Das bedeutet, aber hier greifen die Wicket-Standards – also #attributName.Required.

Ergänzen wir das Beispiel noch um eine Postleitzahl. Ich biete Wicket-Trainings deutschlandweit an, aber ein Effective-Trainer kann nur aus München kommen…

Definieren wir also zusätzlich folgende Property.

//hibernate validator specific
@Range(message = "{zip.muenchen}",min = 80805, max=80805)
private Integer zip;

Folgendes Textfeld.

form.add(new TextField("zip", new PropertyModel(effectiveTrainerModel, "zip"))
.add(new PropertyValidator()));

und folgende Text-Property.

zip.muenchen=Ein Effective-Trainer kann nur aus Muenchen-Schwabing kommen

Probleme

Warum hat es wohl so lange gedauert, bis die erste Implementierung direkt im Wicket-Code zur Verfügung steht?

Wicket arbeitet extrem viel mit PropertyModels. PropertyModels lösen Expressions (diese Punkt-separierten Strings) zur Laufzeit auf.

Das bedeutet, zur Laufzeit müssen wir wissen, welche Typen wir haben und wie Constraints auf diesen konfiguriert sind. Das ist etwas komplizierter. Ich habe auch vor ca. 2 Jahren schon eine JSR-303-BeanValidation Implementierung für Wicket gemacht. Lange nicht so schick wie diese hier, aber hat funktioniert.

Versuchen wir doch mal eine Vereinfachung. Lösen wir die ganzen PropertyModels im Formular auf und ersetzen diese durch ein CompoundPropertyModel. Standard so weit.

IModel effectiveTrainerModel = Model.of(new EffectiveTrainer());
add(new FeedbackPanel("feedback"));
Form form = new Form("form", new CompoundPropertyModel(effectiveTrainerModel));

form.add(new TextField("name")
.add(new PropertyValidator()));
form.add(new TextField("email")
.add(new PropertyValidator()));
form.add(new TextField("phone")
.add(new PropertyValidator()));
form.add(new TextField("date")
.add(new PropertyValidator()));
form.add(new TextField("zip")
.add(new PropertyValidator()));

Sieht tatsächlich besser aus. Funktionierts? Leider nein.

java.lang.IllegalStateException: Could not resolve Property from component: [TextField [Component id = name]]. Either specify the Property in the constructor or use a model that works in combination with a IPropertyResolver to resolve the Property automatically

Das Model der Komponente muss vom Typ IPropertyReflectionAwareModel sein, damit BeanValidation funktioniert. Im Fall CompoundPropertyModel haben wir gar keine speziellen Models.
Das Problem ist, wir kommen so leider nicht ganz einfach an das zu validierende Feld und noch schwieriger, die zugehörige Klasse ran.
Normlerweise durchläuft Wicket standardmässig für jede Expression die Getter-Chain:

"customer.address.street" getCustomer().getAddress().getStreet()

Die Typen der einzelnen Hierarchien sind hierbei egal. Nicht jedoch, wenn es um BeanValidation geht, denn ein JSR303-Validator will den Typ wissen.

Wenn wir mit dem CompoundPropertyModel arbeiten möchten, brauchen wir als einen kleinen Workaround.
Wir teilen Wicket bzw. dem Validator einfach selbst mit, um welchen Typ es sich handelt.

 form.add(new TextField("name")
                .add(new PropertyValidator(new Property(EffectiveTrainer.class,"name"))));
        form.add(new TextField("email")
                .add(new PropertyValidator(new Property(EffectiveTrainer.class,"email"))));
        form.add(new TextField("phone")
                .add(new PropertyValidator(new Property(EffectiveTrainer.class,"phone"))));
        form.add(new TextField("birthDay")
                .add(new PropertyValidator(new Property(EffectiveTrainer.class,"birthDay"))));
        form.add(new TextField("zip")
                .add(new PropertyValidator(new Property(EffectiveTrainer.class,"zip"))));

Validation Groups

Über Gruppen kann noch ein wenig genauer definiert werden, wann eine Property wie validiert wird. Ich bin kein großer Fan hiervon, denn wenn man so etwas wie Gruppen braucht, stellt sich die Frage, ob das Domain-Objekt richtig geschnitten ist. Nichtsdestotrotz, wofür könnte man das brauchen?

Eine Group ist technisch nichts anderes als ein Interface. Definieren wir also ein Interface für unseren EffectiveTrainer.

public interface EffectiveTrainer {
String getName();

String getEmail();

String getPhone();
}

Und benennen die Implementierung so um, dass es Sinn macht in MuenchensEffectiveTrainer.

Die Constraints definieren wir jetzt so um:

@Range.List(value = {
@Range(message = "{zip.muenchen}",min = 80805, max=80805, groups = EffectiveTrainer.class),
@Range(message = "{zip.global}",min = 0, max=Integer.MAX_VALUE)
})
private Integer zip;

Wir haben einen Default-Constraint, der quasi immer dann greift, wenn keine Group definiert ist (alle positiven Zahlen ausser 0 sind erlaubt!).

Im Fall von Münchens Trainer brauchts schon eine Postleitzahl in Schwabing.

Der Einfachheit halber definieren wir einfach nochmal ein zweites Textfeld, das auf ebenfalls auf die Zip-Property im Trainer-Objekt geht.

form.add(new TextField("zip")
.add(new PropertyValidator(new Property(MuenchensEffectiveTrainer.class, "zip"), EffectiveTrainer.class)));

IModel globalZipModel = compoundModel.bind("zip");
form.add(new TextField("globalZip", globalZipModel)
.add(new PropertyValidator(new Property(MuenchensEffectiveTrainer.class, "zip"))));

Im ersten Fall definieren wir im PropertyValidator, dass dieser nur die Group “EffectiveTrainer.class” validieren soll. Im zweiten Fall geben wir gar nichts an, das bedeutet, die Default Group greift.

Gruppen können unterschiedlich validiert werden

Gruppen können unterschiedlich validiert werden

Wie gesagt, Gruppen sollten die Ausnahme sein. Man kann eine Property auf unterschiedliche Weisen validieren, aber der Code ist schwerer zu verstehen.

Über dieses und viele weitere Themen spreche ich auch in meinem Wicket-Workshop
Kontaktieren Sie mich zum Thema Wicket, ich freue mich, wenn ich Sie unterstützen kann.

Effective Trainings Wicket Workshop

Links

JSR-303 Spezifikation

Safe-Model (von Carl Eric Menzel)
(geht auch in die richtige Richtung)

Mein Wicket Workshop