poniedziałek, 17 marca 2008

JPA - Pierwsze kroki

Przydługawy wstęp

Ponieważ od opublikowania ostatecznej wersji specyfikacji EJB3 upłynęło już sporo czasu, i pojawia się coraz więcej implementacji tej technologii - mniej lub bardziej kompletnych. To prawdopodobnie oznacza to, iż EJB3 będzie coraz częściej wybierana jako technologia bazowa w nowych projektach. Dlatego uznałem, że czas najwyższy zapoznać się z tą technologią nieco bliżej. Chciałbym zacząć od części specyfikacji określającej najniższą warstwę przeciętnej webaplikacji – modelu trwałych danych i styku z bazą danych (czy może raczej z JDBC), czyli Java Persistence API – JPA. W zasadzie JPA nie jest niczym rewolucyjnym. Sięgnięto raczej po sprawdzone koncepcje w najpopularniejszym chyba frameworku ORM – Hibernate. Cieszy mnie to tym bardziej, że przez ostatnie dwa lata z Hibernate'em miałem sporo do czynienia. Co więcej obecnie Hibernate stanowi jedną z implementacji JPA – i ja mam zamiar z niej skorzystać. Niewątpliwym plusem JPA jest to, że w zwarty i w dość prosty sposób standaryzuje podstawowe mechanizmy ORM. Interfejs JPA nie jest zbyt rozbudowany i w zaawansowanej aplikacji może się okazać jednak niewystarczający. W JPA nie znajdziemy wielu zaawansowanych elementów istniejących framework'ów ORM. Nie ma w nim API do dynamicznego tworzenia kryterium wyszukiwania, czy też nie ma możliwości definiowania pola klasy jako wyliczanej formuły SQL. Dlatego dostępne implementacje JPA dostarczają rozszerzenia do podstawowej specyfikacji. Oznacza to jednak, że transparentność implementacji specyfikacji JPA może okazać się tylko iluzoryczna. Z perspektywy użytkownika Hibernate'a poznanie JPA jest i tak nieuniknione jeżeli do mapowania chce stosować anotacji. Niemniej stosowanie konfiguracji w stylu JPA oraz EntityManager'a nie jest konieczne – przynajmniej na razie. Inną zaletą JPA jest możliwość jego użycia nie tylko w kontenerze EJB3, ale również w zwykłej JSE. No ale starczy już tych dywagacji, przejdę więc do opisu konkretnego przykładu użycia JPA.

Dziedzina problemu

Do zrobienia jest model, w którego centrum znajduje się byt Wiadomość - Message, który zawiera w sobie: temat - title, treść - content, oraz powiązania do bytu Użytkownik - User w roli nadawcy i odbiorcy.

Konfiguracja JPA

Specyfikacja JPA określa, że plik konfiguracyjny powinien znajdować się w pliku META-INF/persistence.xml. Listing pliku konfiguracyjnego mojej przykładowej aplikacji:
<persistence version="1.0" 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_1_0.xsd">
 <persistence-unit name="jpapg1">
  <!-- Określenie dostawcy implementującego JPA. -->
  <provider>org.hibernate.ejb.HibernatePersistence</provider>
  <!-- 
  <class></class>
   Tu można podać listę klas mapujących model bazodanowy.
   W kontenerach EJB3 jest to zupełnie zbędne, gdyż specyfikacja JPA
   narzuca na implementację obowiązek automatycznego wyszukiwania klas.
   Ta cecha nie jest natomiast określona w przypadku uruchomienia w
   zwykłym środowisku JSE. Hibernate'a można jednak sparametryzować,
   żeby wyszukiwał te klasy również w JSE.
   -->
  <properties>
   <!-- Parametry konfiguracyjne dostawce JPA - Hibernate -->
   <property name="hibernate.archive.autodetection" value="class" />
   <property name="hibernate.show_sql" value="true" />
   <property name="hibernate.format_sql" value="true" />
   <!-- Parametry dostępu do bazy danych. -->
   <property name="hibernate.connection.driver_class" value="org.hsqldb.jdbcDriver" />
   <property name="hibernate.connection.url" value="jdbc:hsqldb:file:jpapg1db" />
   <property name="hibernate.connection.username" value="sa" />
   <property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect" />
   <!--
    Parametr określający generowanie schematu bazy danych
    na podstawie mapingów.
    -->
   <property name="hibernate.hbm2ddl.auto" value="create" />
  </properties>
 </persistence-unit>
</persistence>

Model danych i mapowanie

Ograniczę się tylko na przedstawieniu listingu kodu źródłowego przykładu, który - mam przynajmniej taką nadzieję - jest wystarczająco okroszony komentarzami.
Użytkownik - User
package pl.dwalczak.jpapg1.model;
import java.util.List;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.OneToMany;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;

@NamedQueries(value={
@NamedQuery(
        name="user.findAll",
        query="select u from User u order by u.nickName asc"
),
@NamedQuery(
  name="user.findByNickName",
  query="select u from User u where u.nickName = :nickName"
)})

// Definicja sekwencji "users_seq" służącej do generowania klucza głównego tabeli "users".
@SequenceGenerator(name="users_seq")

// Mapowanie tabeli "users".
@Entity
@Table(name="users")
public class User {

 private Long id;
 private String nickName;
 private List receivedMessages;
 private List sendMessages;
 
 // dostępność domyślnego konstruktora jest wymagana przez JPA.
 public User() {
 }
 
 public User(String nickName) {
  this.nickName = nickName;
 }

 // Klucz główny tabeli (kolumna "usr_id"),
 // którego wartość jest wyznaczana przez sekwencje "users_seq".
 @Id
 @GeneratedValue(strategy=GenerationType.SEQUENCE, generator="users_seq")
 @Column(name="usr_id")
 public Long getId() {
  return id;
 }

 protected void setId(Long id) {
  this.id = id;
 }

 // Mapowanie kolumny "usr_nickname", która musi posiadać unikalne wartośc
 // i mieć maksymalną długość 24 znaków.
 @Column(name="usr_nickname", unique=true, nullable=false, length=24)
 public String getNickName() {
  return nickName;
 }

 public void setNickName(String name) {
  this.nickName = name;
 }

 // Mapowanie połączenia do tabeli "messages" w roli odbiorcy wiadomości.
 // Określenie kolumny w tabeli "messages" po której odbywa się złączenie ("msg_to").
 // Zaznaczenie, że w przypadku zapisu bytu "User" nie ma nic robić z "Message"
 // skojarzonymi przez te powiązanie.
 @OneToMany
 @JoinColumn(name="msg_to", insertable=false, updatable=false)
 public List getReceivedMessages() {
  return receivedMessages;
 }

 public void setReceivedMessages(List receivedMessages) {
  this.receivedMessages = receivedMessages;
 }

 // Definicja połączenia do tabeli "messages" w roli nadawcy wiadomości.
 @OneToMany
 @JoinColumn(name="msg_from", insertable=false, updatable=false)
 public List getSendMessages() {
  return sendMessages;
 }

 public void setSendMessages(List sendMessages) {
  this.sendMessages = sendMessages;
 }
}
Wiadomość - Message
package pl.dwalczak.jpapg1.model;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;

@NamedQueries(value={
@NamedQuery(
        name="message.findReceived",
        query="select m from Message m where m.to = :user"
),
@NamedQuery(
        name="message.findSend",
        query="select m from Message m where m.from = :user"
)})

// Definicja sekwencji "messages_seq" służącej do generowania klucza głównego tabeli "messages".
@SequenceGenerator(name="messages_seq")

// Mapowanie tabeli "messages".
@Entity
@Table(name="messages")
public class Message {

 private Long id;
 private String title;
 private String content;
 private User from;
 private User to;
 
  // dostępność domyślnego konstruktora jest wymagana przez JPA
 public Message() {
 }
 
 public Message(String title, String content, User from, User to) {
  this.title = title;
  this.content = content;
  this.from = from;
  this.to = to;
 }

 // Klucz główny tabeli (kolumna "msg_id"),
 // którego wartość jest wyznaczana przez sekwencje "messages_seq".
 @Id
 @GeneratedValue(strategy=GenerationType.SEQUENCE, generator="messages_seq")
 @Column(name="msg_id")
 public Long getId() {
  return id;
 }

 protected void setId(Long id) {
  this.id = id;
 }

 // Mapowanie kolumny "msg_title", której wartość może mieć maksymalnie 64 znaków.
 @Column(name="msg_title", length=64)
 public String getTitle() {
  return title;
 }

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

 // Mapowanie kolumny "msg_content", której wartość może mieć maksymalnie 1024 znaków.
 @Column(name="msg_content", length=1024)
 public String getContent() {
  return content;
 }

 public void setContent(String content) {
  this.content = content;
 }

 // Mapowanie do tabeli "users" - nadawcy wiadomości.
 // Powiązanie to jest obowiązkowe - musi być ustawione - i jest realizowane przez
 // klucz obcy do tabeli "users", którego wartość przechowuje kolumna "msg_from".
 // Jest to odpowiednik do powiązania zdefiniowanego na User.sendMessages.
 @ManyToOne(optional=false)
 @JoinColumn(name="msg_from", nullable=false)
 public User getFrom() {
  return from;
 }

 public void setFrom(User from) {
  this.from = from;
 }

 // Mapowanie do tabeli "users" - adresat wiadomości.
 // Jest to odpowiednik do powiązania zdefiniowanego na User.receivedMessages.
 @ManyToOne(optional=false)
 @JoinColumn(name="msg_to", nullable=false)
 public User getTo() {
  return to;
 }

 public void setTo(User to) {
  this.to = to;
 }
}

Entity Manager

Entity Manager jest tym w JPA, czym jest Session w Hibernate. W JSE można go pozyskać w następujący sposób:
javax.persistence.Persistence.createEntityManagerFactory($PERSISTANCE_UNIT_NAME).createEntityManager();
W EJB3 można to zrobić przez wstrzyknięcie (przez kontener) do pola w beanie EJB:
import javax.ejb.Stateless;
import javax.persistence.PersistenceContext;
@Stateless
public class ExampleBean implements Example {
 @PersistenceContext
 EntityManager em;
 ...
}
Entity Manager jest punktem wyjścia do wykonania takich operacji jak: - utrwalenie stanu objektu (metoda persist) - wykonanie zapytań (metoda find oraz grupa metod z rodziny createQuery) - manulnego określania tranzakcji - rozpoczęcie, akceptacji oraz wycofania (metoda getTransaction())

Język zapytań

Język zapytań w JPA to JPA-QL (rozszerzenie EJB-QL) i jest bardzo podobny do Hibernate'owego HQL'a. Myślę, że wykorzystane w tym przykładzie zapytania są na tyle proste, że nie wymagają komentarza.

Zasoby

Pliki źródłowe przykładu
Java Persistence with Hibernate - Sample Chapter 2
JPA - javadoc
Hibernate Annotations
Hibernate EntityManager

1 komentarz:

CamilYedrzejuq pisze...

Jakie polecasz ksiązki do nauki JPA dla laików ?