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:

        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
        </dependency>

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ść:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

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:

@Entity
public class Task {
    @GeneratedValue
    @Id
    private Long id;

    @Column
    private String name;

    @Column
    @Lob
    private String description;

    @Column
    private Double budget;

    @Column
    private Boolean done;

    //Gettery, Settery itp.
    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 String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public Double getBudget() {
        return budget;
    }

    public void setBudget(Double budget) {
        this.budget = budget;
    }

    public Boolean getDone() {
        return done;
    }

    public void setDone(Boolean done) {
        this.done = done;
    }

    public Task withId(final Long id) {
        this.id = id;
        return this;
    }

    public Task withName(final String name) {
        this.name = name;
        return this;
    }

    public Task withDescription(final String description) {
        this.description = description;
        return this;
    }

    public Task withBudget(final Double budget) {
        this.budget = budget;
        return this;
    }

    public Task withDone(final Boolean done) {
        this.done = done;
        return this;
    }

    @Override
    public String toString() {
        return "TaskEntity{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", description='" + description + '\'' +
                ", budget=" + budget +
                ", done=" + done +
                '}';
    }
}

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:

spring.jpa.show-sql = true

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:

Hibernate: drop table task if exists
Hibernate: create table task (id bigint generated by default as identity, budget double, description clob, done boolean, name varchar(255), primary key (id))

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:

public interface TaskRepository extends CrudRepository<Task, Long> { }

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:

@EnableAutoConfiguration
@ComponentScan
@EnableJpaRepositories(basePackageClasses = TaskRepository.class)
public class Main {
    public static void main(String[] args) {
        SpringApplication.run(Main.class, args);
    }
}

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:

    @Autowired
    public TaskRepository taskRepository;

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:

    @RequestMapping("/db")
    @ResponseBody
    public String testMethod() {
        StringBuilder response = new StringBuilder();

        Task task = new Task()
                .withName("Task 1")
                .withDescription("Test task")
                .withBudget(123.55)
                .withDone(true);
        taskRepository.save(task);

        for(Task i: taskRepository.findAll()) {
            response.append(i).append("<br>");
        }

        return response.toString();
    }

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:

TaskEntity{id=1, name='Task 1', description='Test task', budget=123.55, done=true}
TaskEntity{id=2, name='Task 1', description='Test task', budget=123.55, done=true}
TaskEntity{id=3, name='Task 1', description='Test task', budget=123.55, done=true}
TaskEntity{id=4, name='Task 1', description='Test task', budget=123.55, done=true}
TaskEntity{id=5, name='Task 1', description='Test task', budget=123.55, done=true}
TaskEntity{id=6, name='Task 1', description='Test task', budget=123.55, done=true}

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:

Insert into task(name, description, budget, done) values('Task 1', 'Description of task 1', 100.00, 1);
Insert into task(name, description, budget, done) values('Task 2', 'Description of task 2 Do', 100.00, 1);
Insert into task(name, description, budget, done) values('Task 3', 'Description of task 3 Do', 50.00, 0);

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

TaskEntity{id=1, name='Task 1', description='Description of task 1', budget=100.0, done=true}
TaskEntity{id=2, name='Task 2', description='Description of task 2 Do', budget=100.0, done=true}
TaskEntity{id=3, name='Task 3', description='Description of task 3 Do', budget=50.0, done=false}
TaskEntity{id=4, name='Task 1', description='Test task', budget=123.55, done=true}

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:

public interface TaskRepository extends CrudRepository<task, long> {

    public List findByDone(Boolean done);

    @Query("select t from Task t where t.description like %?1%")
    public List getByDescriptionLike(String search);
}

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:

    @RequestMapping("/db2")
    @ResponseBody
    public String testMethod2() {
        StringBuilder response = new StringBuilder();

        response.append("Tasks with done set to true:\n");
        for(Task i: taskRepository.findByDone(true)) {
            response.append(i).append("\n");
        }

        response.append("Tasks with done set to false:\n");
        for(Task i: taskRepository.findByDone(false)) {
            response.append(i).append("\n");
        }

        response.append("Tasks with \"Do\" in description:\n");
        for(Task i: taskRepository.getByDescriptionLike("Do")) {
            response.append(i).append("\n");
        }

        return response.toString();
    }

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

Tasks with done set to true:
TaskEntity{id=1, name='Task 1', description='Description of task 1', budget=100.0, done=true}
TaskEntity{id=2, name='Task 2', description='Description of task 2 Do', budget=100.0, done=true}
Tasks with done set to false:
TaskEntity{id=3, name='Task 3', description='Description of task 3 Do', budget=50.0, done=false}
Tasks with "Do" in description:
TaskEntity{id=2, name='Task 2', description='Description of task 2 Do', budget=100.0, done=true}
TaskEntity{id=3, name='Task 3', description='Description of task 3 Do', budget=50.0, done=false}

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:

  <dependency> 
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.34</version>
  </dependency>

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

spring.datasource.url=jdbc:mysql://localhost/nazwa-bazy
spring.datasource.username=login
spring.datasource.password=haslo
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

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.

15 myśli na “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.

  5. Fajny tutorial. Przyznam szczerze że mam już troche doświadczenia komercyjnego ze springiem i spring data, ale gdy rok temu byłem kompletnie zielony to właśnie takiego czegoś mi brakowało. Pozdrawiam

  6. Witam. Na początek – super zawartość.
    Określiłbym się jako poczatkujący, ale zawartość jest jasna i oczywiście żadnych pytań uzupełniających, tylko „jakby” kontynuujące. Czy takie rozwiązanie może być produkcyjne, czy trzeba jeszcze uzupełniać je o wartstwy DAO i Service (czy to już jest rozwiązanie ekwiwalentne). Jeżeli mieszam zagadnienia to prosiłbym o naprowadzenie, w wolnej chwili oczywiście. Pozdrawiam

    1. Cześć, miło to słyszeć, dziękuję.

      Prezentowane przykłądy są uproszczeniami, tak aby nie komplikować i nie zaciemniać obrazu. Produkcyjne rozwiązanie powinno komunikację z bazą mieć w Servisie. Proste operacje jak pokazane w poście działają w kontrolerze, ale jeśli pojawią się np. encje które mają relacje to możesz dostać błąd przy ich pobieraniu.

  7. Mam pewien problem, repo z błędem na na github poniżej:

    https://github.com/scina/JavaSpringPerson.git

    Wszystko (niby) cacy działa w bazie H2, dodaje rekordy, pobiera, edytuje. Jednak gdy chciałem wypełnić bazę z pliku data.sql pojawiły się problemy:
    Insert musiał mieć podany klucz ID – inaczej się nie chce uruchomić.

    INSERT into users(user_login, password) values (‚root’, ‚root’);
    NULL not allowed for column „ID”; SQL statement:

    Jak już wpisze się id, to aplikacja pójdzie, ale wyskakuje błąd gdy się chce coś dodać do bazy, w konsoli występują wpisy z wartościami (?,?,?)

    SQL [n/a]; constraint [„PRIMARY KEY ON PUBLIC.USERS(ID) [1, ‚root’, ‚root’]”; SQL statement: insert into users (password, user_login, id) values (?, ?, ?)

    Jestem zielony, proszę o pomoc.

    1. W UserEntity masz @GeneratedValue(strategy = GenerationType.IDENTITY), z tego co kojarzę to w H2 to nie jest wspierane, spróbuj zmienić na @GeneratedValue(strategy = GenerationType.AUTO), wtedy aplikacja powinna sama dobrać strategię generowania kluczy dla użytej bazy.

  8. Z kluczem sobie poradziłem, ale teraz po testach, chciałbym bazę mieć na serwerze, jednocześnie chciałbym za pierwszym razem załadować przykładowe dane. Chodzi o to, że jak pobiore sobie aplikacje lub ktos inny i poda swoje dane do połączenia ze swoją bazą, to jeśli na niej nic nie ma, to zbuduje tabele i wypełni przykładowymi danymi, takie demo. Po następnym uruchomieniu, bazy ma już nie tykac. Jest to do zrobienia?

    1. Da się to osiągnąć. Możesz przykładowo przy uruchomieniu wykonać zapytanie do bazy które sprawdzi czy są załadowane dane i jeśli nie to uruchomić kod odpowiedzialny za stworzenie tych danych, może to być przykładowo uruchomienie kodu SQL zapisanego w pliku. Dużo narzędzi działa w ten sposób.

      Jeśli chcesz bardziej skomplikowany sposób możesz skorzystać z narzędzi do kontroli wersji bazy danych takich jak np. flyway (flywaydb.org).

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *