Работа с БД через Hibernate
В предыдущей главе мы разобрали, какие вообще технологии есть в хранении данных. В этой главе разберем наиболее популярную технологию именно в Java-мире: фреймворк Hibernate.
Hibernate - это очень продвинутый и навороченный ORM-фреймворк. Он берет на себя всю работу по превращению ваших Java-объектов в таблицы и запросы реляционной базы данных. Все что вам надо для начала - это описать ваши сущности, необходимые запросы и задать несколько свойств подключения к БД, таких как адрес, логин/пароль, тип БД (как уже говорилось, реляционных БД много разных, для каждой есть свой диалект языка SQL).
В этой главе мы используем возможности модуля spring data JPA, полная документация по нему доступна по ссылке https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#reference
Настройки подключения к БД
Для того, чтобы использоваться все высокоуровневые фичи, описанные ниже, достаточно следующих шагов:
В зависимостях проекта подключить spring-boot-starter-data-jpa
Добавить аннотацию @EnableJpaRepositories на ваш класс конфигурации (помеченный как @SpringBootApplication или @Configuration)
В настройках проекта (application.yml) прописать параметры подключения к БД. В случае с MySQL базой на локальном хосте это будет что-то вроде:
spring: datasource: # JDBC ссылка на подключение к БД url: jdbc:mysql://localhost:3306/test_db # Класс JDBC драйвера для работы с нашей БД driver-class-name: com.mysql.cj.jdbc.Driver # Логин и пароль (если нужны) username: user password: 12345 jpa: # Диалект SQL, поддерживаемый нашей БД database-platform: org.hibernate.dialect.MySQL8Dialect hibernate: # Что делать, если структура БД отличается от структуры классов в нашем коде # update означает что надо переделать структуру БД # Другие значения - create (создать БД если ее нет, но не менять если есть) и # none - не делать ничего (чтобы не попортить уже лежащие в БД данные) и выдать ошибку ddl-auto: update
Описание сущностей с помощью JPA аннотаций
Как рассказывалось в предыдущей главе, JPA - это стандартный способ описания структуры сущностей в БД. Вы создаете класс, который будете хранить, а затем помечаете его поля аннотациями, подсказывающими как именно его хранить.
Все аннотации находятся в пакете javax.persistence.
Рассмотрим такой пример. Пусть мы делаем сервис для хранения своего вишлиста. В списке у нас есть товары, у которых есть описание, цена и ссылка на магазин. Такой объект можно представить в БД как:
@Entity // главная аннотация, говорящая что это объект для сохранения в БД
class WishlistEntry {
@Id // это поле - уникальный идентификатор сущности
@GeneratedValue // это поле генерируется СУБД автоматически
public long id;
// для простых полей можно не указывать аннотации, тогда Hibernate сам создаст
// для них столбец в БД с типом и параметрами по умолчанию
String url;
long price;
// По умолчанию длина столбца в БД для строк равна 255, что может быть маловато
// для описания. Дадим подсказку, что это поле должно иметь длину в 1024 символа
@Column(length=1024)
String description;
}
Вуаля, этого достаточно, чтобы Spring на пару с Hibernate на старте приложения нашли этот класс, прочитали аннотации, залезли в БД, создали там таблицу с четырьмя столбцами и подготовили все необходимые select/insert/delete и прочие запросы.
Для более сложных случаев есть множество других аннотаций (например, для задания отношений между объектами, создания дополнительных индексов и вторичных ключей и т.п.), полный список можно найти в документации по JPA и во множестве статей в интернете, например https://thorben-janssen.com/key-jpa-hibernate-annotations/
Репозитории
Основы
Окей, мы создали класс в Java и Hibernate создал нам пустую таблицу в БД. Что делать дальше, как нам что-то сохранить и что-то прочитать из нашей базы?
Hibernate предоставляет множество низкоуровневых механизмов работы с объектами (там и до голой JDBC можно добраться), однако на высоком уровне проще и приятнее работать с т.н. репозиториями.
Репозиторий это интерфейс, помеченный специальной аннотацией, отнаследованный от базового класса Repository
и содержащий методы для сохранения и загрузки объектов из БД. Используются именно интерфейсы, так как нам не надо писать в них реализацию этих методов. Все что от нас требуется - описать что мы хотим сделать, а Spring Boot и Hibernate под капотом за нас сгенерируют весь необходимый код того, как именно это сделать.
В модуле spring-data-jpa уже есть набор различных базовых интерфейсов репозиториев, от которых можно отнаследовать свой репозиторий и сразу получить готовую реализацию типовых методов.
Например, если все что нам нужно - типичные CRUD операции (создание, удаление, изменение, выборка по первичному ключу, выборка всех объектов в таблице), то нам вообще не надо будет ничего писать, достаточно отнаследоваться от JpaRepository
, где все это уже есть:
// JpaRepository имеет два параметра: класс нашей сущности и тип первичного ключа в ней (поля помеченного @Id)
@Repository
interface WishlistEntryRepository extends JpaRepository<WishlistEntry, Long> {
}
Вот и все.
Наш репозиторий является бином в контексте приложения, мы можем получать доступ к нему из других классов с помощью @Autowired аннотации. И затем использовать методы для работы с БД:
@Service
class WishListService {
@Autowired
public WishlistEntryRepository repository;
void createEntry(String descr, String url, long price) {
WishlistEntry entry = new WishlistEntry()
// ... выставляем поля нашего нового объекта
repository.save(entry); // все что нужно, чтобы сохранить его в БД
}
WishlistEntry findEntry(long id) {
return repository.findById(id); // все что нужно чтобы выполнить SELECT запрос к БД по id
}
}
Дополнительные методы
Если нам недостаточно стандартных операций, полученных нами из JpaRepository, мы можем дополнительно определить в репозитории собственные запросы. Для этого есть два варианта.
В случае простых выборок можно использовать магию с именами методов. Все что нам нужно - это создать в репозитории метод, чье имя будет содержать в себе запрос. Например, если мы хотим сделать выборку по точному значению поля "цена" то такой запрос будет выглядеть как
@Repository
interface WishlistEntryRepository extends JpaRepository<WishlistEntry, Long> {
WishlistEntry findByPrice(long price);
}
Имя метода в таком случае начинается с find, затем идет перечисление полей объекта (их может быть несколько, в таком случае нужно использовать and, например findByPriceAndUrl) и ограничений на них (равно, не равно, больше, меньше и т.п.). Полный список того, какие именно запросы можно писать через имена методов, доступен в документации https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods.query-creation
Возвращаемые значения такого метода могут быть разные:
Если возвращаемый тип - это объект, то вернется либо он (если в БД нашлась хотя бы одна запись, подходящая под условия, если их несколько - вернется первая), либо null
Можно использовать Optional<тип> чтобы избегать nullов.
Можно возвращать список - тогда вернутся все объекты, удовлетворяющие условию, либо пустой список если их нет.
Можно использовать спринговый класс Pageable для диапазонов.
Написание запросов вручную
Если по какой-то причине нас не устроило написание запросов путем задания имен методов, или нас не устраивает тот запрос, который генерирует Hibernate, мы можем создавать запросы вручную.
В таком случае мы точно так же определяем метод в нашем классе репозитория, но теперь название у него может быть любое, затем помечаем его аннотацией, в которой пишем сам запрос.
@Repository
interface WishlistEntryRepository extends JpaRepository<WishlistEntry, Long> {
@Query("select e from WishlistEntry e where e.price < :maxPrice")
List<WishlistEntry> findCheaper(@Param("maxPrice") long maxPrice);
}
Параметры метода надо аннотировать с помощью @Param
, затем их имя можно использовать внутри текста запроса.
По умолчанию запросы пишутся на языке HQL, который затем обрабатывается Hibernate и превращается в нативный запрос в зависимости от выбранного диалекта БД. Но у аннотации @Query
есть параметр nativeQuery
, если его выставить - тогда запрос будет трактоваться именно как нативный, и будет отправляться в СУБД без изменений именно в таком виде:
@Repository
interface WishlistEntryRepository extends JpaRepository<WishlistEntry, Long> {
// тот же самый запрос на голом SQL
@Query("select * from WishlistEntry where price < :maxPrice", nativeQuery=true)
List<WishlistEntry> findCheaper(@Param("maxPrice") long maxPrice);
}
При использовании нативных запросов вы теряете гибкость. Вы больше не сможете прозрачно для вашего кода заменить СУБД, так как разные СУБД имеют разные разновидности языка SQL, и их запросы несовместимы друг с другом. С другой стороны, как и любой стандарт, пытающийся объединить много разных технологий, HQL является неким минимальным общим подмножеством всех разновидностей SQL, и не позволяет использовать вашу конкретную СУБД максимально эффективно.
Заключение
Итак, подведем итоги главы. Мы выяснили, что Hibernate - ORM фреймворк, использующий технологию JPA для описания сущностей в БД. Для добавления работы с БД в свой Spring Boot проект вам надо:
Подключить зависимость - модуль spring-boot-data-jpa
Настроить параметры подключения к БД в конфиге
Создать классы сущностей, проставить на них аннотации
Создать классы репозиториев, прописать недостающие необходимые методы для выборок в них
Получить экземпляр нужного репозитория через стандартные механизмы спринга (@Autowired) и использовать его методы для сохранения и загрузки объектов
В простых случаях вам вообще не надо думать о том, как ваши объекты будут располагаться в БД, как будут выглядеть запросы. Вы работаете только с Java-кодом и аннотациями.
По этой причине в большинстве случаев нет нужды глубоко изучать SQL или вашу СУБД. Однако есть такая вещь, как "закон протекающих абстракций", хорошо описанный в статье Джоэля Спольски https://habr.com/ru/company/selectel/blog/512796/. Время от времени вы будете сталкиваться с проблемами, когда Java-код вроде написан правильно, а запросы к БД люто тормозят. Потому что какие-то действия, правильные на верхнем уровне, приводят к неоптимальностям "под капотом", внутри реализации. И хотя фреймворки высокого уровня, как Hibernate, изо всех сил детали реализации от вас прячут, иногда все-таки приходится в них залезать и разбираться. Поэтому базовые знания SQL, оптимизации и настройки БД вам все-таки пригодятся в дальнейшем.
Last updated