Archiv der Kategorie: jpa

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.

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

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)

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

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

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

Über die folgende Zeile

<jpa:repositories base-package="de.effective"/>

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

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

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

public interface BookStoreRepository extends JpaRepository {
}

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

@ContextConfiguration(classes = {ApplicationConfig.class})
public class TestBootsTrap extends AbstractJUnit4SpringContextTests {

@Resource
private BookStoreRepository repository;

@Test
public void testBootstrap(){
assertNotNull(repository);
}
}

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:

public interface BookStoreRepository extends JpaRepository {
}

In der xml-Konfiguration hatten wir folgende Zeile definiert:

<jpa:repositories base-package="de.effective"/>

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.

repository.delete(Book);
repository.delete(String bookId);
repository.findAll();
repository.save(Book);
repository.count();
repository.exists(String bookId);
repository.findOne(String bookId)

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

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

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

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

public Book findByIsbn(String isbn)

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:

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

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


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

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.


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

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:


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

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:


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

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.


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

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.

public interface Specification<T> {
	Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb);
}

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.


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

}

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.


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

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

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

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.


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

Und hier der Testcase dazu:


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

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.

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

Jedes Buch kann einer oder mehreren Kategorien zugeordnet sein.

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

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.

public interface CategoryRepository extends JpaRepository<Category, String> {
}

Der passende Testcase hierzu sieht so aus:

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

    }

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

public List<Book> findByCategories(Category cat);

Und hier der passende Testcase dazu:

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

    }

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

  @Query(value = "select p from Book p JOIN p.categories c where c=:cat ")
    public List<Book> manuallyFindByCategory(@Param(value = "cat")Category cat);

Und den Test erweitern wir einfach:

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

    }

Das generierte SQL ist identisch:

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

und

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

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
Advertisements

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