Skip to content

5. Wprowadzenie do Spring Data JPA

W tym rozdziale omówimy następującą architekturę:

Pomiędzy warstwą [DAO] a sterownikiem JDBC w SGBD wstawiamy warstwę [JPA] (Java Persistence API). Obecnie to warstwa JPA wysyła polecenia SQL przeznaczone dla SGBD. Warstwa [DAO] nie przetwarza już poleceń SQL, a jedynie obiekty zwane encjami JPA, które są obrazami różnych tabel eksploatowanej bazy danych. Pola tych encji są jednoznacznie powiązane z kolumnami tabel za pomocą adnotacji Java. To właśnie pozwala warstwie JPA na przekształcenie w SQL operacji warstwy [DAO] wykonywanych na encjach JPA.

Spring Data to gałąź biblioteki Spring zajmująca się dostępem do danych, niezależnie od tego, czy są one przechowywane w relacyjnej bazie danych SGBDR, bazie NOSQL czy w innych rodzajach repozytoriów. W tym miejscu skupiamy się wyłącznie na SGBDR i dostępie do nich za pośrednictwem JPA. W dalszej części tekstu będziemy czasami używać skrótu [Spring JPA], aby w rzeczywistości odnieść się do [Spring Data JPA]. W powyższej architekturze warstwa [Spring Data] zapewnia warstwie [DAO] narzędzia do zarządzania encjami JPA.

JPA jest w rzeczywistości specyfikacją. Przetestujemy trzy jej implementacje:

5.1. Exemple-01

Na stronie Springa znajduje się wiele samouczków dotyczących rozpoczęcia pracy ze Springiem [http://spring.io/guides]. Wykorzystamy jeden z nich, aby zapoznać się z Spring Data. W tym celu użyjemy Spring Tool Suite (STS).

  • w [1] importujemy jeden z samouczków z [spring.io/guides];
  • w [2] wybieramy samouczek [Accessing Data Jpa], który pokazuje, jak uzyskać dostęp do bazy danych za pomocą Spring Data;
  • w [3] wybieramy projekt skonfigurowany przez Maven;
  • w [4] samouczek może być udostępniony w dwóch formach: [initial], czyli pusta wersja, którą wypełnia się zgodnie z samouczkiem, lub [complete], czyli ostateczna wersja samouczka. Wybieramy tę drugą;
  • w pliku [5] można wybrać opcję wyświetlenia samouczka w przeglądarce;
  • w pliku [6] znajduje się projekt końcowy.

5.1.1. Konfiguracja projektu w Maven

Zależności Maven projektu są skonfigurowane w pliku [pom.xml]:


    <groupId>org.springframework</groupId>
    <artifactId>gs-accessing-data-jpa</artifactId>
    <version>0.1.0</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
        </dependency>
    </dependencies>

    <properties>
        <!-- użyj UTF-8 do wszystkiego -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <start-class>hello.Application</start-class>
</properties>
  • wiersze 5–9: definiują nadrzędny projekt Maven. To właśnie on określa większość zależności projektu. Mogą one być wystarczające, w którym to przypadku nie dodaje się żadnych nowych, lub nie, w którym to przypadku dodaje się brakujące zależności;
  • wiersze 12–15: definiują zależność od [spring-boot-starter-data-jpa]. Ten artefakt zawiera klasy Spring Data;
  • wiersze 16–19: definiują zależność od artefaktu SGBD oraz H2, który umożliwia tworzenie i zarządzanie bazami danych w pamięci.

Przyjrzyjmy się klasom dostarczanym przez te zależności:

Jest ich bardzo wiele:

  • niektóre należą do ekosystemu Spring (te zaczynające się od „spring”);
  • inne należą do ekosystemu Hibernate (hibernate, jboss), z którego implementacją JPA korzystamy tutaj;
  • inne to biblioteki testowe (junit, hamcrest);
  • inne to biblioteki logowania (log4j, logback, slf4j);

Zachowamy je wszystkie. W przypadku aplikacji produkcyjnej należałoby zachować tylko te, które są niezbędne.

W wierszu 26 pliku [pom.xml] znajduje się następujący wiersz:


<start-class>hello.Application</start-class>

Ten wiersz jest powiązany z następującymi wierszami:


<build>
        <plugins>
            <plugin> 
                <artifactId>maven-compiler-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

W wierszach 6–9 wtyczka [spring-boot-maven-plugin] umożliwia wygenerowanie pliku jar zawierającego kod wykonywalny aplikacji. Wiersz 26 pliku [pom.xml] określa zatem klasę wykonywalną tego pliku jar.

5.1.2. Warstwa [JPA]

Dostęp do bazy danych odbywa się za pośrednictwem warstwy [JPA], Java Persistence API:

  

Aplikacja jest prosta i obsługuje klientów [Customer]. Klasa [Customer] należy do warstwy [JPA] i ma następującą postać:


package hello;

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

@Entity
public class Customer {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;
    private String firstName;
    private String lastName;

    protected Customer() {
    }

    public Customer(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    @Override
    public String toString() {
        return String.format("Customer[id=%d, firstName='%s', lastName='%s']", id, firstName, lastName);
    }

}

Klient ma identyfikator [id], imię [firstName] i nazwisko [lastName]. Każda instancja [Customer] reprezentuje wiersz w tabeli bazy danych.

  • wiersz 8: adnotacja JPA, która powoduje, że trwałość instancji [Customer] (tworzenie, odczyt, aktualizacja, usunięcie) będzie zarządzana przez implementację JPA. Z zależności Maven wynika, że wykorzystywana jest implementacja JPA / Hibernate;
  • wiersze 11–12: adnotacje JPA, które powiązują pole [id] z kluczem głównym tabeli [Customer]. Wiersz 12 wskazuje, że implementacja JPA będzie korzystać z metody generowania klucza głównego właściwej dla używanego SGBD, w tym przypadku H2;

Nie ma innych adnotacji dotyczących JPA. W takim przypadku zostaną zastosowane wartości domyślne:

  • tabela o nazwie [Customer] będzie nosiła nazwę klasy, tj. [Customer];
  • kolumny tej tabeli będą nosiły nazwy pól klasy: [id, firstName, lastName], przy czym w nazwie kolumny tabeli nie jest brana pod uwagę wielkość liter;

Należy zauważyć, że w żadnym momencie nie podano nazwy używanej implementacji JPA.

5.1.3. Warstwa [Spring Data]

Klasa [CustomerRepository] implementuje warstwę dostępu do tabeli [Customer]. Jej kod wygląda następująco:

  

package hello;

import java.util.List;

import org.springframework.data.repository.CrudRepository;

public interface CustomerRepository extends CrudRepository<Customer, Long> {

    List<Customer> findByLastName(String lastName);
}

Jest to zatem interfejs, a nie klasa (wiersz 7). Rozszerza on interfejs [CrudRepository], interfejs Spring Data (wiersz 5). Interfejs ten jest definiowany przez dwa typy: pierwszy to typ zarządzanych elementów, w tym przypadku typ [Customer], a drugi to typ klucza głównego zarządzanych elementów, w tym przypadku typ [Long]. Interfejs [CrudRepository] wygląda następująco:


package org.springframework.data.repository;

import java.io.Serializable;

@NoRepositoryBean
public interface CrudRepository<T, ID extends Serializable> extends Repository<T, ID> {

    <S extends T> S save(S entity);

    <S extends T> Iterable<S> save(Iterable<S> entities);

    T findOne(ID id);

    boolean exists(ID id);

    Iterable<T> findAll();

    Iterable<T> findAll(Iterable<ID> ids);

    long count();

    void delete(ID id);

    void delete(T entity);

    void delete(Iterable<? extends T> entities);

    void deleteAll();
}

Interfejs ten definiuje operacje CRUD (Create – Read – Update – Delete), które można wykonać na typie JPA T:

  • wiersz 8: metoda save pozwala na zapisanie encji T w bazie danych. Zapisuje ona encję przy użyciu klucza głównego nadanego jej przez SGBD. Pozwala również na aktualizację encji T identyfikowanej za pomocą jej klucza głównego id. Wybór jednej lub drugiej operacji zależy od wartości klucza głównego id: jeśli ma on wartość null, wykonywana jest operacja zapisania, w przeciwnym razie – operacja aktualizacji;
  • wiersz 10: to samo, ale dla listy encji;
  • wiersz 12: metoda findOne pozwala odzyskać encję T identyfikowaną przez jej klucz pierwotny id;
  • wiersz 22: metoda delete pozwala usunąć encję T identyfikowaną przez jej klucz główny id;
  • wiersze 24–28: warianty metody [delete];
  • wiersz 16: metoda [findAll] pozwala odzyskać wszystkie zapisane encje T;
  • wiersz 18: to samo, ale ograniczone do encji, dla których przekazano listę identyfikatorów;

Wróćmy do interfejsu [CustomerRepository]:


package hello;

import java.util.List;

import org.springframework.data.repository.CrudRepository;

public interface CustomerRepository extends CrudRepository<Customer, Long> {

    List<Customer> findByLastName(String lastName);
}
  • w wierszu 9 można odnaleźć obiekt [Customer] na podstawie jego nazwy [lastName];

I to wszystko, jeśli chodzi o warstwę [DAO]. Nie ma klasy implementującej poprzedni interfejs. Jest ona generowana w czasie wykonywania przez [Spring Data]. Metody interfejsu [CrudRepository] są implementowane automatycznie. W przypadku metod dodanych do interfejsu [CustomerRepository] sytuacja jest różna. Wróćmy do definicji [Customer]:


    private long id;
    private String firstName;
private String lastName;

Metoda w wierszu 9 jest automatycznie implementowana przez [Spring Data], ponieważ odwołuje się do pola [lastName] (wiersz 3) z [Customer]. Gdy Spring Data napotka metodę [findBySomething] w interfejsie do zaimplementowania, implementuje ją za pomocą następującego zapytania JPQL (Java Persistence Query Language):

select t from T t where t.something=:value

Typ T musi zatem posiadać pole o nazwie [something]. W ten sposób metoda

List<Customer> findByLastName(String lastName);

zostanie zaimplementowana za pomocą kodu podobnego do poniższego:

return [em].createQuery("select c from Customer c where c.lastName=:value").setParameter("value",lastName).getResultList()

gdzie [em] oznacza kontekst trwałości JPA. Jest to możliwe tylko wtedy, gdy klasa [Customer] posiada pole o nazwie [lastName], co ma miejsce.

Podsumowując, w prostych przypadkach Spring Data pozwala nam zaimplementować warstwę [DAO] za pomocą prostego interfejsu.

5.1.4. Warstwa [console]

  

Klasa [Application] wygląda następująco:


package hello;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application implements CommandLineRunner {

    @Autowired
    CustomerRepository repository;
    
    public static void main(String[] args) {
        SpringApplication.run(Application.class);
    }

    @Override
    public void run(String... strings) throws Exception {
        // zapisz kilku klientów
        repository.save(new Customer("Jack", "Bauer"));
        repository.save(new Customer("Chloe", "O'Brian"));
        repository.save(new Customer("Kim", "Bauer"));
        repository.save(new Customer("David", "Palmer"));
        repository.save(new Customer("Michelle", "Dessler"));

        // pobierz wszystkich klientów
        System.out.println("Customers found with findAll():");
        System.out.println("-------------------------------");
        for (Customer customer : repository.findAll()) {
            System.out.println(customer);
        }
        System.out.println();

        // pobierz pojedynczego klienta za pomocą ID
        Customer customer = repository.findOne(1L);
        System.out.println("Customer found with findOne(1L):");
        System.out.println("--------------------------------");
        System.out.println(customer);
        System.out.println();

        // pobieranie klientów według nazwiska
        System.out.println("Customer found with findByLastName('Bauer'):");
        System.out.println("--------------------------------------------");
        for (Customer bauer : repository.findByLastName("Bauer")) {
            System.out.println(bauer);
        }
    }

}
  • wiersz 9: klasa implementuje interfejs [CommandLineRunner], który jest interfejsem [Spring Boot] (wiersz 4). Interfejs ten posiada tylko jedną metodę, znajdującą się w wierszu 19;
  • wiersz 8: @SpringBootApplication to adnotacja łącząca kilka adnotacji [Spring Boot]:
    • @Configuration: wskazuje, że klasa jest klasą konfiguracyjną;
    • @EnableAutoConfiguration: nakazuje [Spring Boot] samodzielne utworzenie określonej liczby beanów w oparciu o różne właściwości, w szczególności zawartość ścieżki Classpath projektu. Ponieważ biblioteki Hibernate znajdują się w ścieżce Classpath, bean [entityManagerFactory] zostanie zaimplementowany przy użyciu Hibernate. Ponieważ biblioteka SGBD H2 znajduje się w ścieżce Classpath, bean [dataSource] zostanie zaimplementowany przy użyciu H2. W beanie [dataSource] należy również zdefiniować użytkownika i jego hasło. W tym przypadku Spring Boot użyje domyślnego administratora z H2, który nie ma hasła. Ponieważ biblioteka [spring-tx] znajduje się w ścieżce Classpath, zostanie użyty menedżer transakcji Springa;
    • @EnableWebMvc: jeśli w ścieżce Classpath znajduje się biblioteka [spring-mvc]. W takim przypadku następuje automatyczna konfiguracja aplikacji internetowej;
    • @ComponentScan: informuje Spring, gdzie szukać pozostałych beanów, konfiguracji i usług. Domyślnie są one wyszukiwane w pakiecie zawierającym klasę oznaczoną adnotacją, tj. w pakiecie [hello]. W ten sposób zostaną znalezione klasy [Customer] i [CustomerRepository]. Ponieważ pierwsza z nich posiada adnotację [@Entity], zostanie skatalogowana jako encja do zarządzania przez Hibernate. Ponieważ druga rozszerza interfejs [CrudRepository], zostanie zarejestrowana jako bean Spring;
  • wiersze 11–12: bean [CustomerRepository] jest wstrzykiwany do kodu klasy głównej;
  • wiersz 15: wykonywana jest metoda statyczna [run] klasy [SpringApplication] z projektu Spring Boot. Jej parametrem jest klasa posiadająca adnotację [Configuration] lub [EnableAutoConfiguration]. Następnie odbiegnie wszystko, co zostało wyjaśnione powyżej. Wynikiem jest kontekst aplikacji Spring, tj. zbiór beanów zarządzanych przez Spring;
  • wiersze 19–48: kolejne operacje polegają jedynie na wykorzystaniu metod fasoli implementującej interfejs [CustomerRepository];

Wyniki wyświetlane w konsoli są następujące:

.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.2.2.RELEASE)

2015-03-10 15:35:43.661  INFO 5784 --- [           main] hello.Application                        : Starting Application on Gportpers3 with PID 5784 (started by ST in C:\Users\Serge Tahé\Documents\workspace-sts-3.6.3.RELEASE\gs-accessing-data-jpa-complete)
2015-03-10 15:35:43.708  INFO 5784 --- [           main] s.c.a.AnnotationConfigApplicationContext : Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@5d11346a: startup date [Tue Mar 10 15:35:43 CET 2015]; root of context hierarchy
2015-03-10 15:35:45.230  INFO 5784 --- [           main] j.LocalContainerEntityManagerFactoryBean : Building JPA container EntityManagerFactory for persistence unit 'default'
2015-03-10 15:35:45.254  INFO 5784 --- [           main] o.hibernate.jpa.internal.util.LogHelper  : HHH000204: Processing PersistenceUnitInfo [
    name: default
    ...]
2015-03-10 15:35:45.331  INFO 5784 --- [           main] org.hibernate.Version                    : HHH000412: Hibernate Core {4.3.8.Final}
2015-03-10 15:35:45.332  INFO 5784 --- [           main] org.hibernate.cfg.Environment            : HHH000206: hibernate.properties not found
2015-03-10 15:35:45.334  INFO 5784 --- [           main] org.hibernate.cfg.Environment            : HHH000021: Bytecode provider name : javassist
2015-03-10 15:35:45.651  INFO 5784 --- [           main] o.hibernate.annotations.common.Version   : HCANN000001: Hibernate Commons Annotations {4.0.5.Final}
2015-03-10 15:35:45.754  INFO 5784 --- [           main] org.hibernate.dialect.Dialect            : HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
2015-03-10 15:35:45.877  INFO 5784 --- [           main] o.h.h.i.ast.ASTQueryTranslatorFactory    : HHH000397: Using ASTQueryTranslatorFactory
2015-03-10 15:35:46.154  INFO 5784 --- [           main] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000227: Running hbm2ddl schema export
2015-03-10 15:35:46.169  INFO 5784 --- [           main] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000230: Schema export complete
2015-03-10 15:35:46.779  INFO 5784 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
Customers found with findAll():
-------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']
Customer[id=2, firstName='Chloe', lastName='O'Brian']
Customer[id=3, firstName='Kim', lastName='Bauer']
Customer[id=4, firstName='David', lastName='Palmer']
Customer[id=5, firstName='Michelle', lastName='Dessler']

Customer found with findOne(1L):
--------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']

Customer found with findByLastName('Bauer'):
--------------------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']
Customer[id=3, firstName='Kim', lastName='Bauer']
2015-03-10 15:35:47.040  INFO 5784 --- [           main] hello.Application                        : Started Application in 3.623 seconds (JVM running for 4.324)
2015-03-10 15:35:47.042  INFO 5784 --- [       Thread-1] s.c.a.AnnotationConfigApplicationContext : Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@5d11346a: startup date [Tue Mar 10 15:35:43 CET 2015]; root of context hierarchy
2015-03-10 15:35:47.044  INFO 5784 --- [       Thread-1] o.s.j.e.a.AnnotationMBeanExporter        : Unregistering JMX-exposed beans on shutdown
2015-03-10 15:35:47.046  INFO 5784 --- [       Thread-1] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2015-03-10 15:35:47.047  INFO 5784 --- [       Thread-1] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000227: Running hbm2ddl schema export
2015-03-10 15:35:47.051  INFO 5784 --- [       Thread-1] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000230: Schema export complete
  • wiersze 1–8: logo projektu Spring Boot;
  • wiersz 9: uruchamiana jest klasa [hello.Application];
  • wiersz 10: [AnnotationConfigApplicationContext] to klasa implementująca interfejs [ApplicationContext] biblioteki Spring. Jest to kontener beanów;
  • wiersz 11: bean [entityManagerFactory] jest zaimplementowany za pomocą klasy [LocalContainerEntityManagerFactory], klasy biblioteki Spring. Zarządza on warstwą [JPA];
  • wiersz 12: pojawia się [Hibernate]. Wybrano właśnie tę implementację JPA;
  • wiersz 19: dialektem Hibernate jest wariant SQL, który ma być używany wraz z SGBD. W tym przypadku dialekt [H2Dialect] wskazuje, że Hibernate będzie współpracować z SGBD i H2;
  • wiersze 21–22: utworzono bazę danych. Utworzono tabelę [CUSTOMER]. Oznacza to, że Hibernate został skonfigurowany do generowania tabel na podstawie definicji JPA, w tym przypadku definicji JPA klasy [Customer];
  • wiersze 26–30: wynik metody [findAll] z interfejsu;
  • wiersz 34: wynik metody [findOne] interfejsu;
  • wiersze 38–39: wyniki metody [findByLastName];
  • wiersze 41 i kolejne: logi zamknięcia kontekstu Spring.

5.1.5. Ręczna konfiguracja projektu Spring Data

Duplikujemy poprzedni projekt w projekcie [gs-accessing-data-jpa-02]:

  

W tym nowym projekcie nie będziemy polegać na automatycznej konfiguracji dokonywanej przez Spring Boot. Zrobimy to ręcznie. Może to być przydatne, jeśli domyślne ustawienia nam nie odpowiadają.

Najpierw określimy niezbędne zależności w pliku [pom.xml]:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.springframework</groupId>
    <artifactId>gs-accessing-data-jpa-02</artifactId>
    <version>0.1.0</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
    </parent>

    <dependencies>
        <!-- Spring Data -->
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-jpa</artifactId>
        </dependency>
        <!-- Hibernate -->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
        </dependency>
        <!-- Baza danych H2 -->
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
        </dependency>
        <!-- Tomcat JDBC -->
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jdbc</artifactId>
        </dependency>
    </dependencies>

    <properties>
        <!-- używaj UTF-8 do wszystkiego -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

    <repositories>
        <repository>
            <id>spring-releases</id>
            <name>Spring Releases</name>
            <url>https://repo.spring.io/libs-release</url>
        </repository>
        <repository>
            <id>org.jboss.repository.releases</id>
            <name>JBoss Maven Release Repository</name>
            <url>https://repository.jboss.org/nexus/content/repositories/releases</url>
        </repository>
    </repositories>

    <pluginRepositories>
        <pluginRepository>
            <id>spring-releases</id>
            <name>Spring Releases</name>
            <url>https://repo.spring.io/libs-release</url>
        </pluginRepository>
    </pluginRepositories>

</project>
  • wiersze 10–14: nadrzędny projekt Maven, z którego będziemy korzystać z zdefiniowanych w nim bibliotek;
  • wiersze 18–21: Spring Data używany do dostępu do bazy danych;
  • wiersze 23–26: implementacja specyfikacji JPA w Hibernate;
  • wiersze 28–31: SGBD H2;
  • wiersze 33–36: bazy danych są często używane z pulami otwartych połączeń, co pozwala uniknąć wielokrotnego otwierania i zamykania połączeń. W tym przypadku zastosowano implementację [tomcat-jdbc];

W nowym projekcie encja [Customer] i interfejs [CustomerRepository] pozostają bez zmian. Zmienimy klasę [Application], która zostanie podzielona na dwie klasy:

  • [Config], która będzie klasą konfiguracyjną:
  • [Main], która będzie klasą wykonalną;
  

Klasa wykonywalna [Application] ma teraz następujący wygląd:


package console;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import repositories.CustomerRepository;
import config.AppConfig;
import entities.Customer;

public class Application {
    public static void main(String[] args) {
        // instancjonowanie kontekstu Spring
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        CustomerRepository repository = context.getBean(CustomerRepository.class);

        // zapisanie kilku klientów
        repository.save(new Customer("Jack", "Bauer"));
        repository.save(new Customer("Chloe", "O'Brian"));
        repository.save(new Customer("Kim", "Bauer"));
        repository.save(new Customer("David", "Palmer"));
        repository.save(new Customer("Michelle", "Dessler"));

        ...

        // zamknięcie kontekstu
        context.close();
    }

}
  • wiersz 9: klasa [Application] nie zawiera już adnotacji konfiguracyjnych;
  • wiersze 3–7: należy zauważyć, że nie ma już importów pakietów [Spring Boot];
  • wiersz 12: tworzone są instancje beanów Spring. Uzyskuje się kontekst Spring zawierający odniesienia do tak utworzonych beanów;
  • wiersz 13: żądamy odwołania do fasoli typu [CustomerRepository];

Klasa [Config] , która konfiguruje projekt, wygląda następująco:


package config;

import javax.persistence.EntityManagerFactory;

import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@EnableTransactionManagement
@EnableJpaRepositories(basePackages = { "repositories" })
@Configuration
// @ComponentScan(basePackages={"package1","package2"})
public class AppConfig {

    // baza danych H2
    @Bean
    public DataSource dataSource() {
        // źródło danych TomcatJdbc
        DataSource dataSource = new DataSource();
        // konfiguracja dostępu JDBC
        dataSource.setDriverClassName("org.h2.Driver");
        dataSource.setUrl("jdbc:h2:./demo");
        dataSource.setUsername("sa");
        dataSource.setPassword("");
        // początkowo otwarte połączenie
        dataSource.setInitialSize(1);
        // wynik
        return dataSource;
    }

    // dostawca JPA
    @Bean
    public JpaVendorAdapter jpaVendorAdapter() {
        HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
        hibernateJpaVendorAdapter.setShowSql(false);
        hibernateJpaVendorAdapter.setGenerateDdl(true);
        hibernateJpaVendorAdapter.setDatabase(Database.H2);
        return hibernateJpaVendorAdapter;
    }

    // EntityManagerFactory
    @Bean
    public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
        factory.setJpaVendorAdapter(jpaVendorAdapter);
        factory.setPackagesToScan("entities");
        factory.setDataSource(dataSource);
        factory.afterPropertiesSet();
        return factory.getObject();
    }

    // Menedżer transakcji
    @Bean
    public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager txManager = new JpaTransactionManager();
        txManager.setEntityManagerFactory(entityManagerFactory);
        return txManager;
    }

}
  • wiersz 17: adnotacja [@EnableTransactionManagement] wskazuje, że należy zinterpretować adnotacje [@Transactional]. Metody interfejsów [CrudRepository] posiadają te adnotacje. Są one zatem wykonywane w ramach transakcji;
  • wiersz 18: adnotacja [@EnableJpaRepositories] pozwala wskazać katalogi, w których znajdują się interfejsy Spring Data [CrudRepository]. Interfejsy te staną się komponentami Spring i będą dostępne w jego kontekście;
  • wiersz 19: adnotacja [@Configuration] sprawia, że klasa [Config] staje się klasą konfiguracyjną Springa;
  • wiersz 20: adnotacja [@ComponentScan] pozwala wymienić katalogi, w których należy szukać komponentów Springa. Komponenty Spring to klasy oznaczone adnotacjami Spring, takimi jak @Service, @Component, @Controller itp. W tym przypadku nie ma innych komponentów poza tymi zdefiniowanymi w klasie [AppConfig], dlatego adnotacja została skomentowana;
  • wiersze 24–37: definiują źródło danych, bazę danych H2. To właśnie adnotacja @Bean w wierszu 25 sprawia, że obiekt utworzony przez tę metodę staje się komponentem zarządzanym przez Spring. Nazwa metody może być tutaj dowolna. Musi ona jednak nosić nazwę [dataSource], jeśli metoda EntityManagerFactory z wiersza 51 nie istnieje i jest definiowana przez autokonfigurację;
  • wiersz 30: baza danych będzie nosić nazwę [demo] i zostanie wygenerowana w folderze projektu;
  • wiersze 40–47: definiują używaną implementację JPA, w tym przypadku implementację Hibernate. Nazwa metody może być dowolna;
  • wiersz 43: brak logów SQL;
  • wiersz 44: baza danych zostanie utworzona, jeśli jeszcze nie istnieje;
  • wiersze 50–58: definiują metodę EntityManagerFactory, która będzie zarządzać trwałością JPA. Metoda ta musi obowiązkowo nosić nazwę [entityManagerFactory];
  • wiersz 51: metoda otrzymuje dwa parametry o typach odpowiadających dwóm wcześniej zdefiniowanym beanom. Zostaną one następnie skonstruowane, a następnie wstrzyknięte przez Spring jako parametry metody;
  • wiersz 53: określa używaną implementację JPA;
  • wiersz 54: określa katalogi, w których znajdują się encje JPA;
  • wiersz 55: określa źródło danych, które ma być obsługiwane;
  • wiersze 61–66: menedżer transakcji. Metoda musi obowiązkowo nosić nazwę [transactionManager]. Jako parametr otrzymuje ona bean z wierszy 51–58;
  • wiersz 64: menedżer transakcji jest powiązany z EntityManagerFactory;

Powyższe metody można zdefiniować w dowolnej kolejności.

Uruchomienie projektu daje te same wyniki. W folderze projektu pojawia się nowy plik – plik bazy danych o nazwie H2:

  

5.1.6. Tworzenie archiwum wykonywalnego

Aby utworzyć archiwum wykonywalne projektu, można postępować w następujący sposób:

  • w [1]: tworzymy konfigurację uruchomienia;
  • w [2]: typu [Java Application]
  • w [3]: określa projekt do uruchomienia (użyj przycisku Browse);
  • w [4]: określa klasę do wykonania;
  • w [5]: nazwa konfiguracji uruchomienia – może być dowolna;
  • w [6]: eksportuje się projekt;
  • w [7]: w postaci archiwum wykonywalnego JAR;
  • w [8]: podaje ścieżkę i nazwę pliku wykonywalnego, który ma zostać utworzony;
  • w [9]: nazwa konfiguracji uruchomieniowej utworzonej w [5];
10
  
  • w [10] – utworzony plik archiwum;

Po wykonaniu tych czynności otwieramy konsolę w folderze zawierającym archiwum wykonywalne:

.....\dist>dir
12/06/2014  09:11        15 104 869 gs-accessing-data-jpa-02.jar

Archiwum uruchamia się w następujący sposób:


.....\dist>java -jar gs-accessing-data-jpa-02.jar

W konsoli pojawiają się następujące wyniki:

SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder  więcej szczegółów.
mars 10, 2015 5:27:20 PM org.hibernate.jpa.internal.util.LogHelper logPersistenceUnitInformation
INFO: HHH000204: Processing PersistenceUnitInfo [
    name: default
    ...]
mars 10, 2015 5:27:20 PM org.hibernate.Version logVersion
INFO: HHH000412: Hibernate Core {4.3.8.Final}
mars 10, 2015 5:27:20 PM org.hibernate.cfg.Environment <clinit>
INFO: HHH000206: hibernate.properties not found
mars 10, 2015 5:27:20 PM org.hibernate.cfg.Environment buildBytecodeProvider
INFO: HHH000021: Bytecode provider name : javassist
mars 10, 2015 5:27:22 PM org.hibernate.annotations.common.reflection.java.JavaReflectionManager <clinit>
INFO: HCANN000001: Hibernate Commons Annotations {4.0.5.Final}
mars 10, 2015 5:27:22 PM org.hibernate.dialect.Dialect <init>
INFO: HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
mars 10, 2015 5:27:22 PM org.hibernate.hql.internal.ast.ASTQueryTranslatorFactory <init>
INFO: HHH000397: Using ASTQueryTranslatorFactory
mars 10, 2015 5:27:22 PM org.hibernate.tool.hbm2ddl.SchemaUpdate execute
INFO: HHH000228: Running hbm2ddl schema update
mars 10, 2015 5:27:22 PM org.hibernate.tool.hbm2ddl.SchemaUpdate execute
INFO: HHH000102: Fetching database metadata
mars 10, 2015 5:27:22 PM org.hibernate.tool.hbm2ddl.SchemaUpdate execute
INFO: HHH000396: Updating schema
mars 10, 2015 5:27:22 PM org.hibernate.tool.hbm2ddl.DatabaseMetadata getTableMetadata
INFO: HHH000262: Table not found: Customer
mars 10, 2015 5:27:22 PM org.hibernate.tool.hbm2ddl.DatabaseMetadata getTableMetadata
INFO: HHH000262: Table not found: Customer
mars 10, 2015 5:27:22 PM org.hibernate.tool.hbm2ddl.DatabaseMetadata getTableMetadata
INFO: HHH000262: Table not found: Customer
mars 10, 2015 5:27:22 PM org.hibernate.tool.hbm2ddl.SchemaUpdate execute
INFO: HHH000232: Schema update complete
Customers found with findAll():
-------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']
Customer[id=2, firstName='Chloe', lastName='O'Brian']
Customer[id=3, firstName='Kim', lastName='Bauer']
Customer[id=4, firstName='David', lastName='Palmer']
Customer[id=5, firstName='Michelle', lastName='Dessler']

Customer found with findOne(1L):
--------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']

Customer found with findByLastName('Bauer'):
--------------------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']
Customer[id=3, firstName='Kim', lastName='Bauer']

5.1.7. Tworzenie projektu [Spring Data]

Aby utworzyć szkielet projektu Spring Data, można postępować w następujący sposób:

  • w [1] tworzymy nowy projekt;
  • w [2]: typu [Spring Starter Project];
  • w [3] podajemy nazwę grupy projektu;
  • w [4]: podajemy nazwę artefaktu (w tym przypadku pliku jar), który zostanie utworzony podczas kompilacji projektu;
  • w polu [5]: nazwa projektu w Eclipse – może być dowolna (nie musi być identyczna z wartością z pola [4]);
  • w [7]: określa się, że zostanie utworzony projekt zawierający warstwę [JPA] wraz z SGBD i MySQL. Niezbędne zależności dla takiego projektu zostaną następnie uwzględnione w pliku [pom.xml];
  • w pliku [8] należy podać nazwę folderu projektu;
  • w pliku [9] zakończyć pracę kreatora;
  • w pliku [10]: utworzony projekt;

Plik [pom.xml] zawiera zależności niezbędne dla projektu JPA:


<?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/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>istia.st.springdata</groupId>
    <artifactId>intro-spring-data-01</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>intro-spring-data-01</name>
    <description>démo spring data avec table de produits</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
        <relativePath/> <!-- wyszukiwanie elementu nadrzędnego z repozytorium -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <start-class>demo.IntroSpringData01Application</start-class>
        <java.version>1.7</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
  • wiersze 14–19: nadrzędny projekt Maven;
  • wiersze 28–31: zależność niezbędna dla projektu JPA – obejmie projekt [Spring Data];
  • wiersze 32–36: zależność MySQL od sterownika JDBC;
  • wiersze 37–41: zależności wymagane dla testów JUnit zintegrowanych ze Springiem;

Klasa wykonywalna [Application] nie wykonuje żadnych czynności, ale jest wstępnie skonfigurowana:


package demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class IntroSpringData01Application {

    public static void main(String[] args) {
        SpringApplication.run(IntroSpringData01Application.class, args);
    }
}
  • adnotacja [@SpringBootApplication] sprawia, że klasa ta staje się klasą samokonfigurującą się w projekcie;

Klasa testowa [ApplicationTests] nie wykonuje żadnych czynności, ale jest wstępnie skonfigurowana:


package demo;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = IntroSpringData01Application.class)
public class IntroSpringData01ApplicationTests {

    @Test
    public void contextLoads() {
    }

}
  • wiersz 9: adnotacja [@SpringApplicationConfiguration] umożliwia wykorzystanie pliku konfiguracyjnego [IntroSpringData01Application]. Klasa testowa będzie w ten sposób korzystać ze wszystkich beanów zdefiniowanych w tym pliku;
  • wiersz 8: adnotacja [@RunWith] umożliwia integrację Springa z JUnit: klasa będzie mogła zostać uruchomiona jako test JUnit. [@RunWith] jest adnotacją JUnit (wiersz 4), podczas gdy klasa [SpringJUnit4ClassRunner] jest klasą Spring (wiersz 6);

Teraz, gdy mamy już szkielet aplikacji JPA, możemy go uzupełnić.