JPA i walidacja encji w projekcie GWT

Całkiem niedawno opisywałem moje boje z mechanizmem RequestFactory, który okazał się być całkiem fajnym tworem. Za jego pomocą udało się nam wtedy stworzyć aplikację z serwisami zorientowanymi na dane, gdzie warstwę dostępu do danych zapewniał nam Hibernate. Aplikacja potrafiła wykonać jedynie podstawowe operacje CRUD na pojedynczej encji i już wtedy zapowiedziałem, że w dalszym ciągu czekają na nas relacje między encjami oraz walidacja encji. Dzisiaj będę kontynuować stworzony wtedy projekt i zajmę się drugim z zapowiedzianych tematów, czyli normą JSR 303 definiującą reguły sprawdzania poprawności obiektów. Walidacja nie będzie skomplikowana i ma na celu jedynie pokazanie działania wraz z mechanizmem RequestFactory.

Aby materiału na dzisiaj nie było za mało, pokażę jak skonfigurować projekt, aby skorzystać ze standardu JPA, którego API zastąpi nam Hibernate’a. Ten ostatni będzie dalej obecny pod postacią dostawcy implementacji JPA, jednak jak zauważymy później, kod źródłowy będzie całkowicie od niego niezależny. Przy okazji pokuszę się o małe porównanie JPA z Hibernate’m.

Zacząłem trochę inaczej niż zawsze, gdyż na wstępie zazwyczaj skupiam się na sensowności/celu zastosowania danej technologii, a nie na tym co będziemy robić. Jeśli czujesz się trochę zawiedziony, to już to nadrabiam :) Po co używać więc JPA, jeśli mamy w projekcie doskonale działającego Hibernate’a? Po co uczyć się nowego, nieco innego API? Czy użycie JPA ma jakieś zalety, ewentualnie wady? Odpowiedzi na te pytania mogą być różne i wszystko zależy od tego, jaką aplikację tworzymy i w jakim środowisku będzie ona pracować. Przede wszystkim należy pamiętać, że Hibernate nie jest jedynym narzędziem ORM’owym i istnieje cała masa alternatywnych rozwiązań. Możemy np. trafić do zespołu, który będzie faworyzował Oraclowego TopLink‚a, bądź rozwijanego przez fundację Apache – Open JPA. Możemy też być uzależnieni od środowiska, np. osadzenie aplikacji na serwerze Google App Engine będzie wymagało od nas użycia DataNucleus‚a.

Ktoś w końcu wpadł na pomysł, aby ustandaryzować i ujednolicić API, którym będziemy się posługiwać i tak powstała pierwsza specyfikacja Java Persistence API – JPA 1.0. Używając więc interfejsów dostarczonych przez JPA, pod spodem możemy mieć podstawioną dowolną ich implementację. Szybko jednak okazało się, że możliwości standardu JPA 1.0 są tak małe, że programiści niechętnie myśleli o porzuceniu właściwego API ich ulubionego narzędzia ORM. Dzisiaj sytuacja jest już nieco inna, gdyż do dyspozycji mamy najnowszą wersję standardu JPA oznaczoną numerem 2.0, która spełnia już zdecydowaną większość wymagań stawianych przez deweloperów. Jeśłi poznamy więc JPA, będziemy mogli posługiwać się dowolną implementacją, skupiając się na rozwoju projektu, a nie poznawaniu innych technologii. Może się jednak zdarzyć tak, że JPA będzie nas w pewien sposób ograniczało, bo używając np. Hibernate’a dojdziemy do celu znacznie szybciej. Możemy wtedy np. skorzystać z dodatkowych adnotacji dostarczonych przez Hibernate’a, utracimy jednak wtedy pełną niezależność od wybranego narzędzia. Nie jest to jednak takie złe, gdyż zazwyczaj narzędzie ORM wybieramy dla projektu tylko raz i później nie ma potrzeby jego zmiany, więc w trakcie rozwoju możemy posiłkować się dodatkami przez nie serwowanymi. Nie zmienia to jednak faktu, że główną robotę wykonujemy poprzez interfejsy JPA.

Jeśli nieco rozjaśniłem Ci sensowność użycia JPA, zapraszam do dalszej części. Nie będę powtarzał tu implementacji klas, które od zeszłego razu praktycznie się nie zmieniły, więc jeśli nie zapoznałeś się jeszcze z poprzednim projektem, proszę zrób to teraz. Jeśli już to zrobiłeś, możemy kontynuować.

Konfiguracja

Na początek wyrzucamy z projektu wszystkie pliki związane z Hibernate’m, tj. plik konfiguracyjny hibernate.cfg.xml oraz plik z mapowaniem Car.hbm.xml. Nowym plikiem konfiguracyjnym będzie persistence.xml umieszczony w katalogu /src/main/resources/META-INF. Wewnątrz archiwum war będzie się on znajdował w lokalizacji WEB-INF/classes/META-INF/persistence.xml.

UWAGA: META-INF/persistence.xml nie jest właściwą lokalizacją!

persistence.xml
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
    version="2.0">
  <persistence-unit name="manager" transaction-type="RESOURCE_LOCAL">
    <properties>
      <property name="javax.persistence.jdbc.driver" value="org.hsqldb.jdbcDriver"/>
      <property name="javax.persistence.jdbc.user" value="test"/>
      <property name="javax.persistence.jdbc.password" value="test"/>
      <property name="javax.persistence.jdbc.url" value="jdbc:hsqldb:file:test.db;shutdown=true"/>
      <property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect"/>
      <property name="hibernate.hbm2ddl.auto" value="update"/>
      <property name="hibernate.max_fetch_depth" value="3"/>
    </properties>
  </persistence-unit>
</persistence>

Dodatkowy plik z mapowaniami nie będzie potrzebny, gdyż skorzystamy z adnotacji umieszczonych bezpośrednio w klasie encji.

Implementacja

Zamiast klasy HibernateUtil stworzyłem nową klasę o nazwie JPA, pełniącą tą samą rolę. W przypadku Hibernate’a klasa ta utrzymywała pojedynczą instancję SessionFactory i pozwalała na zwracanie obiektów Session związanych z danym wątkiem. W JPA jest podobnie, gdyż rolę SessionFactory pełni tu EntityManagerFactory, a zamiast obiektów Session otrzymujemy instancje klasy EntityManager. Między Hibernate’m a JPA jest jednak drobna różnica w API. SessionFactory dostarcza dwie metody pozyskania obiektu sesji: getCurrentSession i openSession, gdzie pierwsza zwraca sesję powiązaną z kontekstem (jeśli w kontekście sesja jest już utworzona, zwracana jest ta sama instancja), a druga zawsze tworzy nowy obiekt sesji. W przypadku EntityManagerFactory dostępna jest jedynie metoda createEntityManager, która zawsze zwraca nowy obiekt EntityManager’a. Rozwiązanie to kłóci się trochę z zastosowanym przez nas w projekcie filtrze (TransactionFilter), który rozpoczynał i kończył transakcję dla każdego odwołania do serwisu, bo z poziomu filtru i serwisu nie mielibyśmy dostępu do tej samej sesji zarządzanej przez EntityManager’a.

Dopiszemy jednak niezbędny kod, który będzie tworzył pojedynczego EntityManager’a na wątek i zwracał tą samą instancję, zanim obsługa żądania nie zostanie zakończona.

JPA.java
public class JPA {

  private static JPA instance = new JPA();

  private EntityManagerFactory factory;
  private ThreadLocal<EntityManager> localManager;

  public static JPA get() {
    return instance;
  }

  private JPA() {
    factory = Persistence.createEntityManagerFactory("manager");
    localManager = new ThreadLocal<EntityManager>();
  }

  public static synchronized EntityManager em() {
    EntityManager em = get().localManager.get();
    if (em == null) {
      em = get().factory.createEntityManager();
      get().localManager.set(em);
    }
    return em;
  }

  public static synchronized void close() {
    EntityManager em = get().localManager.get();
    if (em != null) {
      em.close();
      get().localManager.set(null);
    }
  }
}

Skorzystałem tu ze zmiennej typu ThreadLocal zapewniającej dostęp do przechowywanej wartości jedynie w ramach tego samego wątku.

Kluczowe zmiany zaszły w klasie CarDao, która teraz obsługuje dostęp do danych poprzez JPA.

CarDao.java
public class CarDao {

  public List<Car> list() {
    CriteriaQuery<Car> crit = JPA.em().getCriteriaBuilder().createQuery(Car.class);
    crit.select(crit.from(Car.class));
    return JPA.em().createQuery(crit).getResultList();
  }

  public Car create(Car car) {
    JPA.em().persist(car);
    return car;
  }

  public void update(Car car) {
    JPA.em().merge(car);
  }

  public void remove(Car car) {
    JPA.em().merge(car);
    JPA.em().remove(car);
  }
}

Na pierwszy rzut oka znacząco różni się implementacja metody list(). Różnice wynikają z nieco odmiennego mechanizmu zapytań poprzez kryteria, który w przypadku JPA wymaga powołania większej liczby obiektów. Metodę list() można zrealizować w o wiele krótszy sposób:

  public List list() {
    return JPA.em().createQuery("select c from Car c").getResultList();
  }

Ja jednak nie przepadam za pisaniem zapytań pseudo-sql’owych, więc zostajemy przy rozwiązaniu z użyciem kryteriów.

Różnicę między JPA i API Hibernate’a widać tez w metodzie remove(Car). JPA nie zezwoli na usunięcie obiektu, który nie jest związany z aktualnym kontekstem, więc jeśli został on przesłany z innego źródła (w tym przypadku obiekt Car otrzymujemy jako argument serwisu), należy złączyć jego stan z obiektem bazodanowym. JPA nie posiada też metody usuwania obiektu przyjmującej jako atrybut jego identyfikator, trzeba więc zawsze najpierw pobrać dany obiekt, a dopiero później go usunąć.

Ostatnie zmiany w części serwerowej odbyły się w klasie Car, która została opatrzona teraz w szereg adnotacji odwzorowujących mapowanie oraz reguły walidacji. Do klasy Car przeniesiona została również funkcja VersionInterceptor’a, który podnosił numerek wersji przy każdej zmianie w danym obiekcie.

Car.java
@Entity
public class Car implements DatabaseEntity<Long> {

  @Id
  @GeneratedValue
  private Long id;

  private Integer version;

  @NotNull
  @Size(min = 1, max = 30)
  private String brand;

  @NotNull
  @Size(min = 1, max = 30)
  private String model;

  @NotNull
  @Size(min = 1, max = 30)
  private String color;

  public Car() {
  }

  @PrePersist
  protected void init() {
    version = 0;
  }

  @PreUpdate
  protected void updateVersion() {
    version++;
  }

  // getters and setters
}

Klasa została opatrzona adnotacją @Entity, która zapewnia nam możliwość składowania obiektów Car w bazie danych. Identyfikator otrzymał adnotacje @Id oraz @GeneratedValue, dzięki czemu będzie pełnił on rolę klucza podstawowego, a jego wartość dla nowych obiektów będzie generowana domyślnym algorytmem. Wszystkie trzy atrybuty, które podlegają modyfikacji przez użytkownika (brand, model i color) posiadają adnotacje @NotNull oraz @Size zapewniające kontrolę wprowadzanych danych.
Na uwagę zasługują metody init() oraz updateVersion() opatrzone kolejno adnotacjami @PrePersist oraz @PreUpdate. Adnotacja @PrePersist powoduje, że metoda zostanie wykonana tuż przed pierwszym zapisem obiektu do bazy, a @PreUpdate będzie wywoływało metodę przed każdym wywołaniem instrukcji UPDATE.

Obsługa komunikatów błędów walidacji w mechanizmie RequestFactory jest trywialna i wymaga jedynie zaimplementowania metody onViolation(Set) dla odpowiednich Receiver’ów pełniących rolę wywołań zwrotnych.

new Receiver<Void>() {

  @Override
  public void onFailure(ServerFailure error) {
    LOG.log(Level.SEVERE, error.getMessage());
    super.onFailure(error);
  }

  @Override
  public void onViolation(Set<Violation> errors) {
    violationDialog.show(errors);
  }

  @Override
  public void onSuccess(Void response) {
  }
}

Obiekty klasy Violation zawierają nazwę pola, które nie przeszło sprawdzania poprawności oraz komunikat o błędzie, który można wyświetlić użytkownikowi. Poniżej wynik działania aplikacji:

Sample GWT RequestFactory - Validation error

Pełny kod źródłowy projektu możesz pobrać z repozytorium lub tej lokalizacji.

Możesz spodziewać się również kontynuacji projektu, gdyż zgodnie z zapowiedzią pozostała jeszcze obsługa relacji między encjami w mechanizmie RequestFactory. Dodatkowo w głowie mam jeszcze jeden mały smaczek, który z pewnością Cię zaciekawi. Na dzisiaj to tyle, dzięki za lekturę!

Skomentuj

6 Komentarze.

  1. Wielkie dzięki za kolejny genialny artykuł :smile: , liczę że w kolejnej części pokażesz jak utrwalić relację wiele-do-wielu

    • W dalszej kontynuacji tematu o RequestFactory przedstawię realizację mapowań między encjami. Myślę, że znajdzie się też miejsce dla relacji wiele-do-wielu w JPA, więc jest na co czekać :)

  2. Hej,

    Czy do automatycznego inkrementowania numeru wersji nie lepiej wykorzystać adnotację @Version?

  3. Kolejny raz dzięki za art. Czy planujesz też publikować wpisy o składowaniu danych do GAE np z wykorzystaniem Objectify?

    • Myślę, że też się do tego dobiorę, jednak wcześniej w planach mam jeszcze kilka innych artykułów. Nie mniej postaram się zapoznać dokładniej z tematem Objectify i coś skrobnąć o nim w najbliższym czasie :)

Skomentuj


UWAGA - Możesz używać HTML tags and attributes:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

CommentLuv badge