08 lipca 2007

Java Persistence w Spring Framework wspomaganym przez Eclipse z Spring IDE oraz m2eclipse

We wtorek za 2 tygodnie - 17.07.2007 - prowadzę prezentację o Java Persistence API podczas spotkania grupy Warszawa JUG. Nie mam wiele czasu, ale tyle pomysłów, aby zaprezentować JPA, że zacząłem przygotowywania już teraz, aby powiedzieć wiele na temat i w zaplanowanym czasie.

Jedną z rzeczy, które zamierzam pokazać jest integracja JPA z popularnymi szkieletami programistycznymi jak Spring Framework czy JBoss Seam. 1-2 przykłady, kilka(naście) słów i przechodzę do kolejnego tematu. Wymaga to przygotowania przykładów zawczasu i przećwiczenia ich we wszystkich możliwych wariantach, bo kiedy wystąpi wyjątek dobrze jest mieć rozwiązanie w zapasie. Kiedy przygotowywałem poszczególne sekcje prezentacji i zatrzymałem się na Spring Framework natrafiłem na artykuł, który zaplanowałem przeczytać już jakiś czas temu - Introduction to Spring 2 and JPA. Nie mogłem wyobrazić sobie lepszego momentu na lekturę niż właśnie dzisiaj, teraz.

W tym samym czasie pojawiły się nowe wersje narzędzi Eclipse IDE 3.3 oraz Spring IDE 2.0, a poza tym, właśnie podczas rozpoznawania SCA zatrzymałem się na rozpoznawaniu implementation.spring i konstruowaniu aplikacji z jego wykorzystaniem. Najwyższa pora popróbować się z JPA wspartym przez Spring Framework i Spring IDE i utworzyć aplikację do dalszych eksperymentów technologicznych.

Korzystając z Spring IDE tworzę projekt - File > New > Other... > Spring > Spring Project.

, gdzie podaję wymagane informacje, m.in. nazwa projektu to spring-jpa.

Na bazie wspomnianego artykułu tworzę encję Prezenter (pakiet pl.jaceklaskowski.spring.ranking.entity)

package pl.jaceklaskowski.spring.ranking.entity;

import java.io.Serializable;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Prezenter implements Serializable {
private static final long serialVersionUID = 1L;

@Id
@GeneratedValue(strategy = GenerationType.TABLE)
private long id;

private String imie;
private String nazwisko;

public Prezenter() {
}

public Prezenter(String imie, String nazwisko) {
this.imie = imie;
this.nazwisko = nazwisko;
}

public long getId() {
return id;
}

public String getImie() {
return imie;
}

public void setImie(String imie) {
this.imie = imie;
}

public String getNazwisko() {
return nazwisko;
}

public void setNazwisko(String nazwisko) {
this.nazwisko = nazwisko;
}

}

oraz interfejs usługi zarządzania prezenterami PrezenterService.

package pl.jaceklaskowski.spring.ranking;

import java.util.List;

import pl.jaceklaskowski.spring.ranking.entity.Prezenter;

public interface PrezenterService {
public void save(Prezenter prezenter);

public void delete(Prezenter prezenter);

public List<Prezenter> findAll();

public Prezenter findByPrezenterId(long id);
}

Ideą jest utworzenie aplikacji, która mogłaby zamodelować sytuację, w której encja Prezenter jest w relacji 1-do-wielu z encją Prezentacja, która jest w relacji 1-do-wielu z encją Ocena. Rozbudowanie aplikacji pozostawię na później, a w tej notatce zajmę się jedynie sposobem uruchomienia JPA w Spring Framework i to jak najmniejszym wysiłkiem bazując na lekturze artykułu.

Podczas lektury artykułu natrafiłem na kilka elementów, które są dla mnie niezrozumiałe. Dlaczego modeluje się encję Employee z dwoma identyfikatorami - JPA (identyfikator technologiczny) oraz numer (identyfikator biznesowy). Dlaczego public List<Employee> findByEmployeeNumber(String empno) zwraca listę Employee, zamiast pojedyńczą encję Employee? Dlaczego public Employee save(Employee emp) zwraca Employee? I w końcu, dlaczego klasa encji Employee i Address nie implementują interfejsu java.io.Serializable, co jest zalecane (żeby nie napisać jest dobrą praktyką)? Pytania pozostaną zapewne bez odpowiedzi, ale nie ma to większego znaczenia w realizacji postawionego na dzisiaj celu.

Tworzę implementację PrezenterService - PrezenterDAO, która reprezentuje warstwę dostępu do danych (ang. Data Access Object).

package pl.jaceklaskowski.spring.ranking;

import java.util.List;

import org.springframework.orm.jpa.support.JpaDaoSupport;

import pl.jaceklaskowski.spring.ranking.entity.Prezenter;

public class PrezenterDAO extends JpaDaoSupport implements PrezenterService {

public void delete(Prezenter prezenter) {
getJpaTemplate().remove(prezenter);
}

public List<Prezenter> findAll() {
return getJpaTemplate().find("select p from Prezenter p");
}

public Prezenter findByPrezenterId(long id) {
return getJpaTemplate().find(Prezenter.class, id);

}

public void save(Prezenter prezenter) {
getJpaTemplate().persist(prezenter);
}

}

I w tym momencie, korzystając z klasy JpaDaoSupport wchodzę na tereny Spring Framework. To jest jeden z tych momentów, w którym znacząco dostrzegam zaletę wprowadzenia systemu kontrolującego zależności i pozwalającego mi na deklaratywne ich definiowanie - Apache Maven 2 (M2) czy Apache Ivy. Skoro tworzę aplikację w IDE, oczekuję wsparcia podświetlenia składni i takie tam. Jednakże jego użyteczność kończy się, w przypadku stosowania odwołań do typów, które nie są dla niego widoczne. Chcę skorzystać z typów dostarczanych przez Spring Framework, ale...nie mam go zainstalowanego na moim komputerze, więc nawet, gdybym chciał to jak miałbym zdefiniować zależność w Eclipse? Pozostaje jedynie zainstalować Springa, ale po co miałbym tracić na to czas? Potrzebna mi wyłącznie biblioteka, a nie cała wersja dystrybucyjna projektu, nieprawdaż? Korzystając z m2 wystarczyłoby przecież zadeklarować zależność w pliku konfiguracyjnym projektu - pom.xml - i m2 pobrałaby mi ją. Wyraźnie przyspieszenie mojej produktywności - zrzucić obowiązki na usługę, która się w danej kwestii specjalizuje. Bezcenne.

Nie, nie dam rady tak marudzić. Włączam Maven > Enable Dependency Managemenet na projekcie korzystając z zainstalowanej wtyczki m2eclipse (kompletnie nie wiem, co się zaraz stanie, ale coś mi mówi, że może być tylko lepiej). O! Tego się nie spodziewałem - tworzy się pom.xml dla projektu. Wspaniale!

Wciskam przycisk Next > i co widzę? Możliwość zdefiniowania zależności projektowych. Brawo!

Skrzętnie korzystam z okazji i deklaruję zależność od biblioteki specyfikacji JPA (opieram się o bibliotekę dostarczaną przez projekt Apache Geronimo)


oraz Spring Framework.

Wyszukiwanie zależności przegląda moje lokalne repozytorium, więc na razie zadowolony jestem z posiadania możliwości dodania wersji Spring Framework 2.0.2 chociaż istnieje już wersja 2.0.6. Zaraz się i tym zajmę. Ostatecznie kończę deklarowanie zależności z następującymi bibliotekami.


Wciskam przycisk Finish i zostaje stworzony plik pom.xml, który zaraz modyfikuję o zmianę związaną z aktualną wersją Spring Framework 2.0.6. Okazuje się, że wtyczka wykonuje jakieś skomplikowane czynności, bo Eclipse nie odpowiada. Zmroziła go moja pomysłowość?! ;-)

...po 15-20 sekundach...

Jest! Plik pom.xml jest automatycznie otwarty, wprowadzam zmianę i zapominam o przejściowych problemach. Wracamy do JpaDaoSupport i PrezenterService, i w ogóle Spring Framework i JPA.

Zalet integracji JPA w Spring Framework nie będę przedstawiał - artykuł robi to wyśmienicie. Jednego nie można nie wspomnieć - Spring Framework znacznie upraszcza tworzenie aplikacji, nie tylko korzystającej z JPA.

Pora na plik konfiguracyjny Spring Framework domyślnie nazywany beans.xml, chociaż bardziej znamienne nazwy są zawsze na miejscu. Może spring-jpa-beans.xml jako zlepek nazwy projektu (która sama w sobie nie jest czymś wyrafinowanym) i beans.xml jako dziedzictwo domyślnej nazwy?

Do tworzenia pliku XML skorzystamy z świeżo zainstalowanej wtyczki Spring IDE 2.0. Niech się przekonam, gdzie Spring IDE umiejscowi plik w strukturze projektu zarządzanego przez m2 (powinno być src/main/resources, ale skąd Spring IDE miałby to wiedzieć, że projekt jest kontrolowany przez m2).

Wybieram menu New > Other... > Spring > Spring Bean Definition


Ach, to jest rozwiązanie - panel pytający użytkownika o podanie miejsca położenia pliku. No tak - czego możnaby oczekiwać?! Sprytnie.

Ostatecznie przekopiowujemy zawartość spring.xml z artykułu z odpowiednimi modyfikacji. Ciekawostką wtyczki Spring IDE jest podpowiedź przy uzupełnianiu wartości atrybutów ziaren Spring.

Dostawcą JPA w przykładzie jest TopLink JPA. Możliwi dostawcy JPA w Spring Framework to Hibernate JPA, Apache OpenJPA oraz TopLink JPA (więcej w liście typów w pakiecie org.springframework.orm.jpa.vendor).

Dodaję nowe zależności w projekcie - HSQL oraz TopLink Essentials JPA - korzystając z pomocy mojego artykułu Nauka Java Persistence z Apache Maven 2 i dostawcami JPA: OpenJPA, Hibernate i TopLink, w którym opisałem sposób dodawania zależności od TopLink Essentials.

Tworzę JUnit test, który rozszerza AbstractJpaTests, więc konieczne dodaję kolejną zależność spring-mock-2.0.6.jar do projektu.

package pl.jaceklaskowski.spring.ranking;

import org.springframework.test.jpa.AbstractJpaTests;

import pl.jaceklaskowski.spring.ranking.entity.Prezenter;

public class PrezenterServiceIntegrationTest extends AbstractJpaTests {
private PrezenterService prezenterService;

private long janKowalskiId;

public void setPrezenterService(PrezenterService prezenterService) {
this.prezenterService = prezenterService;
}

protected String[] getConfigLocations() {
return new String[] { "classpath:/spring-jpa-beans.xml" };
}

protected void onSetUpInTransaction() throws Exception {
Prezenter prezenter = new Prezenter("Jan", "Kowalski");
prezenterService.save(prezenter);
janKowalskiId = prezenter.getId();
}

public void testPrezenterExists() {
Prezenter prezenter = prezenterService.findByPrezenterId(janKowalskiId);
assertEquals("Jan", prezenter.getImie());
assertEquals("Kowalski", prezenter.getNazwisko());
}
}

Uruchomienie testu potwierdza poprawne zestawienie środowiska (poza brakiem konfiguracji log4j, ale kto by się tym przejmował teraz).

log4j:WARN No appenders could be found for logger (org.springframework.util.ClassUtils).
log4j:WARN Please initialize the log4j system properly.
[TopLink Config]: 2007.07.08 10:12:51.312--ServerSession(9182681)--Thread(Thread[main,5,main])--The alias name for the entity class [class pl.jaceklaskowski.spring.ranking.entity.Prezenter] is being defaulted to: Prezenter.
[TopLink Config]: 2007.07.08 10:12:51.328--ServerSession(9182681)--Thread(Thread[main,5,main])--The table name for entity [class pl.jaceklaskowski.spring.ranking.entity.Prezenter] is being defaulted to: PREZENTER.
[TopLink Config]: 2007.07.08 10:12:51.343--ServerSession(9182681)--Thread(Thread[main,5,main])--The column name for element [private long pl.jaceklaskowski.spring.ranking.entity.Prezenter.id] is being defaulted to: ID.
[TopLink Config]: 2007.07.08 10:12:51.359--ServerSession(9182681)--Thread(Thread[main,5,main])--The column name for element [private java.lang.String pl.jaceklaskowski.spring.ranking.entity.Prezenter.imie] is being defaulted to: IMIE.
[TopLink Config]: 2007.07.08 10:12:51.359--ServerSession(9182681)--Thread(Thread[main,5,main])--The column name for element [private java.lang.String pl.jaceklaskowski.spring.ranking.entity.Prezenter.nazwisko] is being defaulted to: NAZWISKO.
[TopLink Info]: 2007.07.08 10:12:51.531--ServerSession(9182681)--Thread(Thread[main,5,main])--TopLink, version: Oracle TopLink Essentials - 2.0 (Build 40 (03/30/2007))
[TopLink Config]: 2007.07.08 10:12:51.531--ServerSession(9182681)--Connection(6131844)--Thread(Thread[main,5,main])--connecting(DatabaseLogin(
platform=>HSQLPlatform
user name=> ""
connector=>JNDIConnector datasource name=>null
))
[TopLink Config]: 2007.07.08 10:12:51.781--ServerSession(9182681)--Connection(5555373)--Thread(Thread[main,5,main])--Connected: jdbc:hsqldb:mem:spring-jpa
User: SA
Database: HSQL Database Engine Version: 1.8.0
Driver: HSQL Database Engine Driver Version: 1.8.0
[TopLink Config]: 2007.07.08 10:12:51.781--ServerSession(9182681)--Connection(20738936)--Thread(Thread[main,5,main])--connecting(DatabaseLogin(
platform=>HSQLPlatform
user name=> ""
connector=>JNDIConnector datasource name=>null
))
[TopLink Config]: 2007.07.08 10:12:51.781--ServerSession(9182681)--Connection(29422309)--Thread(Thread[main,5,main])--Connected: jdbc:hsqldb:mem:spring-jpa
User: SA
Database: HSQL Database Engine Version: 1.8.0
Driver: HSQL Database Engine Driver Version: 1.8.0
[TopLink Info]: 2007.07.08 10:12:51.859--ServerSession(9182681)--Thread(Thread[main,5,main])--file:/C:/.eclipse/sandbox/spring-jpa/target/-spring-jpaPU login successful
[TopLink Fine]: 2007.07.08 10:12:51.875--ServerSession(9182681)--Connection(30844270)--Thread(Thread[main,5,main])--CREATE TABLE PREZENTER (ID NUMERIC(19) NOT NULL, IMIE VARCHAR(255), NAZWISKO VARCHAR(255), PRIMARY KEY (ID))
[TopLink Fine]: 2007.07.08 10:12:51.890--ServerSession(9182681)--Connection(26750913)--Thread(Thread[main,5,main])--CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT NUMERIC(38), PRIMARY KEY (SEQ_NAME))
[TopLink Fine]: 2007.07.08 10:12:51.890--ServerSession(9182681)--Connection(6775863)--Thread(Thread[main,5,main])--SELECT * FROM SEQUENCE WHERE SEQ_NAME = 'SEQ_GEN_TABLE'
[TopLink Fine]: 2007.07.08 10:12:51.890--ServerSession(9182681)--Connection(20092482)--Thread(Thread[main,5,main])--INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN_TABLE', 1)
[TopLink Fine]: 2007.07.08 10:12:51.984--ClientSession(31212095)--Connection(181086)--Thread(Thread[main,5,main])--UPDATE SEQUENCE SET SEQ_COUNT = SEQ_COUNT + ? WHERE SEQ_NAME = ?
bind => [50, SEQ_GEN_TABLE]
[TopLink Fine]: 2007.07.08 10:12:51.984--ClientSession(31212095)--Connection(181086)--Thread(Thread[main,5,main])--SELECT SEQ_COUNT FROM SEQUENCE WHERE SEQ_NAME = ?
bind => [SEQ_GEN_TABLE]

Zaprezentowany w artykule plik persistence.xml ukazuje działanie Spring Framework i jego modułu integrującego JPA, którzy symulują działanie serwera aplikacyjnego Java EE, w którym nie definiuje się klas (za pomocą elementu class), które podlegają zarządzaniu przez zarządcę trwałego (kolejny argument, że Spring Framework jest/staje się semi-serwerem aplikacyjnym Java EE).

Na zakończenie pełny plik konfiguracyjny projektu - pom.xml.

<?xml version="1.0" encoding="UTF-8"?>
<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>spring-jpa</groupId>
<artifactId>spring-jpa</artifactId>
<version>0.0.1</version>
<repositories>
<repository>
<id>java.net</id>
<name>java.net Maven Repository</name>
<url>https://maven-repository.dev.java.net/nonav/repository</url>
<layout>legacy</layout>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.apache.geronimo.specs</groupId>
<artifactId>geronimo-jpa_3.0_spec</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jpa</artifactId>
<version>2.0.6</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-mock</artifactId>
<version>2.0.6</version>
</dependency>
<dependency>
<groupId>hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<version>1.8.0.7</version>
</dependency>
<dependency>
<groupId>toplink.essentials</groupId>
<artifactId>toplink-essentials</artifactId>
<version>2.0-40</version>
</dependency>
</dependencies>
</project>

Na dzisiaj wystarczy - szkielet aplikacji ze Spring i JPA stworzony. Cały projekt dostępny jest jako spring-jpa.zip. Wystarczy rozpakować, zaimportować do wybranego IDE (w paczce zawarte są pliki konfiguracyjne dla Eclipse IDE) i uruchomić test jednostkowy PrezenterServiceIntegrationTest. Zakładając istnienie wtyczek i dostępności zależności powinno działać.

Kolejne odsłony tematu planuję rozszerzyć o przedstawienie sposobu uaktywnienia Apache OpenJPA w Spring Framework jako dostawcy JPA oraz wykorzystanie bazy danych PostgreSQL lub Derby (zamiast HSQL). Planuję również przejść ścieżkę integracji aplikacji opartej na Spring i JPA z SCA, a później rozpoznanie integracji Spring z iBatis (tyle się o nim pisze).