Java

JPA w aplikacji SE

Spis treści

     1 Wstęp
     2 Podstawy
     3 Program
     4 Tworzenie projektu
     5 Implementacja
     6 Podsumowanie


Wstęp


W powszechnym mniemaniu JPA - Java Persistance API jest częścią technologi Java EE i nie może być używana w aplikacjach SE. To podejście jest błędne ponieważ, wraz z powstaniem specyfikacji EJB 3.0 oddzielono do osobnej specyfikacji całą część poświęconą trwałości i ORM. Nowa specyfikacja nosi nazwę JPA 1.0 i może być używana zarówno w aplikacjach EE, SE jak i web. Poniższy tekst prezentuje jak połączyć aplikację SE z narzędziami JPA za pomocą Apache Maven. Nie poruszam jednak szczegółów związanych z JPA, Maven'em ani z Hibernate'm. Jeżeli nie czujesz się na siłach by zagłębić się w któryś z tych tematów radzę rozpocząć od lektury tutoriali umieszczonych na stronach lub w serwisie 4programmers.

Podstawy


JPA do prawidłowego działania wymaga dwóch elementów. Pierwszym z nich jest RDBMS w naszym przypadku będzie to HSQLDB. Wybór tego rozwiązanie jest zasadny jeżeli chcemy, aby baza danych działała tylko lokalnie na komputerze klienta, nie była duża (do 1GB, powyżej warto zainteresować się innymi rozwiązaniami) oraz nie wymagała od klienta instalowania dodatkowego oprogramowania w postaci serwera bazy danych.
Drugim elementem wymaganym przez JPA jest biblioteka implementująca tą specyfikację. W tym przypadku będzie to Hibernate. Wybór ten jest podyktowany tym, że jest to obecnie najpopularniejsze rozwiązanie ORM dla Javy. Dodatkowo należy tu zauważyć, że twórcy specyfikacji JPA bardzo dużo czerpali właśnie z Hibernate.

Uwaga!
Wszystkie adnotacje wykorzystywane w artykule pochodzą z pakietu javax.persistance. Zwróć uwagę, że adnotacje o takich samych nazwach są też dostępne w pakiecie org.hibernate.annotations. Pomylenie tych dwóch typów adnotacji może spowodować poważne problemy.

Program


Przykładowa aplikacja będzie operować na dwóch obiektach.
package pl.koziolekweb.programmers.jpajse;
 
import javax.persistence.CascadeType;
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.OneToOne;
 
@Entity
@NamedQueries(value = { @NamedQuery(name = "getUserByAge", query = "SELECT user FROM User as user WHERE user.age.value = :ageValue") })
public class User {
 
        @Id
        @GeneratedValue(strategy=GenerationType.AUTO)
        private Long id;
 
        private String name;
 
        @OneToOne(cascade = CascadeType.ALL)
        @JoinColumn(name = "AGE_ID")
        private Age age;
 
        public static User find(long id) {
                return Main.getEntityManager().find(User.class, new Long(id));
        }
 
        public static void delete(long id) {
                Main.getEntityManager().remove(find(id));
        }
 
        public static void merge(User user) {
                Main.getEntityManager().merge(user);
        }
 
        public long getId() {
                return id;
        }
 
        public void setId(long id) {
                this.id = id;
        }
 
        public String getName() {
                return name;
        }
 
        public void setName(String name) {
                this.name = name;
        }
 
        public Age getAge() {
                return age;
        }
 
        public void setAge(Age age) {
                this.age = age;
        }
 
        @Override
        public String toString() {
                return "User [age=" + age + ", id=" + id + ", name=" + name + "]";
        }
}

package pl.koziolekweb.programmers.jpajse;
 
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
 
@Entity
public class Age {
 
        @Id
        @GeneratedValue(strategy=GenerationType.AUTO)
        private Long id;
 
        private Short value;
 
        public static Age find(long id) {
                return Main.getEntityManager().find(Age.class, new Long(id));
        }
 
        public static void delete(long id) {
                Main.getEntityManager().remove(find(id));
        }
 
        public static void merge(Age age) {
                Main.getEntityManager().merge(age);
        }
 
        public long getId() {
                return id;
        }
 
        public Age setId(long id) {
                this.id = id;
                return this;
        }
 
        public Short getValue() {
                return value;
        }
 
        public Age setValue(Short value) {
                this.value = value;
                return this;
        }
 
        @Override
        public String toString() {
                return "Age [id=" + id + ", value=" + value + "]";
        }
}


Program utworzy obiekty klasy User i Age, zwiąże je ze sobą po czym zapisze je w bazie danych. Następnie odczyta obiekt klasy User za pomocą zapytania getUserByAge.
Nie będzie to program interaktywny, to znaczy użytkownik nie będzie miał wpływu na jego działanie, nie będzie wprowadzał żadnych danych. Ma to na celu uniknięcie zbędnego kodu, który by tylko rozpraszał nas i odciągał od meritum.

Tworzenie projektu


Utwórz projekt za pomocą Apache Maven (od wersji 2.0.10 możesz użyć mvn archetype:generate). Nadaj mu odpowiednie kwalifikatory. Następnie utwórz katalog src/main/resources/META-INF i umieść w nim plik persistance.xml taki jak poniżej:
<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_1_0.xsd"
        version="1.0">
        <persistence-unit name="JPAJSE" transaction-type="RESOURCE_LOCAL">
                <class>pl.koziolekweb.programmers.jpajse.User</class>
                <class>pl.koziolekweb.programmers.jpajse.Age</class>
                <properties>
                        <property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect" />
                        <property name="hibernate.connection.driver_class" value="org.hsqldb.jdbcDriver" />
                        <property name="hibernate.connection.username" value="sa" />
                        <property name="hibernate.connection.password" value="" />
                        <property name="hibernate.connection.url" value="jdbc:hsqldb:file:./target/db/jpajse" />
                        <property name="hibernate.hbm2ddl.auto" value="create" />
                        <property name="hibernate.show_sql" value="false" />
                        <property name="hibernate.format_sql" value="false" />
                </properties>
        </persistence-unit>
</persistence>

Kolejnym krokiem jest umieszczenie pliku log4j.xml w katalogu src/main/resources/:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
        <!--
                This is a sample log4j configuration file. It should be written to a
                file called "log4j.xml" and must appear in the classpath.
        -->
 
<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">
 
        <!-- Output messages to the console -->
        <appender name="console" class="org.apache.log4j.ConsoleAppender">
                <layout class="org.apache.log4j.PatternLayout">
                        <param name="ConversionPattern" value="%d{yyyyMMdd HH:mm:ss} %-5p %C{1}() - %m%n" />
                </layout>
        </appender>
 
        <!--
                Set the level appropriately: levels are: debug, info, warn, error,
                fatal
        -->
        <logger name="org.hibernate">
                <level value="warn" />
                <appender-ref ref="console" />
        </logger>
        <logger name="org.hibernate.tool.hbm2ddl">
                <level value="error" />
                <appender-ref ref="console" />
        </logger>
 
        <root>
                <priority value="info" />
                <appender-ref ref="console" />
        </root>
 
</log4j:configuration>

Powyższa konfiguracja minimalizuje zawartość wyświetlanego logu. Możesz oczywiście wyświetlić log w celu analizy działania programy. Poniżej konfiguracja wyświetlająca wszystko:
<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_1_0.xsd"
        version="1.0">
        <persistence-unit name="JPAJSE" transaction-type="RESOURCE_LOCAL">
                <class>pl.koziolekweb.programmers.jpajse.User</class>
                <class>pl.koziolekweb.programmers.jpajse.Age</class>
                <properties>
                        <property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect" />
                        <property name="hibernate.connection.driver_class" value="org.hsqldb.jdbcDriver" />
                        <property name="hibernate.connection.username" value="sa" />
                        <property name="hibernate.connection.password" value="" />
                        <property name="hibernate.connection.url" value="jdbc:hsqldb:file:./target/db/jpajse" />
                        <property name="hibernate.hbm2ddl.auto" value="create" />
                        <property name="hibernate.show_sql" value="true" />
                        <property name="hibernate.format_sql" value="true" />
                </properties>
        </persistence-unit>
</persistence>

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
        <!--
                This is a sample log4j configuration file. It should be written to a
                file called "log4j.xml" and must appear in the classpath.
        -->
 
<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">
 
        <!-- Output messages to the console -->
        <appender name="console" class="org.apache.log4j.ConsoleAppender">
                <layout class="org.apache.log4j.PatternLayout">
                        <param name="ConversionPattern" value="%d{yyyyMMdd HH:mm:ss} %-5p %C{1}() - %m%n" />
                </layout>
        </appender>
 
        <!--
                Set the level appropriately: levels are: debug, info, warn, error,
                fatal
        -->
        <logger name="org.hibernate">
                <level value="info" />
                <appender-ref ref="console" />
        </logger>
        <logger name="org.hibernate.tool.hbm2ddl">
                <level value="debug" />
                <appender-ref ref="console" />
        </logger>
 
        <root>
                <priority value="info" />
                <appender-ref ref="console" />
        </root>
 
</log4j:configuration>

Zauważ też, że dla naszych potrzeb baza danych jest tworzona od nowa za każdym razem za pomocą wpisu <property name="hibernate.hbm2ddl.auto" value="create" />. W produkcyjnej aplikacji warto zmienić ten wpis na validate, ale oznacza to, że musimy z programem dystrybuować też pustą bazę danych.
Na koniec należy jeszcze wyedytować plik pom.xml dodając zależności i konfigurując odpowiednie wtyczki:
<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>pl.koziolekweb.programmers.jpajse</groupId>
        <artifactId>JPAJSE</artifactId>
        <version>1.0-SNAPSHOT</version>
        <packaging>jar</packaging>
        <name>JPA Project</name>
        <build>
                <plugins>
                        <plugin>
                                <artifactId>maven-compiler-plugin</artifactId>
                                <configuration>
                                        <source>1.5</source>
                                        <target>1.5</target>
                                </configuration>
                        </plugin>
                        <plugin>
                                <groupId>org.apache.maven.plugins</groupId>
                                <artifactId>maven-jar-plugin</artifactId>
                                <version>2.2</version>
                                <configuration>
                                        <archive>
                                                <manifest>
                                                        <mainClass>pl.koziolekweb.programmers.jpajse.Main</mainClass>
                                                        <addClasspath>true</addClasspath>
                                                        <classpathPrefix>dependency/</classpathPrefix>
                                                </manifest>
                                        </archive>
                                </configuration>
                        </plugin>
                </plugins>
        </build>
        <dependencies>        
        <!-- Persistance -->
                <dependency>
                        <groupId>javax.persistence</groupId>
                        <artifactId>persistence-api</artifactId>
                        <version>1.0</version>
                </dependency>
                <dependency>
                        <groupId>org.hibernate</groupId>
                        <artifactId>hibernate</artifactId>
                        <version>3.2.6.ga</version>
                </dependency>
                <dependency>
                        <groupId>org.hibernate</groupId>
                        <artifactId>hibernate-annotations</artifactId>
                        <version>3.4.0.GA</version>
                </dependency>
                <dependency>
                        <groupId>org.hibernate</groupId>
                        <artifactId>hibernate-commons-annotations</artifactId>
                        <version>3.3.0.ga</version>
                </dependency>
                <dependency>
                        <groupId>org.hibernate</groupId>
                        <artifactId>hibernate-entitymanager</artifactId>
                        <version>3.4.0.GA</version>
                </dependency>
                <dependency>
                        <groupId>org.hibernate</groupId>
                        <artifactId>ejb3-persistence</artifactId>
                        <version>3.3.2.Beta1</version>
                </dependency>
                <dependency>
                        <groupId>org.hibernate</groupId>
                        <artifactId>hibernate-tools</artifactId>
                        <version>3.2.3.GA</version>
                </dependency>
                <dependency>
                        <groupId>hsqldb</groupId>
                        <artifactId>hsqldb</artifactId>
                        <version>1.8.0.10</version>
                </dependency>
                <dependency>
                        <groupId>org.slf4j</groupId>
                        <artifactId>slf4j-api</artifactId>
                        <version>1.5.6</version>
                </dependency>
                <dependency>
                        <groupId>org.slf4j</groupId>
                        <artifactId>slf4j-log4j12</artifactId>
                        <version>1.5.6</version>
                </dependency>
                <dependency>
                        <groupId>log4j</groupId>
                        <artifactId>log4j</artifactId>
                        <version>1.2.13</version>
                </dependency>
                <!--  test -->
                <dependency>
                        <groupId>junit</groupId>
                        <artifactId>junit</artifactId>
                        <version>4.5</version>
                        <scope>test</scope>
                </dependency>
                <dependency>
                        <groupId>org.dbunit</groupId>
                        <artifactId>dbunit</artifactId>
                        <version>2.2</version>
                        <scope>test</scope>
                </dependency>
        </dependencies>
</project>


Implementacja


Przejdźmy teraz do implementacji naszego programu. Jest ona bardzo prosta, a cały program wygląda następująco:

package pl.koziolekweb.programmers.jpajse;
 
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
 
import org.apache.log4j.Logger;
 
public class Main {
 
        private static EntityManager entityManager;
 
        public static void main(String[] args) {
                init();
 
                Age age = new Age();
                age.setValue(new Short((short) 10));
 
                User user = new User();
                user.setName("User");
                user.setAge(age);
 
                Logger.getRootLogger().info(user);
 
                getEntityManager().getTransaction().begin();
                getEntityManager().persist(user);
                getEntityManager().flush();
                getEntityManager().getTransaction().commit();
 
                User user2 = (User) getEntityManager().createNamedQuery("getUserByAge").setParameter("ageValue",
                                new Short((short) 10)).getSingleResult();
                Logger.getRootLogger().info(user);
                Logger.getRootLogger().info(user2);
                getEntityManager().close();
        }
 
        /**
         * 
         */
        private static void init() {
                EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("JPAJSE");
                entityManager = entityManagerFactory.createEntityManager();
        }
 
        public static EntityManager getEntityManager() {
                return entityManager;
        }
 
}


Po spakowaniu i uruchomieniu programu za pomocą mavena możemy podziwiać rezultaty naszych zmagań.

Podsumowanie


JPA można bardzo łatwo połączyć z aplikacjami SE. Dzięki użyciu lekkich baz danych takich jak HSQLDB użytkownik oprogramowania ma do dyspozycji podstawowe możliwości jakie udostępnia RDBMS, a jednoczesnie nie musi instalować dodatkowego oprogramowania.