Spring Boot – Interakcja z bazą danych czyli Spring Data JPA

W poprzednim poście pokazującym jak zacząć ze Spring Bootem pokazałem jak stworzyć Hello World dla aplikacji web. Przyszedł czas aby do naszą aplikację połączyć z bazą danych. Będzie nam potrzebne kilka rzeczy:

  • Baza danych
  • Model – klasy POJO z adnotacjami które będą reprezentować schemat bazy danych (więcej o tym za chwilę)
  • Repozytoria – Klasy lub interfejsy które będą definiowały operacje które można wykonać na modelu (takie jak zapisanie obiektu w bazie, wyszukiwanie obiektów itp)

Mogą tutaj pojawić się elementy projektu które pokazywałem w poprzednim poście więc zachęcam do jego przeczytania. Zacznijmy po kolei.

W niektórych miejscach odwołuję się do projektu z poprzedniego postu który pokazywał jak zacząć ze Spring Bootem, polecam się z nim zaznajomić, znajduje się pod adresem: http://blog.mloza.pl/spring-boot-szybkie-tworzenie-aplikacji-web-w-javie/

Kod źródłowy gotowego projektu można znaleźć na GitHubie pod adresem: https://github.com/mloza/spring-boot-database

Baza danych

Spring Data JPA łączy się z wieloma różnymi bazami danych. Może bez problemu działać na bazach typu embedded jak h2, MySQL, Oracle i wielu wielu innych. Jeśli nie mamy żadnego serwera bazodanowego najszybszym sposobem na sprawdzenie działania kodu w akcji jest użycie bazy h2 w trybie memory. Jest ona uruchamiana wraz z naszą aplikacją i przetrzymywana w całości w pamięci więc jeśli zrestartujemy aplikację baza zostanie usunięta. Baza ta też może być również zapisywana do pliku ale o tym może innym razem. Aby dodać bazę h2 do projektu wystarczy dodać zależność w Mavenie:

Dzięki temu Spring Boot sam wykryje, że odpowiednie klasy znajdują się na classpath-ie i się odpowiednio skonfiguruje. Jednak aby to zrobił musi wiedzieć, że tworzymy aplikacje która będzie używać bazy danych. Aby go o tym poinformować dodajemy do pom-a kolejną zależność:

Mając obie zależności w pom-ie jesteśmy gotowi aby stworzyć model.

Model

W najprostszych słowach model będzie reprezentował strukturę naszej bazy danych. Każdy obiekt zostanie odwzorowany w tabeli, a każde pole w obiekcie będzie oddzielną kolumną. Możemy też w modelu opisać relacje pomiędzy obiektami które zostaną odwzorowane w bazie danych za pomocą kluczy obcych. Instancja obiektu jest odwzorowywana w bazie danych jako pojedyncza krotka (wiersz, wpis).

Stworzenie klas modelu spowoduje, że Spring przy uruchomieniu połączy się z bazą danych, sprawdzi strukturę bazy danych i dokona potrzebnych modyfikacji (stworzy nieistniejące tabele, doda pola do tabel itp.).

Importy adnotacji powinny pochodzić z pakietu javax.persistence

Najprostszy model definiujący tabelkę z zadaniami, która posiada właściwości takie jak: nazwa, opis, budżet i czy zostało wykonane może wyglądać w następujący sposób:

Najważniejsze są pola klasy i adnotacje, jednak umieściłem tutaj również mutatory (gettery, settery itp.) ponieważ przydadzą się one w późniejszej części wpisu. Co robią kolejne adnotacje:

  • @Entity – adnotacja dla klasy, mówi o tym, że klasa reprezentuje encję w skrócie możemy przyjąć, że będzie ona odwzorowana na pojedynczą tabelkę (relację dla baz relacyjnych)
  • @Id – definiuje klucz główny, jest on unikalnym identyfikatorem rekordu w bazie danych, w naszym przykładzie występuje z adnotacją @GeneratedValue które mówi o tym, że wartość ta powinna zostać wygenerowana automatycznie (nie ma dla nas znaczenia jak, przykładowo w bazie danych MySQL tak opisane pole zostanie utworzone z opcją auto_increment, znaczy to tyle, że każdy kolejny wstawione rekord dostanie kolejną wartość liczby, rekordy będą otrzymywać kolejno 1, 2, 3….n)
  • @Column – informuje, że pole to jest kolumną w bazie danych
  • @Lob – informuje, że będziemy przechowywać w tym polu duże obiekty, dla przykładu pole typu string z samą adnotacją @Column w MySQL zostanie stworzone jako VARCAHR(255), czyli nie zapiszemy w nim wartości dłuższych niż 255 znaków. Jeżeli dołączona zostanie adnotacja @Lob pole to będzie typu TEXT.

Po uruchomieniu aplikacji struktura zostanie automatycznie stworzona. Jeżeli chcemy zobaczyć zapytania jakie zostały użyte do jej utworzenia możemy dodać do pliku application.properties wpis:

Plik application.properties może znajdować się w kilku miejscach. Najlepiej umieścić go na classpath poprzez utworzenie go w katalogu resources. Jeśli budujemy fat jara możemy również go umieścić w tym samym katalogu co jar, wtedy zostanie wczytany stamtąd. Daje nam to możliwość nadpisywania opcji w gotowej aplikacji.

Dzięki temu na konsoli będziemy mogli zobaczyć wszystkie zapytania wykonane do bazy włącznie z tymi które zostały użyte do stworzenia struktury bazy danych. Aktualnie Spring jeszcze nie wie gdzie szukać naszych klas definiujących strukturę, jednak gdy już wszystko skonfigurujemy, zobaczymy że, do stworzenia naszej tabelki zostaną użyte następujące zapytania:

Mając już model możemy przejść do repozytoriów.

Repozytoria

Model zdefiniował jaką strukturę mają nasze dane. Repozytoria będą definiować jakie operacje możemy wykonać na naszych danych. Podstawowe operacje CRUD (Create, Read, Update, Delete) dostajemy od Springa, inne operacje jak wyszukiwanie po polach musimy dodać sami.

Najprostsze repozytoria tworzone są jako interfejsy. Cały kod który jest potrzebny do wykonania akcji jest generowany przez Springa. Zacznijmy więc od najprostszego repozytorium które dostarczy nam wcześniej wspomnianych operacji CRUD. Nasze repozytorium będzie operować na tabeli Task zdefiniowanej w poprzednim punkcie. Wygląda ono następująco:

I właśnie ta jedna linijka załatwia całą sprawę, dzięki niej możemy użyć następujących operacji:

  • Task save(Task t) – zapisz task do bazy danych
  • Iterable save(Iterable t) – zapisanie kolekcji obiektów
  • Task findOne(Long id) – znajduje wpis z kluczem podanym jako parametr
  • boolean exists(Long id) – sprawdź czy wpis z kluczem istnieje
  • Iterable findAll() – pobierz wszystkie wpisy z bazy danych
  • Iterable findAll(Iterable IDs) – znajdź wszystkie elementy z kluczami na liście IDs
  • long count() – policz elementy w tabeli
  • void delete(Long id) – usuń element z kluczem id
  • void delete(Task r) – usuń obiekt z tabelki
  • void delete(Iterable IDs) – usuń wszystkie obiekty których klucze znajdują się na liście IDs
  • deleteAll() – wyczyść tabelkę

Więcej o repozytoriach będzie w kolejnych podpunktach, a teraz spróbujmy użyć stworzonych przez nas klas.

Praktyczne zastosowanie

Napisaliśmy już trochę kodu jednak nadal nic on nie zrobił. Spróbujmy więc użyć wcześniej przygotowanych klas aby coś w tej bazie zapisać.

Najpierw powiedzmy Springowi, ze będziemy używać repozytoriów i gdzie ich powienien szukać. Do klasy Main musimy dodać adnotację @EnableJpaRepositories wraz z parametrem basePackagesClasses tak aby wyglądało to następująco:

Jeśli występuje problem z odnalezieniem klas (class x.y.Z is not managed) spróbuj do ComponentScan dodać parametr basePackageClasses = {Task.class, PageController.class}. Zauważyłem również, że problem ten występuje gdy klasa Main jest w pakiecie poniżej innych klas np. Main jest w pakiecie x.y.z, a Entity w pakiecie x.y.a, przeniesienie Main do x.y pomaga.

Potrzebny będzie nam kontroller, w poprzednim poście stworzyliśmy PageController który może być teraz użyty. Zdefiniujmy pole klasy z typem naszego repozytorium, oraz dodajmy do niego adnotację @Autowired:

Adnotacja ta mówi Springowi aby poszukał klasy z typem TaskRepository i przypisał ją do naszego pola klasy. Przejdźmy teraz do metody która zapisze nam nowy obiekt do bazy i wyświetli aktualną zawartość bazy w przeglądarce:

Na początku tworzymy nowy task, po prostu przez operator new i wypełniamy pola wartościami. Następnie przy pomocy repozytorium zapisujemy task w bazie danych. Następnie w pętli for pobieramy wszystkie zapisane krotki i dodajemy je do odpowiedzi która zostanie wysłana do przeglądarki. Wszystkie adnotacje zostały omówione w poprzednim poście, więc jeśli coś jest niejasne zachęcam do przeczytania go. Po uruchomieniu aplikacji i przejściu na stronę http://localhost:8080/db powinniśmy ujrzeć dodany task. Jeśli odświeżymy kilkukrotnie stronę otrzymamy coś podobnego:

Inicjowanie bazy danych

Spring JPA daje możliwość wrzucenia skryptu SQL który zostanie wykonany przy uruchomieniu aplikacji, np do zainicjowania danych testowych. Wystarczy wrzucić do resources plik o nazwie data.sql i zostanie on automatycznie wykonany. Dodajmy zatem taki plik o przykładowej zawartości:

Po zrestartowaniu aplikacji i odświeżeniu strony zobaczymy stworzone wpisy przez skrypt. Powinno to wyglądać mniej więcej tak:

Będzie to przydatne przy kolejnych przykładach.

Bardziej skomplikowane operacje na repozytoriach

Rozszerzenie interfejsu CrudRepository daje nam podstawowe operacje. Ale przyjmijmy, że chcemy wyszukać wszystkie taski które mają ustawione done na wartość true. Lub wszystkie które w description zawierają jakiś ciąg znaków. Aby to osiągnąć wystarczy zmodyfikować repozytorium do następującej postaci:

Wciąż jest to interfejs. Cały kod potrzebny do obsługi jest generowany przez Springa. Przedstawiłem dwa typy metod. Pierwsza z nich to findByDone(Boolean done). Tutaj Spring domyśla się co ma zrobić po nazwie metody. Można by w podobny sposób zapisać findByName(String name). Możemy również łączyć warunki i tworzyć nazwy takie jak findByDoneAndName(Boolean done, String name). Podobnie można użyć operatora OR.
Drugim typem metod jest metoda z adnotacją @Query. Jeśli nie możemy wyrazić naszych zamiarów za pomocą nazwy metody wtedy przekazujemy Query które zostanie wykonane przy wywołaniu tej metody. Domyślnie Query jest zapisane jako JQL (JPA Query Language), dodanie do adnotacji parametru nativeQuery = true sprawi, że query będzie traktowane jako query w dialekcie używanej bazy danych. Sprawdźmy zatem czy zadziała to w praktyce. Dodajmy drugą metodę do naszego kontrolera:

Restart serwera i przejście na stronę http://localhost:8080/db2 powinno zwrócić nam taką odpowiedź:

Konfiguracja innych baz danych

Bazy w pamięci przydają się jeśli chcemy szybko stworzyć prototyp aplikacji lub przy testowaniu. Jednak nie nadają się do pracy na produkcji. Jeśli chcemy użyć innej bazy wystarczy podać jej parametry w pliku application.poperties oraz dodać sterownik bazy do projektu. Przykładowo dla MySQL, sterownik możemy załadować jako zależność w Mavenie:

A do pliku application.properties należy dodać:

Po uruchomieniu, nasza aplikacja będzie próbować się łączyć do serwera MySQL.

Podsumowanie

Wiemy już jak wykonywać podstawowe interakcje z bazą danych. W kolejnym wpisie pokażę jak ładnie przedstawić wynik pracy użytkownikowi za pomocą widoków, a później wrócimy jeszcze do JPA aby zaznajomić się ze sposobem opisywania relacji pomiędzy encjami.

8 thoughts on “Spring Boot – Interakcja z bazą danych czyli Spring Data JPA”

  1. Jako jeszcze osoba początkująca ze Springiem, muszę się upewnić 😀
    We fragmencie, gdzie pokazałeś @EnableJpaRepositories widać również dwa inne elementy, które wchodzą w skład @SpringBootApplication. Możesz powiedzieć, dlaczego wybrałeś akurat takie rozwiązanie? Czy jest ono w czymś lepsze?
    Pozdrawiam 🙂

    1. Gdy zaczynałem pracę ze Spring Bootem nie było jeszcze @SpringBootApplication, została ona dodana w wersji 1.2.0.RELEASE. Co prawda używam już tej wersji w poście, ale jeszcze konfigurowałem po staremu, dopiero później znalazłem informację, że można zastąpić to wszystko jedno adnotacją. W poście również przekazywałem przez tą adnotacje gdzie ma szukać klas (basePackageClasses), czasem z jakiegoś powodu nie udawało mu się odnaleźć moich repozytoriów. Aktualnie nic nie stoi na przeszkodzie aby używać @SpringBootApplication.

  2. Wow. To straszne, że ten blog jest tak trudny do odnalezienia, to pewnie przez niezbyt rozbudowany content. Naprawdę świetnie tłumaczysz. Nie wiem dlaczego, ale proste zagadnienia na wszystkich stronach są strasznie pogmatwane i nieprzejrzyste. Dopiero tutaj znalazłem wszystko przedstawione tak jak to moim zdaniem powinno wyglądać. Dziękuje, naprawdę

    1. Miło mi to słyszeć, dziękuję! Tak, muszę jeszcze popracować nad zawartością, niestety ostatnio nie mam czasu wrócić do pisania artykułów.

  3. Mam nadzieję, ze mi wybaczysz pytanie niezwiązane z postem, ale mam nadzieję że mimo wszystko mi pomożesz. Chodzi mi o mapowanie relacji – np. czy jeśli tworzyłabym bloga i chciałabym aby pod każdym postem była możliwość komentowania to komentarze do danego posta powinne być relacją @ManyToOne?
    W sensie że byłaby tabela ‘posty’ z id_posta+ nagłówkiem + treścią posta , tabela ‘komentarze’ z id_komentarza+ autorem + treścią i tabela ‘posty_komentarze” z id_posta + id_komentarza?

    1. Troszkę tutaj jest pomieszane, relacja o której piszesz (ta z tabelą pośrednią posty_komentarze) to relacja ManyToMany. Przy relacji ManyToOne musisz wstawić kolumnę post_id w tabeli komentarze i tam trzymać odniesienie do odpowiedniego posta. I tak, powinnaś użyć relacji ManyToOne.

  4. Witam. po wykonaniu według tego przepisu mam jakiś problem z beanem.
    Error creating bean with name ‘pageController’: Injection of autowired dependencies failed; nested exception is org.springframework.beans.factory.BeanCreationException: Could not autowire field: public repository.TaskRepository springbootquickstart.PageController.taskRepository; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name ‘taskRepository’: Invocation of init method failed; nested exception is java.lang.IllegalArgumentException: Not an managed type: class model.Task

    1. Wygląda jakby Spring nie mógł znaleźć Twojej klasy, może inaczej stworzyłeś pakiety? Możesz dodać adnotację @EntityScan(basePackageClasses = Task.class) do klasy Main, powinno pomóc.

Leave a Reply

Your email address will not be published. Required fields are marked *