В этом материале мы рассмотрим, как сделать минимальное Spring Boot приложение с поддержкой JPA, реализованной с помощью Hibernate. Приложение будет консольным, данные будут храниться в развёрнутой в оперативной памяти базе данных H2 (то есть по окончании работы приложения данные пропадут). Такое приложение очень удобно использовать для экспериментов при изучении технологии Hibernate.
Создание приложения с помощью Spring Initializr
Перейдите на сайт Spring Initializr, выберете/заполните в левой части экрана параметры приложения по своему усмотрению. Обычно достаточно оставить все параметры как они есть (может быть стоит выбрать имя проекта). В нижней части левой половины экрана выбрана та или иная версия джавы. Нужно убедится, что у вас в системе версия такая же или более новая.
В правой части экрана нажмите на кнопку ADD DEPENDENCIES…
В появившемся меню выберите два пункта (удерживайте ctrl при выборе пункта меню, если не хотите, чтобы меню закрывалось после каждого клика):
- Spring Data JPA
- H2 Database
Больше ничего выбирать не нужно. Провайдером персистентности, имплементирующим спецификацию JPA, в Spring по умолчанию является Hibernate. Он будет стянут в проект в качестве транзитивной зависимости.
Затем нажмите кнопку GENERATE
Браузер автоматически скачает архив с проектом, который можно открыть в любой IDE. В проекте помимо различный служебных файлов сборщиков (Maven’а или Gradle’а) будут следующие джава файлы:
Названия файлов могут отличаться в зависимости от того, как был назван проект в настройках Spring Initializr’а. Первый файл (не тестовый) будет иметь следующее содержание:
1 2 3 4 5 6 7 8 9 10 11 12 |
package com.example.demo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } } |
В подобных случаях приложение запускают прямо из IDE, нажимая на кнопку play.
Свой код можно добавить либо сразу после вызова метода SpringApplication.run(..), либо создать в этом же классе метод, возвращающий бин ApplicationRunner с инструкциями для выполнения. Метод SpringApplication.run(..) запустит выполнение этих инструкций сразу после создания контекста. В случае веб-приложения эти инструкции будут выполнены до фактического запуска приложения в контейнере.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
package com.example.demo; import org.springframework.boot.ApplicationRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; @SpringBootApplication public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } @Bean public ApplicationRunner run() { return args -> { System.out.println("Hello from Spring Boot application"); }; } } |
Обратите внимание, что вывод в консоль из такого бина часто смешивается с другим служебным выводом Spring’а и происходит параллельно с процессом старта всего приложения:
Если в процессе обучения и экспериментов мы полагаемся на такой вывод, то надо помнить, что он может теряться среди других строк. Кроме того, обратите внимание, что если вы привыкли, что Spring Boot приложение — это всегда веб-приложение, запущенное в Tomcat или другом сервере, то в данном случае это самое обычно консольное приложение, которое закончит свою работу, как только достигнет последней инструкции.
Эксперименты с Hibernate
Теперь давайте проведём первый опыт с Hibernate. Для этого добавим в проект класс сущности Person:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; @Entity public class Person { @Id @GeneratedValue private Long id; private String name; private Integer age; //Конструкторы, геттеры и сеттеры, toString() и т.д. } |
Далее добавим бин репозитория для этой сущности:
1 2 3 4 5 6 |
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface PersonRepository extends JpaRepository <Person, Long> { } |
Обратите внимание, что бин является интерфейсом, а не классом. Имплементация для данного интерфейса будет создана автоматически.
Воспользоваться этим репозиторием можно, модифицировав метод public ApplicationRunner run() следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
@Bean public ApplicationRunner run(PersonRepository personRepository) { return args -> { Person irina = new Person("Irina", 29); Person vladimir = new Person("Vladimir", 30); personRepository.save(irina); System.out.println("Persist person: " + irina); personRepository.save(vladimir); System.out.println("Persist person: " + vladimir); List<Person> fetchResult = personRepository.findAll(); System.out.println("Persisted persons: " + fetchResult); }; } |
Обратим внимание на параметр метода run(). В него мы передаём объявленный ранее бин PersonRepository. Spring сам найдёт этот бин у себя в контексте и передаст его в качестве параметра. Таким образом мы можем передать в наш код любое количество бинов для экспериментов с ними внутри метода run(). Хотя обычно, конечно же, их количество не превышает одного-двух.
Кроме того, возможно, что над классом приложения понадобится добавить следующие аннотации:
1 2 3 4 5 |
@EnableJpaRepositories("com.example.demo") @EntityScan("com.example.demo") @SpringBootApplication public class DemoApplication { ... |
В них мы указываем приложению, где лежат сущности и репозитории. Но в последних версиях Spring’а этого обычно не требовалось.
Если метод toString() класса Person был переопределён должным образом, то вывод приложения будет следующим (вывод этих инструкций может быть перемешан со служебным выводом Spring’а):
1 2 3 |
Persist person: Person{id=1, name='Irina', age=29} Persist person: Person{id=2, name='Vladimir', age=30} Persisted persons: [Person{id=1, name='Irina', age=29}, Person{id=2, name='Vladimir', age=30}] |
Таким образом, мы увидели следующее:
- Мы никак не конфигурировали базу данных, мы даже нигде явно не указывали, что будем пользоваться бд, развёрнутой в оперативной памяти. Тот факт, что бд H2 указана в качестве зависимости в проекте и проект не содержит никаких конфигураций, заставил Spring создать дефолтную конфигурацию, которая развёртывает базу в памяти, использует её по умолчанию, не требуя ни логина, ни пароля.
- Spring создал имплементацию интерфейса PersonRepository, которая содержала стандартные методы, необходимые для работы с сохранением состояния, в том числ методы save() и findAll(), которые мы попробовали.
Основной недостаток использования ApplicationRunner заключается в том, что прорабатывая новую тему, мы, как правило, затираем код, оставшийся от предыдущих экспериментов. Таким образом у нас не только данные долго не живут, но и наши упражнения тоже. Тогда как время от времени возникает необходимость вернуться к старым записям, чтобы освежить в памяти те или иные моменты.
Поэтому, если практиковаться нужно много и есть желание не терять проделанные упражнения, то для этих упражнений разумнее использовать тесты. Автоматически сгенерированный проект содержит папку с тестами и даже один тестовый класс DemoApplicationTests (название класса зависит от названия проекта).
Давайте перенесём наш эксперимент с репозиторием в тестовый класс:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import java.util.List; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsInAnyOrder; @SpringBootTest class DemoApplicationTests { @Autowired PersonRepository personRepository; @BeforeEach void cleanTable() { personRepository.deleteAll(); } @Test void personRepositoryIsAvailableAndWorks() { Person irina = new Person("Irina", 29); Person vladimir = new Person("Vladimir", 30); personRepository.save(irina); System.out.println("Persist person: " + irina); personRepository.save(vladimir); System.out.println("Persist person: " + vladimir); List<Person> fetchResult = personRepository.findAll(); System.out.println("Persisted persons: " + fetchResult); assertThat("All the persons are fetched from db", fetchResult, containsInAnyOrder(irina, vladimir)); } } |
Мы заавтоварили бин PersonRepository в класс теста и он стал доступен во всех тестовых методах. Затем мы создали метод personRepositoryIsAvailableAndWorks(), в котором проверили работоспособность репозитория. Мы как и раньше выводили результаты в консоль. Кроме того, мы написали ассерт.
В общем случае, когда мы просто экспериментируем с новыми технологиями, то особой необходимости писать ассерты нет. Мы просто выводим результаты в консоль, где их анализируем.
Таким образом на каждый интересный момент изучаемой технологии, с которым мы хотим поиграться и поэкспериментировать, мы можем создавать отдельный тестовый метод (и группировать их в отдельные классы). И нет никакой необходимости затирать код предыдущих экспериментов.
Такой проект это хорошая учебная тетрадь. Так как изучать технологии и тем более Hibernate и не прорабатывать их написанием кода — пустая трата времени.