Работа с БД через Hibernate

В предыдущей главе мы разобрали, какие вообще технологии есть в хранении данных. В этой главе разберем наиболее популярную технологию именно в Java-мире: фреймворк Hibernate.

Hibernate - это очень продвинутый и навороченный ORM-фреймворк. Он берет на себя всю работу по превращению ваших Java-объектов в таблицы и запросы реляционной базы данных. Все что вам надо для начала - это описать ваши сущности, необходимые запросы и задать несколько свойств подключения к БД, таких как адрес, логин/пароль, тип БД (как уже говорилось, реляционных БД много разных, для каждой есть свой диалект языка SQL).

В этой главе мы используем возможности модуля spring data JPA, полная документация по нему доступна по ссылке https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#reference

Настройки подключения к БД

Для того, чтобы использоваться все высокоуровневые фичи, описанные ниже, достаточно следующих шагов:

  1. В зависимостях проекта подключить spring-boot-starter-data-jpa

  2. Добавить аннотацию @EnableJpaRepositories на ваш класс конфигурации (помеченный как @SpringBootApplication или @Configuration)

  3. В настройках проекта (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 проект вам надо:

  1. Подключить зависимость - модуль spring-boot-data-jpa

  2. Настроить параметры подключения к БД в конфиге

  3. Создать классы сущностей, проставить на них аннотации

  4. Создать классы репозиториев, прописать недостающие необходимые методы для выборок в них

  5. Получить экземпляр нужного репозитория через стандартные механизмы спринга (@Autowired) и использовать его методы для сохранения и загрузки объектов

В простых случаях вам вообще не надо думать о том, как ваши объекты будут располагаться в БД, как будут выглядеть запросы. Вы работаете только с Java-кодом и аннотациями.

По этой причине в большинстве случаев нет нужды глубоко изучать SQL или вашу СУБД. Однако есть такая вещь, как "закон протекающих абстракций", хорошо описанный в статье Джоэля Спольски https://habr.com/ru/company/selectel/blog/512796/. Время от времени вы будете сталкиваться с проблемами, когда Java-код вроде написан правильно, а запросы к БД люто тормозят. Потому что какие-то действия, правильные на верхнем уровне, приводят к неоптимальностям "под капотом", внутри реализации. И хотя фреймворки высокого уровня, как Hibernate, изо всех сил детали реализации от вас прячут, иногда все-таки приходится в них залезать и разбираться. Поэтому базовые знания SQL, оптимизации и настройки БД вам все-таки пригодятся в дальнейшем.

Last updated