Category Archives: jpa

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).

<?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).

<?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.

@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.

@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 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.

public interface CustomerRepository extends JpaRepository<Customer, Long> {
}

Und folgenden Test, der das Speichern und Laden eines Kunden testet.

@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.

<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.

@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.

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.

@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

@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

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.

 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.

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

 @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

 

 

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:

[code lang=”java” inline=”yes”]
@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();
    }

}
[/code]

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.

[code lang=”java” inline=”yes”]

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

[/code]

Um das Ganze zu testen definieren wir folgenden Test-Case

[code lang=”java” inline=”yes”]

@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());
    }

}

[/code]

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

 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.

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&lt;Comment&gt; comments = new ArrayList&lt;Comment&gt;();

Hibernate generiert uns folgendes SQL.

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&lt;Comment&gt; comments = new ArrayList&lt;Comment&gt;();

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

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.

 

Spring Data und Queries – Elegantes Suchen und Filtern mit Spring und JPA

Da ich mich derzeit recht intensiv mit dem Thema Spring und JPA (und damit Gott sei Dank auch mit dem Thema Spring-Data) auseinandersetze, möchte ich hier für mich ein wenig dokumentieren, wie das doch nicht ganz einfach zu verstehende Konzept von Filtern und Suchen funktioniert.

Das Thema Spring-Data wird übrigens auch in meinem (im Entstehen begriffenen) Buch intensiv behandelt.

Das Projekt aufsetzen

Folgende pom deklariert alle notwendigen Dependencies inklusive Spring, Spring-Data, Hibernate und HsqlDB.

[code language=”xml”]
<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/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org</groupId>
<artifactId>rest</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>

<properties>
<spring.version>3.1.0.RELEASE</spring.version>
<hibernate.version>3.5.6-Final</hibernate.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</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-test</artifactId>
<version>${spring.version}</version>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>${spring.version}</version>
</dependency>

<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>1.0.3.RELEASE</version>
</dependency>

<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>${hibernate.version}</version>
</dependency>

<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>${hibernate.version}</version>
</dependency>

<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<version>2.2.6</version>
</dependency>

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.8.2</version>
</dependency>

</dependencies>

<build>
<finalName>rest</finalName>

<plugins>

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.2</version>
<configuration>
<source>1.6</source>
<target>1.6</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>

</plugins>

</build>

</project>

[/code]

Für die Spring-Konfiguration benötigen wir folgende Konfigurationsklasse.
Diese Klasse definiert den notwendige EntityManagerFactoryBean, den TransactionManager und den JpaVendorAdapter.
Leider brauchen wir für die Verwendung von spring-data immer noch eine Xml-Konfigurationsdatei (siehe hier)

[code language=”java”]
@Configuration
@ImportResource(value = "classpath:de/effective/spring-contextt.xml")
@ComponentScan(basePackages = "de.effective")
public class ApplicationConfig {

@Bean
public LocalContainerEntityManagerFactoryBean emf(DataSource dataSource) {
LocalContainerEntityManagerFactoryBean emf = new LocalContainerEntityManagerFactoryBean();
emf.setDataSource(dataSource);
emf.setJpaDialect(new HibernateJpaDialect());
emf.setJpaVendorAdapter(jpaVendorAdapter());
emf.setPersistenceUnitManager(null);
emf.setPackagesToScan("de.effective");
return emf;
}

@Bean
public JpaVendorAdapter jpaVendorAdapter() {
HibernateJpaVendorAdapter jpaVendorAdapter = new HibernateJpaVendorAdapter();
jpaVendorAdapter.setShowSql(true);
jpaVendorAdapter.setDatabase(Database.HSQL);
jpaVendorAdapter.setGenerateDdl(true);
return jpaVendorAdapter;
}

@Bean
public JpaTransactionManager transactionManager(EntityManagerFactory emf){
return new JpaTransactionManager(emf);
}
}
[/code]

Folgende XML-Konfigurationsdatei definiert eine embedded-hsql-db sowie das Bootstrapping für Spring-Data.

[code language=”xml”]
<?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:jpa="http://www.springframework.org/schema/data/jpa"
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/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa-1.0.xsd">
<jdbc:embedded-database id="embeddedDS" type="HSQL"/>
<jpa:repositories base-package="de.effective"/>
</beans>

[/code]

Über die folgende Zeile

[code language=”xml”]
<jpa:repositories base-package="de.effective"/>
[/code]

werden alle Spring-Data Repositories automatisch als Spring-Beans gewired.

Damit wir die Funktionalität von Spring-Data verwenden, definieren wir eine einfache Entity (als Beispiel implementieren
wir den bereits 100-fach implementieren BookStore:).

[code language=”java”]
@Entity
public class Book implements Serializable {

@Id
@Column(name="isbn")
private String isbn;
@Column(name="title")
private String title;
@Column(name="prize")
private double prize;

public Book(){}

public Book(String title, String isbn, double prize) {
this.title = title;
this.isbn = isbn;
this.prize = prize;
}

public String getTitle() {
return title;
}

public String getIsbn() {
return isbn;
}

public double getPrize() {
return prize;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;

Book book = (Book) o;

if (Double.compare(book.prize, prize) != 0) return false;
if (isbn != null ? !isbn.equals(book.isbn) : book.isbn != null) return false;
if (title != null ? !title.equals(book.title) : book.title != null) return false;

return true;
}

@Override
public int hashCode() {
int result;
long temp;
result = title != null ? title.hashCode() : 0;
result = 31 * result + (isbn != null ? isbn.hashCode() : 0);
temp = prize != +0.0d ? Double.doubleToLongBits(prize) : 0L;
result = 31 * result + (int) (temp ^ (temp >>> 32));
return result;
}
}
[/code]

Und natürlich ein passenden Spring-Data-Jpa-Repository:

[code language=”java”]
public interface BookStoreRepository extends JpaRepository {
}
[/code]

Zuletzt definieren wir einen Testdall, der die Spring-Konfiguration testet.

[code language=”java”]
@ContextConfiguration(classes = {ApplicationConfig.class})
public class TestBootsTrap extends AbstractJUnit4SpringContextTests {

@Resource
private BookStoreRepository repository;

@Test
public void testBootstrap(){
assertNotNull(repository);
}
}
[/code]

Damit haben wir ein voll funktionsfähigen Beispiel, um mit Spring-Data loszulegen.

Spring Data Einführung

Das Ziel des Spring-Data Projektes ist es, die Arbeit mit JPA möglichst stark zu vereinfachen und den kompletten Boiler-Plate-Code zu vermeiden.

Für die Definition eines Repositories mit Spring-Data reicht es völlig aus, folgendes Interface zu definieren:

[code language=”java”]
public interface BookStoreRepository extends JpaRepository {
}
[/code]

In der xml-Konfiguration hatten wir folgende Zeile definiert:

[code language=”xml”]
<jpa:repositories base-package="de.effective"/>
[/code]

Alle Interfaces die eines der Spring-Data Interfaces erweitern (beispielsweise hier JpaRepository) werden automatisch
als Spring-Beans bereitgestellt und bieten u.a. bereits folgende Methoden, die automatisch funktionieren.

[code language=”java”]
repository.delete(Book);
repository.delete(String bookId);
repository.findAll();
repository.save(Book);
repository.count();
repository.exists(String bookId);
repository.findOne(String bookId)
[/code]

Damit lassen sich typische CRUD-Applikationen Out-of-the-Box umsetzen.

Testen wir das Ganze in unserem Testfall am beispiel von save / delete:

[code language=”java”]
@Test
public void saveAndDeleteBook(){
assertTrue(repository.findAll().isEmpty());
Book book = new Book("Domain Driven Design","abcdef",39.95);
repository.save(book);
assertFalse(repository.findAll().isEmpty());
assertTrue(repository.exists(book.getIsbn()));
repository.delete(book);
assertTrue(repository.findAll().isEmpty());
}
[/code]

Der Test ist direkt grün und funktioniert:).

Suchen mit Spring-Data und Queries

Oft möchte man auf spezielle Datensätze filtern.
Hierzu gibt es diverse Möglichkeiten, die einfachste (die aber meiner Ansicht nach eher unpraktikabel ist), ist die Definition von folgender Methode im Repository (wenn wir beispielsweise nach ISBN suchen möchten).

[code language=”java”]
public Book findByIsbn(String isbn)
[/code]

Es ist keine eigene Implementierung nötig, Spring Data erkennt bereits aus der Methodensignatur was gemacht werden soll.
Das Ganze testen wir in einem Testfall:

[code language=”java”]
@Test
public void findByIsbn(){
Book book = new Book("Domain Driven Design", "abcdef", 39.95);
repository.save(book);
assertNull(repository.findByIsbn("unknown_isbn"));
assertEquals(book, repository.findByIsbn(book.getIsbn()));
}
[/code]

Auch dieser Testfall ist direkt grün.

Diese Funktionalität hat übrigens zur Laufzeit kaum Auswirkung, da Spring-Data beim Aufbau des Spring-Kontextes NamedQueries aufbaut und somit zur Laufzeit alles schön schnell ist.

Es ist übrigens Vorsicht geboten, denn SPring-Data ist hier ein wenig extrik, denn es reagiert auf Camel-Case. Der folgende Fall zeigt zum Einen auf, dass Spring-Data die ganze Arbeit bereits beim Aufbau des Spring-Kontextes macht (auch das Parsen aller Methoden und Aufbau der Queries) als auch die Case-Sensitivität.

Hierzu benenne ich die Methode im Repository einfach um in findByIsBn(String isbn).

[code language=”java”]

@Test
public void findByIsbn() {
if (true)
throw new RuntimeException("Wird nicht auftreten, da schon in der setup Methode Exceptions fliegen");
Book book = new Book("Domain Driven Design", "abcdef", 39.95);
repository.save(book);
assertNull(repository.findByIsBn("unknown_isbn"));
assertEquals(book, repository.findByIsBn(book.getIsbn()));
}

[/code]

Schon in der 2. Zeile im Test würde eine Exception fliegen. Spring-Data bricht aber bereits früher ab, da bereits beim Aufbauen des Spring-Kontextes die entsprechende Exception fliegt.

[code]

Caused by: java.lang.IllegalArgumentException: No property is found for type class de.effective.Book

[/code]

Da Case-Sensitivität für mich hier etwas seltsam anmutet, bevorzuge ich die manuelle Definition von Queries (das ist natürlich rein subjektiv).

Folgendes Beispiel ist vollkommen äquivalent:

[code language=”java”]

@Query(value = "SELECT p from Book p where p.isbn=:isbn")
public Book manuallyFindByIsbn(@Param(value = "isbn") String isbn);

[/code]

Mit @Query kann man über jede beliebige Methode einen JPQL-Ausdruck setzen, mit @Param kann mit auf Named-Parameters zugreifen. Leider kann Spring-Data das nicht automatisch machen, da dies schon von der JVM nicht unterstützt wird:).

Hier ist der äquivalente Testfall:

[code language=”java”]

@Test
public void manuallyFindByIsbn() {
Book book = new Book("Domain Driven Design", "abcdef", 39.95);
repository.save(book);
assertNull(repository.manuallyFindByIsbn("unknown_isbn"));
assertEquals(book, repository.manuallyFindByIsbn(book.getIsbn()));
}

[/code]

Und wie sollte es anders sein, auch dieser Test ist sofort grün.

Paging

Bisher habe ich es nicht gebraucht, aber das Feature ist nett, deswegen will ich es hier kurz erwähnen. Pagination lässt sich extrem einfach realisieren.

[code language=”java”]

@Test
public void findInPages(){

for(int i = 20; i>0; i–){
repository.save(new Book("A Test Book",""+ i , i));
}
assertEquals(20, repository.findAll().size());

Page page = repository.findAll(new PageRequest(1, 3));
List books = page.getContent();
assertEquals(3, books.size());
}

[/code]

Der Test sollte recht einfach zu lesen sein, wir speichern 20 Bücher, wollen aber davon nur jeweils 3 laden.

Specifications

Der eigentliche Titel dieses Artikels lautat ja nicht Spring-Data (hierzu gibt es ja auch weiß Gott schon genügend Tutorials), sondern elegantes SUchen und Filtern mit Spring-Data.

Was Spring Data bietet ist das Konzept der Specifications.

[code language=”java”]
public interface Specification<T> {
Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb);
}
[/code]

Das Konzept der Specifications finde ich ehrlich gesagt nicht besonders intuitiv (ich persönlich mag aber auch die JPA Criteria API überhaupt nicht).

Wer sich die Criteria-API von JPA2 anschaut wird früher oder später (eher früher) über genau die 3 Typen stossen, die die Specification in ihrer toPredicate-Methode übergeben bekommt.

  • Root -Das Root ist quasi die Wurzel einer Query, analog der Defintion “Book b” in der JPQL Abfrage “select from Book b”, die Identifikationsvariable.
  • CriteriaQuery – Entspricht der Query die aufgebaut wird
  • CriteriaBuilder -Wird verwendet, um eine Query selbst zu erzeugen, als auch die einzelnen Bestandteile der Query (where-Clause)

(Übrigens normalerweise bekommt man den CriteriaBuilder über den EntityManager mit der Methode “getCriteriaBuilder” und die CriteriaQuery über den CriteriaBuilder und die Methode “newQuery”. Es ist nicht immer so komfortabel wie hier in Spring-Data:), die CriteriaQuery wird dann an die createQuery-Methode des EntityManagers übergeben, um diese auszuführen).

Als Beispiel hier mal ein Testcase, der die Verwendung mit der Criteria-API zeigt.

[code language=”java”]

@Resource
private EntityManagerFactory em;

@Test
public void simpleCriteriaAPITest() {

Book ddd = new Book("Domain Driven Design", "1", 39.95);
//neu
Book tdd = new Book("Feature Driven Development", "2", 29.95);
//gebraucht mit anderer isbn:)
Book fdd = new Book("Feature Driven Development","3", 19.95);
repository.save(ddd);
repository.save(tdd);

EntityManager manager = em.createEntityManager();
CriteriaBuilder builder = manager.getCriteriaBuilder();
CriteriaQuery query = builder.createQuery(Book.class);
Root<Book> bookRoot = query.from(Book.class);
Predicate title = builder.equal(bookRoot.get("title"), "Feature Driven Development");

assertEquals(2, manager.createQuery(query).getResultList().size());

Predicate price = builder.greaterThan(bookRoot.get("prize").as(Double.class), 20d);

//override
query.where(title, price);
assertEquals(1, manager.createQuery(query).getResultList().size());

}

[/code]

Kommen wir zurück zum Thema

Specifications und Spring-Data.

Spring-Data kapselt die Verwendung der Criteria API in sogenannte Specifications (das Konzept ist im Buch DDD von Eric Evans erklärt).

Um mit Specifications arbeiten zu können muss unser Interface zusätzlich das Interface SpecificaionExecutor implementieren.

[code language=”java”]

public interface BookStoreRepository extends JpaRepository<Book, String>, JpaSpecificationExecutor<Book> {

public Book findByIsbn(String isbn);

@Query(value = "SELECT p from Book p where p.isbn=:isbn")
public Book manuallyFindByIsbn(@Param(value = "isbn") String isbn);
}

[/code]

Hierdurch erhält unser Repository folgende neuen Methoden, mit denen wir arbeiten können.

[code language=”java”]
T findOne(Specification<T> spec);
List<T> findAll(Specification<T> spec);
Page<T> findAll(Specification<T> spec, Pageable pageable);
List<T> findAll(Specification<T> spec, Sort sort);
long count(Specification<T> spec);

[/code]

Eine Specification kapselt nochmals viel vom ganzen Boilerplate-Code der JPA Criteria-API weg, im Prinzip kann man sich vorstellen, dass die Specifiation eine Prüfung darstellt und für jedes Buch hier im Beispiel entweder TRUE (=gehört in den Ergebnisraum) oder FALSE (=gehört nicht in den Ergebnisraum) liefert.

Das Ergebnis einer Specification sind alle Entities, die TRUE liefern.

Im Folgenden sieht man als Beispiel eine IsbnSpecification, die alle Bücher mit einer bestimmten ISBN filtert.

[code language=”java”]

public class IsbnSpecification implements Specification<Book> {

private String isbn;

public IsbnSpecification(String isbn){
this.isbn = isbn;
}

@Override
public Predicate toPredicate(Root<Book> bookRoot, CriteriaQuery<?> cq, CriteriaBuilder cb) {
return cb.equal(bookRoot.get("isbn"),isbn);
}
}

[/code]

Und hier der Testcase dazu:

[code language=”java”]

@Test
public void findByIsbnWithSpecification() {
Book ddd = new Book("Domain Driven Design", "abcdef", 39.95);
Book tdd = new Book("Test-Driven-Development", "abcdefg", 29.95);
repository.save(ddd);
repository.save(tdd);
IsbnSpecification specification = new IsbnSpecification(ddd.getIsbn());
List<Book> result = repository.findAll(specification);
assertEquals(1, result.size());
assertEquals(ddd, result.get(0));
}

[/code]

Einige Codebeispiele

Anbei folgen noch einige Code-Beispiele, die ich einfach nützlich finde und eine gute Referenz sind (aber gar nicht unbedingt was mit Spring-Data zu tun haben müssen):

Join-Table

Definieren wir eine Kategorie für Bücher.

[code language=”java”]
@Entity
@Table(name="CATEGORY")
public class Category implements Serializable {

@Id
@Column(name="CATEGORY")
private String category;

public Category(String category){
this.category = category;
}

public Category(){}

public String getCategory() {
return category;
}

public void setCategory(String category) {
this.category = category;
}
}
[/code]

Jedes Buch kann einer oder mehreren Kategorien zugeordnet sein.

[code language=”java”]
@OneToMany(fetch = FetchType.EAGER)
@JoinTable(name = "BOOK_CATEGORY", joinColumns = @JoinColumn(name="ISBN"),
inverseJoinColumns = @JoinColumn(name="CATEGORY"))
private List<Category> categories;

public Book(){}

public Book(String title, String isbn, double prize) {
this(title, isbn, prize, new ArrayList<Category>());
}

public Book(String title, String isbn, double prize, List<Category> categories){
this.title = title;
this.isbn = isbn;
this.prize = prize;
this.categories = categories;
}

[/code]
Das passende Mapping hierzu ist @JoinTable, da die Kategorien nichts von der Book-Entity wissen sollen.

Zusätzlich definieren wir uns ein CategoryRepository, um Kategorien speichern und laden zu können.
[code language=”java”]
public interface CategoryRepository extends JpaRepository<Category, String> {
}
[/code]

Der passende Testcase hierzu sieht so aus:
[code language=”java”]
@Test
public void saveWithSeveralCategories(){
Category cat1 = new Category("Krimi");
Category cat2 = new Category(("Sci-Fi"));

categoryRepository.save(cat1);
categoryRepository.save(cat2);

Book book = new Book("Analysis Patterns","abc", 39.95, Arrays.asList(new Category[]{cat1, cat2}));
repository.save(book);

Book persistendBook = repository.findByIsbn("abc");
assertEquals(2, persistendBook.getCategories().size());

}
[/code]

Was ich jetzt machen möchte ist, nach einem Buch mit einer bestimmten Kategorie zu suchen:
Nichts einfacher als das, und zwar im BookStoreRepository:

[code language=”java”]
public List<Book> findByCategories(Category cat);
[/code]

Und hier der passende Testcase dazu:

[code language=”java”]
@Test
public void findByCategory(){
Category cat1 = new Category("Krimi");
Category cat2 = new Category("Sci-Fi");
Category cat3 = new Category("Romanze");

categoryRepository.save(cat1);
categoryRepository.save(cat2);
categoryRepository.save(cat3);

Book book = new Book("Analysis Patterns","abc", 39.95, Arrays.asList(new Category[]{cat1, cat2}));
repository.save(book);

List<Book> krimis = repository.findByCategories(new Category("Krimi"));
assertEquals(1, krimis.size());

List<Book> romanzen = repository.findByCategories(new Category("Romanze"));
assertTrue(romanzen.isEmpty());

}
[/code]

Ok, wie aber würde das mit einer manuell generierten JPQL aussehen?

[code language=”java”]
@Query(value = "select p from Book p JOIN p.categories c where c=:cat ")
public List<Book> manuallyFindByCategory(@Param(value = "cat")Category cat);
[/code]

Und den Test erweitern wir einfach:

[code language=”java”]
@Test
public void findByCategory(){
Category cat1 = new Category("Krimi");
Category cat2 = new Category("Sci-Fi");
Category cat3 = new Category("Romanze");

categoryRepository.save(cat1);
categoryRepository.save(cat2);
categoryRepository.save(cat3);

Book book = new Book("Analysis Patterns","abc", 39.95, Arrays.asList(new Category[]{cat1, cat2}));
repository.save(book);
List<Book> krimis = repository.findByCategories(new Category("Krimi"));
assertEquals(1, krimis.size());

List<Book> manualKrimis = repository.manuallyFindByCategory(new Category("Krimi")) ;
assertEquals(1, manualKrimis.size());

List<Book> romanzen = repository.findByCategories(new Category("Romanze"));
assertTrue(romanzen.isEmpty());

List<Book> manualRomanzen = repository.manuallyFindByCategory(new Category("Romanze"));
assertTrue(manualRomanzen.isEmpty());

}
[/code]

Das generierte SQL ist identisch:
[code language=”sql”]
Hibernate: select book0_.isbn as isbn0_, book0_.prize as prize0_, book0_.title as title0_ from Book book0_ inner join BOOK_CATEGORY categories1_ on book0_.isbn=categories1_.ISBN inner join CATEGORY category2_ on categories1_.CATEGORY=category2_.CATEGORY where category2_.CATEGORY=?
[/code]
und
[code language=”sql”]
Hibernate: select book0_.isbn as isbn0_, book0_.prize as prize0_, book0_.title as title0_ from Book book0_ inner join BOOK_CATEGORY categories1_ on book0_.isbn=categories1_.ISBN inner join CATEGORY category2_ on categories1_.CATEGORY=category2_.CATEGORY where category2_.CATEGORY=?

[/code]

Der Sourcecode ist natürlich auf Github gehostet und zwar hier.

Sobald ich Zeit finde, werde ich dieses kleine Beispiel erweitern.

Anbei noch einige Links, die das Thema ebenfalls behandeln:

Spring-Data Tutorial zum Thema Queries

Referenz-Dokumentation zu Spring-Data

Spring Data API

Specifications in Spring Data

Spring-Data Java Konfiguration

Criteria-API Turorial


War dieser Blogeintrag für Sie interessant? Evtl. kann ich noch mehr für Sie tun.

Trainings & Know-How aus der Praxis zu

  • Apache Wicket 1.4.x, 1.5.x, 1.6.x
  • GIT – Best Practices, Einsatz, Methoden
  • Spring
  • Java
  • Scrum & Kanban
  • Agiles Arbeiten
Consulting & Softwareentwicklung

  • Requirements Engineering
  • Qualitätssicherung
  • Software-Entwicklung
  • Architektur
  • Scrum & Kanban

EclipseLink – Persistence für OSGI Teil 1

Für diesen Artikel hab ich mir mal das EclipseLink Projekt angeschaut, und mich darin ein bisschen eingearbeitet.

Zuerst mal eine schnelle Einführung. Was ist EclipseLink?

EclipseLink ist ein Persistenzframework für Eclipse-Applikationen und kann in verschiedensten Umgebungen arbeiten. Es bietet u.a.

  • Persistenz von Objekten über XML (JAXB)
  • Relationale Datenbanken (JPA)
  • Objektorientierte Datenbanken!
  • usw

Eine wirklich sehr gute Einführung gibts unter Link 2 (s.u)

In diesem ersten Artikel beleuchten wir mal die Installation und entwickeln eine einfache CRUD Anwendung mit Eclipse und EclipseLink.

HIerzu hol ich mir zunächst mal die benötigten Libs von hier. Das sind die OSGI-Bundles, die zum Arbeiten mit EL benötigt werden. DIes sind laut der beigelegten Readme-Datei folgende Bundles:

EclipseLink Core Functionality
——————————
– org.eclipse.persistence.core
– org.eclipse.persistence.asm
– org.eclipse.persistence.antlr
– javax.activation
– javax.jms
– javax.mail
– javax.persistence
– javax.resource
– javax.transaction
– javax.xml.rpc
– javax.xml.soap
– javax.xml.stream
– org.apache.ant

EclipseLink JPA
—————
– org.eclipse.persistence.jpa
– javax.persistence (version 1.99 required for OSGI)

EclipseLink Moxy
—————-
– org.eclipse.persistence.moxy
– javax.xml.bind

EclipseLink SDO
—————
– org.eclipse.persistence.sdo
– commonj.sdo

Schon mal eine ganze Menge. Diese entpacke ich zunächst mal in ein lokales Verzeichnis, bei mir g:developmentlibseclipse_link, und füge diese meiner TargetPlattform hinzu über

Window/Preferences/Plug-In Development / Target-Plattform/Add

target

Gruppiert man jetzt nach Location sollte man die zusätzlich geladenen Bundles erkennen:

groupes

Ok, denke ich mir eine einfache Demoanwendung aus, dann komme ich zwangsläufig wieder mal auf meine Adressverwaltung, die Personenobjekte in einer relationalen Datenbank speichert.

Hierzu sollte ein Formular angeboten werden, welches die Bearbeitung einer Person ermöglicht, das könnte etwa wie folgt aussehen (Über die Implementierung schweige ich mich hier mal aus, da dies nicht Gegenstand des Artikels sein soll).

Die einfache Implementierung unserer Person sieht so aus (man beachte den PropertyChangeSupport, der es uns erlaubt, Databinding zu verwenden).

package de.md.eclipselink.example.domain;

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;

public class Person {

private String vorname;
private String nachname;
private String email;
private String telefon;
private String notiz;

private PropertyChangeSupport support;

public Person(){
this.support = new PropertyChangeSupport(this);
}

public Person(String email, String nachname, String notiz,
String telefon, String vorname) {
this();
this.email = email;
this.nachname = nachname;
this.notiz = notiz;
this.telefon = telefon;
this.vorname = vorname;

}

public String getVorname() {
return vorname;
}

public void setVorname(String vorname) {
String old = this.vorname;
this.vorname = vorname;
support.firePropertyChange(“vorname”, old, vorname);
}

public String getNachname() {
return nachname;
}

public void setNachname(String nachname) {
String old = this.telefon;
this.nachname = nachname;
support.firePropertyChange(“nachname”, old, nachname);
}

public String getEmail() {
return email;
}

public void setEmail(String email) {
String old = this.email;
this.email = email;
support.firePropertyChange(“email”, old, email);
}

public String getTelefon() {
return telefon;
}

public void setTelefon(String telefon) {
String old = this.telefon;
this.telefon = telefon;
support.firePropertyChange(“telefon”, old, telefon);
}

public String getNotiz() {
return notiz;
}

public void setNotiz(String notiz) {
String old = this.notiz;
this.notiz = notiz;
support.firePropertyChange(“notiz”, old, notiz);
}

public void addPropertyChangeListener(PropertyChangeListener listener) {
this.support.addPropertyChangeListener(listener);
}

public void removePropertyChangeListener(PropertyChangeListener listener) {
this.support.removePropertyChangeListener(listener);
}

}
Weiterhin definieren wir einen Editor nebst EditorInput und 3 Commands mit Handlern für BEarbeiten, Laden und Speichern.

Die Laden und Speichern-Commands packen wir direkt zum Editor über eine Menü-Contribution.

<extension
point=”org.eclipse.ui.menus”>
<menuContribution
locationURI=”toolbar:de.md.eclipselink.example.editor”>
<toolbar
id=”de.md.eclipselink.example.editor.toolbar”>
<command
commandId=”de.md.eclipselink.example.save”
icon=”icons/alt_window_16.gif”
label=”speichern”
style=”push”>
</command>
<command
commandId=”de.md.eclipselink.example.load”
icon=”icons/alt_window_16.gif”
label=”laden”
style=”push”>
</command>
</toolbar>
</menuContribution>
</extension>

Die new-Command packen wir in das File-Menü:

<menuContribution
locationURI=”menu:file”>
<command
commandId=”de.md.eclipselink.example.new”
label=”Neu”
style=”push”>
</command>
</menuContribution>
</extension>

Das ganze könnte dann ungefähr so aussehen:

editor
Der Code für das Binding im Editor sieht so aus:
ctx = new DataBindingContext();
Person person = ((PersonEditorInput) getEditorInput()).getPerson();
ctx.bindValue(SWTObservables.observeText(vorname, SWT.Modify), BeansObservables
.observeValue(person, “vorname”), null, null);
ctx.bindValue(SWTObservables.observeText(nachname, SWT.Modify), BeansObservables
.observeValue(person, “nachname”), null, null);
ctx.bindValue(SWTObservables.observeText(telefon, SWT.Modify), BeansObservables
.observeValue(person, “telefon”), null, null);
ctx.bindValue(SWTObservables.observeText(email, SWT.Modify), BeansObservables
.observeValue(person, “email”), null, null);
ctx.bindValue(SWTObservables.observeText(notiz, SWT.Modify), BeansObservables
.observeValue(person, “notiz”), null, null);

Laden wir jetzt mal Probeweise eine Person bei der Instantiirung des Editors im entsprechenden Bearbeiten – Handler:
@Override
public Object execute(ExecutionEvent event) throws ExecutionException {

try {
HandlerUtil
.getActiveWorkbenchWindow(event)
.getActivePage()
.openEditor(
new PersonEditorInput(new Person(“myemail@web.de”,
“mueller”, “eine wichtige NOtiz”,
“089/123455″, “heinrich”)), PersonEditor.ID);
} catch (PartInitException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

return null;
}
sieht das ganze so aus:
editor1
Offenbar funktionierts. Nun müssen wir die Logik für das Laden und Speichern in den entsprechenden Handlern
implementieren. Hierfür implementieren wir ein DAO.
package de.md.eclipselink.example.domain;

public interface IPersonService {

public abstract Person loadPersonByName(String name);

public abstract void savePerson(Person person);

public abstract void deletePerson(Person person);

}

Nun implementieren wir die EclipseLink Unterstützung. Hierfür gibt es hier
eine entsprechende Anleitung, die uns aber primär noch nicht weiterbringt, fangen wir mal mit kleinen
Schritten an.

Zuerst müssen wir eine dependency auf das Plugin javax.persistence definieren, damit
wir Zugriff auf die entsprechenden Annotationen haben.
depend1

Danach können wir unsere Person-Bean annotieren:

Dank Lovely JPA reicht folgendes:
@Entity
public class Person {

@Id
@GeneratedValue(strategy=GenerationType.SEQUENCE)
private Long id;

private String vorname;
private String nachname;
private String email;
private String telefon;
private String notiz;

private PropertyChangeSupport support;

public Person(){
this.support = new PropertyChangeSupport(this);
}

public Person(String email, String nachname, String notiz,
String telefon, String vorname) {
this();
this.email = email;
this.nachname = nachname;
this.notiz = notiz;
this.telefon = telefon;
this.vorname = vorname;

}

public String getVorname() {
return vorname;
}

public void setVorname(String vorname) {
String old = this.vorname;
this.vorname = vorname;
support.firePropertyChange(“vorname”, old, vorname);
}

public String getNachname() {
return nachname;
}

public void setNachname(String nachname) {
String old = this.telefon;
this.nachname = nachname;
support.firePropertyChange(“nachname”, old, nachname);
}

public String getEmail() {
return email;
}

public void setEmail(String email) {
String old = this.email;
this.email = email;
support.firePropertyChange(“email”, old, email);
}

public String getTelefon() {
return telefon;
}

public void setTelefon(String telefon) {
String old = this.telefon;
this.telefon = telefon;
support.firePropertyChange(“telefon”, old, telefon);
}

public String getNotiz() {
return notiz;
}

public void setNotiz(String notiz) {
String old = this.notiz;
this.notiz = notiz;
support.firePropertyChange(“notiz”, old, notiz);
}

public void addPropertyChangeListener(PropertyChangeListener listener) {
this.support.addPropertyChangeListener(listener);
}

public void removePropertyChangeListener(PropertyChangeListener listener) {
this.support.removePropertyChangeListener(listener);
}

Die einfachst mögliche Implementierung des DAO ist:
public class PersonService implements IPersonService {

private EntityManager mgr;
private static PersonService INSTANCE = new PersonService();

private PersonService() {
mgr = Persistence.createEntityManagerFactory(“personUnit”)
.createEntityManager();

}

public static PersonService getInstance(){
return INSTANCE;
}

/*
* (non-Javadoc)
*
* @see
* de.md.eclipselink.example.domain.IPersonService#loadPersonByName(java
* .lang.String)
*/
public Person loadPersonByName(String name) {
return mgr.find(Person.class,name);
}

/*
* (non-Javadoc)
*
* @see
* de.md.eclipselink.example.domain.IPersonService#savePerson(de.md.eclipselink
* .example.domain.Person)
*/
public void savePerson(Person person) {
mgr.merge(person);
}

/*
* (non-Javadoc)
*
* @seede.md.eclipselink.example.domain.IPersonService#deletePerson(de.md.
* eclipselink.example.domain.Person)
*/
public void deletePerson(Person person) {
mgr.remove(person);
}
}

Einfacher gehts nicht. Für den Zugriff bieten wir eine statische “getInstance” Methode an, SIngleton
lässt grüssen, normalerweise würde ich das nciht so machen, und jedem auf die Finger klopfen
der es probiert, aber “for the sake of demonstration, why not”, wie einmal ein kluger Mann gesagt hat.

Hierfür müssen wir noch unsere Persistence.xml definieren, diese könnte so aussehen:
<persistence>
<persistence-unit name=”examplePersistenceUnit”
transaction-type=”RESOURCE_LOCAL”>
<provider>org.eclipse.persistence.jpa.PersistenceProvider<provider/>

</persistence-unit>
</persistence>

Doch halt, das wichtigste fehlt uns ja noch, für Testzwecke brauchen wir eine Datenbank, wir verwenden hierzu
im ersten Schritt eine relationale Datenbank, und am einfachsten die HSQLDB und betreiben diese InMemory.
Hierzu erst mal das entsprechende Jar von hier runterladen und in den Classpath aufnehmen.
Da dies hier nur zu Demozwecken passieren soll, ist es auf jedenfall in ORdnung , alles in ein Plug-In
zu packen.

Um EclipseLink mit HSQL betreiben zu können, erweitern wir die Persistence.xml um folgende Einträge:

<persistence>
<persistence-unit name=”examplePersistenceUnit”
transaction-type=”RESOURCE_LOCAL”>
<provider>org.eclipse.persistence.jpa.PersistenceProvider<provider/>

<properties>
<property name=“eclipselink.target-database” value=“HSQL”/>
<property name=“eclipselink.ddl-generation” value=“drop-and-create-tables”/>
<property name=“eclipselink.ddl-generation.output-mode” value=“database”/>
<property name=“eclipselink.logging.level” value=“FINEST”/>
</properties>
</persistence-unit>
</persistence>

Damit haben können wir die HSQL ansprechen, ausserdem werden unsere Tables jedesmal neu generiert.

Starten ich aber jetzt einfach mal die Anwendung, sehe ich folgendes:
Exception Description: An attempt has been made to use PersistenceUnit [personUnit],
but no bundle is available that defines that persistence unit.

Was ist passiert? Scheinbar ist alles an Ort und Stelle…
1. persistence.xml im Ordner “META-INF”
2. Attribute korrekt gemappt (behaupte ich jetzt mal)

Trotzdem kann scheinbar die Konfiguration nicht geladen werden. Wirft man mal einen Blick in die Klasse
org.eclipse.persistence.jpa.osgi.PersistenceProvider – übrigens die Klasse, die über den
Aufruf Persistence.createEntityManagerFactory… angesprochen wird, dann sieht man das hier
nach einem Parameter der folgenden Art gesucht wird:

PersistenceUnitProperties.CLASSLOADER

Ich setzt mal voraus, das jeder der sich schon ein wenig mit der Materie auseinandergesetzt hat, darüber
Bescheid weiß, das innerhalb OSGi, jedes Bundle seinen eigenen Classloader besitzt.
Scheinbar muss hier der Classloader zum Laden der persistence.xml gesetzt werden.
Versuchen wir das doch einfach mal, da die Datei persistence.xml im einzigen Plug-In unserer Anwendung liegt,
versuchen wir einfach mal, den Classloader der PersonService-Klasse zu setzen (da dieser Classloader
dem BundleClassLoader entspricht).
(Um Zugriff auf die Konstanten zu haben, muss das entspechende Package importiert werden)

Map map = new HashMap();
map.put(PersistenceUnitProperties.CLASSLOADER, Person.class
.getClassLoader());

Gleicher Aufruf:
Caused by: javax.persistence.PersistenceException:
No Persistence provider for EntityManager named personUnit

Verdammt! Scheinbar kann die persistence.xml noch immer nicht geladen werden.
Versuchen wir direkt die Implementierung und ignorieren die API:

org.eclipse.persistence.jpa.osgi.PersistenceProvider p =
new org.eclipse.persistence.jpa.osgi.PersistenceProvider();
Map map = new HashMap();
map.put(PersistenceUnitProperties.CLASSLOADER, Person.class
.getClassLoader());
EntityManagerFactory fac = p.createEntityManagerFactory(“personUnit”,
map);
EntityManager manager = fac.createEntityManager();

Und?[EL Warning]: 2008.11.21 21:57:09.046–ServerSession(1535180)–Thread(Thread[main,6,main])–java.sql.SQLException:
No suitable driver found for jdbc:hsqldb:mem:personDb

Aha!! Scheinbar wurde der EntityManager erzeugt, und es wurde versucht, auf die HSQL-Datenbank zuzugreifen.
Klingt schonmal besser, scheinbar wurde der Treiber nicht geladen.

Ok, soweit so gut, was jetzt ein Problem sein dürfte ist, das der entsprechende JDBC-Treiber nicht geladen wurde, was aber bekannt ist, ist das seit Java6 kein manuelles Laden von JDBC-Treibern mehr nötig ist, sondern dies wird automatisch über den ConnectionManager gehandhabt.
Zur leichteren Nachvollziehbarkeit machen wir jetzt folgendes:
Statt die DB direkt in-Memory zu erzeugen speichern wir die Daten lokal auf der Festplatte. Hierfür ändern wir den ConnectionString folgendermassen ab:
jdbc:hsqldb:file:d:/myperson/person”
Scheinbar hat EclipseLink auch ein Problem, wenn PersitentEntities über Plug-Ins verteilt sind (was ich ziemlich komisch finde, da dies ja genau einer DER Knackpunkte in Bezug auf OSGi ist, und das dies eigentlich ein Grund ist, das EclipseLink überhaupt eine Daseinsberechtigung hat.
Um das zu testen, erstelle ich ein zweites Plug-In, welches nichts anderes bietet als ein package „de.md.eclipselink.domain.ext“ und hierin eine Klasse „Car“, die einer bestimmten Person zugeordnet werden kann. Hierfür bekommt die Person-Klasse ein Attribut „Car”, welches gemappt wird mit „@OneToOne“.
@OneToOne
private Car auto;
Starten wir die Anwendung und versuchen eine Person zu laden bekommen wir wie erwartet:
Caused by: javax.persistence.PersistenceException: Exception [EclipseLink-28018] (Eclipse Persistence Services – 1.0.2 (Build 20081024)): org.eclipse.persistence.exceptions.EntityManagerSetupException

Klar, wir haben Car ja noch nicht als Entity markiert. Das machen wir gleich mal in der persistence.xml
<persistence-unit name=”personUnit” transaction-type=”RESOURCE_LOCAL”>
<provider>org.eclipse.persistence.jpa.PersistenceProvider
</provider>

<class>de.md.eclipselink.example.domain.Person
</class>
<class>de.md.eclipselink.example.domain.ext.Car</class>
<properties>
<property name=”eclipselink.target-database” value=”HSQL” />
<property name=”eclipselink.ddl-generation” value=”drop-and-create-tables” />
<property name=”eclipselink.ddl-generation.output-mode”
value=”database” />
<property name=”eclipselink.logging.level” value=”FINEST” />
<property name=”eclipselink.jdbc.driver” value=”org.hsqldb.jdbcDriver” />
<property name=”eclipselink.jdbc.url” value=”jdbc:hsqldb:file:d:/myperson/person” />
<property name=”eclipselink.jdbc.user” value=”sa” />
<property name=”eclipselink.jdbc.password” value=”” />
</properties>
</persistence-unit>

Starten wir jetzt die Anwendung und speichern eine Person und überprüfen hinterher die Logs der HSQl sieht man folgendes:
INSERT INTO CAR VALUES(2,’rv-md’,’audi’,’pkw’)
D.h. eine Entity wurde automatisch aus einem anderen Plug-In geladen und gespeichert. Dies würde mit beispielsweise Hibernate als PersistenceProvider nicht funktionieren (Hier würde man stattdessen eine Buddy-Policy definieren um den Classpath zu erweitern).
D.h. die Gerüchte, die in den Foren umtreiben scheinen sich nicht zu bewahrheiten und EclipseLink scheint durchaus in der Lage, Entities aus beliebigen Plug-Ins zu persistieren.
Ok, bauen wir mal eine einfach e Laden funktion ein , wir definieren im package „de.md.eclipselink.example.commands“ die Klasse „LoadHandler“ mit folgendem Codefragment:
public class LoadHandler extends AbstractHandler {

@Override
public Object execute(ExecutionEvent event) throws ExecutionException {
String name = event.getParameter(“name”);
Person person = PersonService.getInstance().loadPersonByName(name);
if (person != null) {
try {
HandlerUtil.getActiveWorkbenchWindow(event).getActivePage()
.openEditor(new PersonEditorInput(person),
PersonEditor.ID);
} catch (PartInitException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
return null;
}

}
Hierfür brauchen wir ein Parametrisiertes Command.
Wie das funktioniert erkläre ich mal im schnellgang, denn zum Einen ist es heute bereits sehr spät
zum Anderen will ich darüber irgendwann einen eigenen Blogeintrag machen.

Für das Command zum Laden einer Person definieren wir folgendes:

<command

categoryId=”de.md.eclipselink.example”

id=”de.md.eclipselink.example.load”

name=”load”>

<commandParameter

id=”de.md.eclipselink.example.personName”

name=”personName”

optional=”true”>

<values

class=”de.md.eclipselink.example.commands.LoadCommandParameter”>

</values>

</commandParameter>

</command>

Wir definieren also einen Parameter für unser Command, wie aber rufe ich jetzt einen solchen Parameter auf..
Die Dokumentation hierzu ist denkbar schlecht und ich habe echt lange gebraucht, das Ding
zum LAufen zu kriegen. Im Prinzip funktioniert das ganze innerhalb des Handlers so:

public Object execute(ExecutionEvent event) throws ExecutionException {
String name = event.getParameter(“name”);
Person person = PersonService.getInstance().loadPersonByName(name);

Wie aber kommt der Name in das ExecutionEvent?
IHandlerService service = (IHandlerService) getSite()
.getService(IHandlerService.class);
ICommandService cS = (ICommandService) getSite()
.getService(ICommandService.class);
try {

Command com = cS.getCommand(“de.md.eclipselink.example.load”);
IParameter t = com.getParameter(“de.md.eclipselink.example.personName”);
Parameterization par = new Parameterization(t,”franz mueller”);
ParameterizedCommand pc = new ParameterizedCommand(com,new Parameterization[]{par});
service.executeCommand(pc, null);
} catch (Exception e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}

Links:
http://wiki.eclipse.org/Packaging_and_Deploying_EclipseLink_JPA_Applications_(ELUG)#How_to_Specify_the_Persistence_Unit_Name
http://wiki.eclipse.org/Introduction_to_EclipseLink_Application_Development_(ELUG)
http://wiki.eclipse.org/Introduction_to_Data_Access_(ELUG)#External_Connection_Pools
http://wiki.eclipse.org/Introduction_to_EclipseLink_Transactions_(ELUG)
http://wiki.eclipse.org/Introduction_to_EclipseLink_JPA_%28ELUG%29
http://wiki.eclipse.org/Using_EclipseLink_JPA_Extensions_(ELUG)
http://www.weheartcode.com/2008/08/27/eclipselink-in-j2se-rcp-applications/

http://www.jughh.org/download/attachments/1605843/EclipseLink.pdf?version=1