Jednym z głównych modułów frameworka Spring jest moduł Spring Data, dzięki któremu możemy zminimalizować niemal do zera ilość tak zwanego „boiler-plate” kodu przy tworzeniu znanych z Domain Driven Design repozytoriów. DDD jest podejściem do tworzenia obiektowego modelu naszej aplikacji, natomiast repozytoria są to klasy, które odpowiadają za pobieranie obiektów domenowych z zewnętrznego źródła danych, dodatkowo również tam umieszcza się różnorakie metody odpowiadające za zwracanie obiektów spełniających dane kryteria. Mówiąc jeszcze prościej – repozytoria odpowiadają za obsługę operacji typu CRUD (zestawu podstawowych operacji bazodanowych – Create, Read, Update, Delete) dla danego obiektu domenowego.
Utworzenie projektu
Zacznijmy tradycyjnie od utworzenia nowego projektu – w tym wypadku skorzystamy z generatora projektów springowych – strony http://start.spring.io/
Interesuję nas taka konfiguracja projektu:
Niezbędne dla nas zależności to JPA i H2 (wbudowana, lekka baza danych).
Utworzenie obiektu domenowego i repozytorium
Po utworzeniu projektu w IntelliJ dodajmy prosty obiekt domenowy Employee
, z którego robimy od razu encję:
package pl.devfoundry.querydemo.domain; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; @Entity public class Employee { @Id @GeneratedValue private int id; private String name; private int age; private Employee() { } public Employee(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return "Employee{" + "id=" + id + ", name='" + name + '\'' + ", age=" + age + '}'; } }
Następnie w tej samej paczce co klasa Employee
dodajmy paczkę repository
i tam utwórzmy nowy interfejs EmployeeRepository
, który będzie rozszerzał interfejs JpaRepository
rodem ze Spring Data
package pl.devfoundry.querydemo.domain.repository; import org.springframework.data.jpa.repository.JpaRepository; import pl.devfoundry.querydemo.domain.Employee; public interface EmployeeRepository extends JpaRepository<Employee, Integer> { }
Interfejs JpaRepository
potrzebuje dwóch wartości generycznych – pierwsza z nich odpowiada encji, której repozytorium chcemy utworzyć. Drugie natomiast typowi klucza głównego tej encji (w naszym wypadku int
-> Integer
)
Teraz w głównej klasie aplikacji, możemy śmiało korzystać z tego interfejsu, by dodawać, usuwać i wyszukiwać wszystkich pracowników. To wszystko bez napisania nawet linijki kodu związanego z JPA.
Użyjmy naszego repozytorium. Utwórzmy springowy bean, który w metodzie init()
będzie tworzył nowego pracownika.
package pl.devfoundry.querydemo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import pl.devfoundry.querydemo.domain.Employee; import pl.devfoundry.querydemo.domain.repository.EmployeeRepository; import javax.annotation.PostConstruct; @Component public class DataLoader { @Autowired EmployeeRepository employeeRepository; @PostConstruct public void init() { System.out.println("Database operations"); employeeRepository.save(new Employee("Pawel", 32)); } }
Wstrzykujemy poprzez @Autowired
komponent EmployeeRepository
, warto zwrócić tu uwagę, że sami takiego komponentu nigdzie nie definiowaliśmy – został on wygenerowany przez Spring Data automatycznie na podstawie interefejsu. Dzięki użyciu @PostConstruct
mamy pewność, że kawałek kodu zawarty w metodzie init()
wywoła się zaraz po utworzeniu komponenetu DataLoader
.
Dodatkowo do pliku application.properties dodajmy wpisy:
spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true
Dzięki nim na konsoli będziemy widzieć jakie kwerendy wykonuje Hibernate.
Uwaga! Jeśli używasz Javy 9+ do dependencji wygenerowanego projektu należy dodać wpis:
<dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> <version>2.3.0</version> </dependency>
A dlaczego to już temat na zupełnie osobny wpis, albo i więcej. Na chwilę obecną musisz mi zaufać 🙂
Po wywołaniu tego kodu pośród logów zaobserwować można wpis:
Hibernate: insert into employee (age, name, id) values (?, ?, ?)
Czyli Hibernate dodał pracownika do bazy.
Proste wyszukiwania bazodanowe
Dodatkowo możemy w bardzo łatwy sposób dodawać metody, które będą wyszukiwały pracownika po danym parametrze. Wystarczy dodać metodę findByName
do interfejsu by Spring Data automagicznie automatycznie utworzył niezbędny kod JPA i wygenerował kod dla metody.
public interface EmployeeRepository extends JpaRepository<Employee, Integer> { List<Employee> findByName(String name); }
@Component public class DataLoader { @Autowired EmployeeRepository employeeRepository; @PostConstruct public void init() { System.out.println("Database operations"); employeeRepository.save(new Employee("Pawel", 32)); find(); } private void find() { List<Employee> pawels = employeeRepository.findByName("Pawel"); pawels.forEach(System.out::println); } }
W logach znajdziemy wpis:
Employee{id=1, name='Pawel', age=32}
Podobnie możemy utworzyć metody np. findByNameAndAge
, findByAge
, findByNameOrAge
Własne metody wyszukiwania
Co w przypadku gdy chcemy utworzyć metodę, która wyszuka nam pracowników z imieniem zaczynającym się na literę „A”?
Możemy dodać deklarację metody findByNameStartingWith
do interfejsu repozytorium i automat nam wygeneruję co trzeba. Jak widać Spring Data ma rozwiązanie na wszystko, ale na potrzeby tego wpisu załóżmy, że nie ma powyższej funkcjonalności i musimy dodać do naszego repozytorium własne zapytanie JPA. Robimy takie rzeczy za pomoca adnotacji @Query
przy deklaracji metody w interfejsie EmployeeRepository
public interface EmployeeRepository extends JpaRepository<Employee, Integer> { List<Employee> findByName(String name); @Query("Select e from Employee e WHERE e.name like 'P%'") List<Employee> myOwnQuery(); }
Wówczas wywołanie myOwnQuery()
wywoła na bazie zapytanie JPQL zawarte jako parametr dla adnotacji @Query
.
@Component public class DataLoader { @Autowired EmployeeRepository employeeRepository; @PostConstruct public void init() { System.out.println("Database operations"); employeeRepository.save(new Employee("Pawel", 32)); find(); } private void find() { List<Employee> pawels = employeeRepository.myOwnQuery(); pawels.forEach(System.out::println); } }
Oczywiście do tak utworzonych zapytań możemy przesyłać parametry, robi się to w następujący sposób:
public interface EmployeeRepository extends JpaRepository<Employee, Integer> { List<Employee> findByName(String name); @Query("Select e from Employee e WHERE e.name like CONCAT(:firstLetter,'%')") List<Employee> findByFirstLetter(@Param("firstLetter") String letter); }
@Component public class DataLoader { @Autowired EmployeeRepository employeeRepository; @PostConstruct public void init() { System.out.println("Database operations"); employeeRepository.save(new Employee("Pawel", 32)); find(); } private void find() { List<Employee> pawels = employeeRepository.findByFirstLetter("P"); pawels.forEach(System.out::println); } }
Co ponownie zwróci nam wszystkich pracowników z imieniem zaczynającym się na literę P, a będzie to jeden pracownik o imieniu ‚Pawel’.
To są tylko podstawowe przykłady użycia modułu Spring Data, będącego częścią składową Spring Framework, a już one pokazują jaka moc w nim drzemie i jak bardzo przycina ilość kodu potrzebnego do komunikacji z bazą danych.
Kod przykładu dostępny jest na platformie gitub.
Bardzo fajne przykłady (nie mówię tylko tym wpisie). A Spring Data robi wrażenie, szczególnie że rozwiązuje zapytania po nazwie metody. 🙂 Czy w prostych przypadkach lepiej używać CrudRepository zamiast JpaRepository?
Hej, dzięki 🙂 Różnicy wielkiej nie ma – tzn. JpaRepository ma więcej metod (CrudRepository + Paging + Sorting + operacje batchowe), więc narzut na zasoby jest żaden. Moim zdaniem to kwestia tylko i wyłącznie tego ile funkcjonalności chcesz udostępnić użytkownikom repozytorium. Dodatkowo, jeśli dopiero się uczysz to lepiej od razu wskoczyć w JpaRepozytory i poznać jego wszystkie możliwości, bo na przykład wspomniana obsługa stronicowania wyników to chleb powszedni w każdej biznesowej aplikacji.
Przystępnie opisane. Dzięki