Model (Entity) с Hibernate

  • Рубрика записи:Spring back-end

В проекте Spring обязательно присутствует папка model. Она содержит в себе классы-сущности (entity). Без них никуда – это кирпичики back-end-приложения. Именно они представляют собой данные. Для удобного создания сущностей используется фреймворк Hibernate. Будем разбираться, что к чему. Приступим к наполнению первой из папок структуры Spring-проекта!

Что такое entity

В структуре Spring-проекта, entity – это сущность в контексте работы с БД, это сырые данные. Сущность – это объект, который отражён в базе данных. Сущностью может быть книга (класс-сущность Book) со своим названием, автором, годом издания (поля String title, String author, int publishYear). Это может быть пользователь (класс User) или статья (класс Article). И многие подобные понятия. Так проще представлять данные и оперировать ими. В общем, данные без поведения, так что никакого ООП.

В общем говоря, сущность – это класс, где хранятся данные. Причём эти классы соответствуют таблицам в базе данных. Один класс – одна таблица. Один объект этого класса – одна строка из соответствующей таблицы.

Но как данные из таблиц привести к объектам? Успокойтесь, нам не нужно это делать ручками. Ведь у нас есть Hibernate!

Базовые аннотации Hibernate

Hibernate – это фреймворк для работы с базами данных в среде Java. Он представляет данные в виде объектов Java. Главная цель Hibernate – облегчить и ускорить процесс разработки приложений, связанных с базами данных.

И это отлично. Это помогает программистам-разработчикам бэк-энда жить. Можно удобно прописать классы-сущности вместо многословного кода конвертаций туда-сюда, который включал бы ещё и написание запросов SQL. Да, вы правильно понимаете: нам не придётся писать никакого SQL-кода, хоть мы и работаем с базами данных!

Давайте рассмотрим базовые аннотации, которые предоставляет нам Hibernate.

Entity

Во-первых, каждая сущность должна быть помечена аннотацией @Entity:

Так ваш Spring-проект поймёт, что этот класс есть сущность, и будет обращаться с его объектами соответствующе.

Table

В JPA и Hibernate по умолчанию имя таблицы формируется на основе имени класса, но оно приводится к стандартному формату базы данных. В разных СУБД может быть разное поведение. Обычно в большинстве баз данных имена таблиц приводятся к нижнему регистру. Если класс назван User, то таблица будет названа user.

Если же вы хотите указать другое название таблицы, нужно указать его явно при помощи аннотации @Table:

Id и GeneratedValue

Теперь приступим к содержимому класса. У таблиц, как правило, есть уникальный столбец – первичный ключ – идентификатор. Для сущности Hibernate он вообще обязателен. И задаётся он аннотацией @Id перед полем класса. Она помечает, что поле является идентификатором. Также прописывается, что это поле генерируется автоматически (аннотация @GeneratedValue). Эта аннотация используется, чтобы сказать, как именно нужно генерировать значения для первичного ключа сущностей (а можно по-разному).

По умолчанию стоит GenerationType.AUTO. Часто используется GenerationType.IDENTITY. Но давайте поговорим о всех способах генерации уникальных значений, об их отличиях и где они используются.

GenerationType.AUTO. Часто является хорошим выбором, так как Hibernate самостоятельно выбирает стратегию генерации, опираясь на базу данных. Обычно используется автоинкрементируемый столбец (т.е. аналогично действию GenerationType.IDENTITY).

GenerationType.IDENTITY. Использует автоинкрементируемый столбец в базе данных. Этот подход часто используется с СУБД, поддерживающими автоинкремент. Однако, может вызвать проблемы в случае, если требуется сохранение и получение идентификаторов до фактической вставки записей.

GenerationType.SEQUENCE. Использует последовательность базы данных (если она поддерживается). Требует определения генератора последовательности.

GenerationType.TABLE. Использует таблицу базы данных для хранения и управления значениями идентификаторов. Как правило, медленнее в сравнении с другими стратегиями, так как таблица генерации требует дополнительных запросов. Может быть полезен, если в базе данных не поддерживаются последовательности.

Column

Как и в случае с названием класса, проект по умолчанию считает, что столбцы в таблице называются так же, как и поля класса. Если это не так, можно задать другие названия столбцов, соответствующих полям класса:

Temporal

Наверняка рано или поздно вам понадобится хранить дату и время в базе данных. Тогда пригодится и аннотация @Temporal:

Она указывает, каким именно образом нужно хранить дату/время. По умолчанию – TemporalType.TIMESTAMP. TemporalType может принимать 3 значения:

  • TemporalType.TIMESTAMP – Значение TIMESTAMP используется для сохранения полной метки времени, включая и дату, и время. Это наиболее общий случай и используется, когда важны как дата, так и время.
  • TemporalType.DATE – Значение DATE используется, когда вам нужно сохранить только дату без времени. Например, если у вас есть поле, представляющее дату рождения, и вам не важен конкретный момент времени в этот день.
  • TemporalType.TIME – Значение TIME используется, когда вам нужно сохранить только время без даты. Например, если у вас есть поле, представляющее время проведения события, и вам не важна дата.

Transient

Последняя базовая аннотация Hibernate, которую мы рассмотрим, это @Transient. Аннотация @Transient используется для пометки поля, которое не должно быть сохранено в базе данных. Если поле помечено как @Transient, Hibernate проигнорирует его при выполнении операций CRUD (create, read, update, delete). Это может быть полезно, например, для временных или вычисляемых полей, которые не должны быть отражены в таблице базы данных.

Требования к entity

Нельзя просто взять и пометить класс как @Entity. Ему ещё надо соответствовать определённым требованиям. Ваша среда разработки даже для некоторых требований будет подсказывать, подчёркивать класс красным, мол, не соответствует.

Итак, требования к классу-сущности:

  1. Аннотация @Entity. Класс, который представляет сущность, должен быть помечен аннотацией @Entity. Это указывает Hibernate, что этот класс представляет объект, который может быть сохранен в базе данных.
  2. Присутствие первичного ключа (@Id). У сущности должно быть поле, которое является первичным ключом. Это поле должно быть помечено аннотацией @Id.
  3. Пустой конструктор. Класс сущности должен иметь пустой конструктор. Hibernate использует его для создания объектов при чтении из базы данных.
  4. Геттеры и сеттеры для всех полей. Чтобы Hibernate мог правильно установить значения полей и получить их при работе с объектами, должны быть предоставлены геттеры и сеттеры для всех полей сущности.
  5. Соответствие типов данных. Типы полей сущности должны соответствовать типам данных в базе данных. Если у вас в столбце name в БД хранится строка, вы не можете в классе написать поле Integer name.

Соблюдайте эти правила и всё будет хорошо.

Между делом, всякие конструкторы, геттеры и сеттеры можно прописывать не ручками и даже не автоматической генерацией кода среды разработки. Нет. Можно сделать так, чтобы этот код не мозолил глаза и не отвлекал от главного. А делается это при помощи библиотеки Lombok. Например, одна аннотация @Getter над классом – и у всех полей есть геттеры. Одна аннотация @Setter – … Ну, вы сами догадались.

Аннотации связей между таблицами

Куда же без связей между таблицами? Ведь каждый мало-мальски серьёзный Spring-проект имеет таблицы, ссылающиеся на другие таблицы. Как нам в этом деле поможет Hibernate?

Сначала узнаем про параметры, которые постоянно используются при различных связях. Это атрибуты cascade и fetch.

Cascade

Параметр cascade (каскадирование) определяет, нужно ли распространять всякие операции текущей сущности на помечаемое поле. Попроще: у вас сущность собака Dog, которая хранит у себя в поле ссылку на связанную сущность хозяина Owner. Если вы в этом поле хозяину поменяете имя, а потом сохраните сущность Dog, то таблица dog в БД в любом случае обновится, но обновлять ли таблицу owner?

Именно за это отвечает cascade. И вот какие значения у него есть:

  • По умолчанию – если параметр cascade не задан, тогда каскадирование не применяется вовсе, и в случае с собакой и хозяином таблица owner никак бы не изменилась, хотя в сущности собаки имя хозяина и было изменено.
  • CascadeType.ALL – самая частая вместе с предыдущей. Каскадирование всех операций: обновления свежими данными из базы (REFRESH), создания (PERSIST), удаления (REMOVE), сохранения сущностей (MERGE), а также отсоединения сущностей друг от друга (DETACH). В случае с собачкой обновится и таблица owner тоже, хотя напрямую запрос на обновление сущности Owner послан не был (а только обновление сущности Dog, которая имеет ссылку на Owner).
  • CascadeType.REFRESH – каскадирование обновления данных сущностей свежей информацией из базы.
  • CascadeType.PERSIST – каскадирование создания новых сущностей.
  • CascadeType.REMOVE – каскадирование удаления сущностей.
  • CascadeType.MERGE – каскадирование сохранения изменений сущностей в базе данных.
  • CascadeType.DETACH – каскадирование отсоединения сущностей друг от друга. Например, если у собачки Dog поставить другого хозяина Owner в поле, то в базе данных предыдущий хозяин должен отсоединиться, больше не ссылаться на эту собаку.

Эти значения можно использовать как по одному:

Так и сразу массивом:

Fetch

Параметр fetch определяет, как информация из связанных сущностей будут извлекаться из базы данных: сразу при получении этой сущности, или попозже.

  • FetchType.LAZY означает отложенную загрузку данных. Например, когда объект Dog загружается, связанный с ним объект Owner не будет автоматически загружен из базы данных. Вместо этого данные Owner будут загружены только тогда, когда вы явно обратитесь к полю owner.
  • FetchType.EAGER означает мгновенную (жадную) загрузку данных. Связанный объект (поле owner) будет загружен сразу же вместе с родительским объектом (объектом класса Dog) при запросе.

Значение по умолчанию (если не задано) для fetch – это FetchType.LAZY для связей @OneToMany и @ManyToMany, и FetchType.EAGER для связей @OneToOne и @ManyToOne. Это логично: когда надо подгружать только один объект, можно это сделать и сразу. Но когда там целое множество объектов – не, давайте загрузим только при необходимости.

С cascade и fetch более-менее разобрались. Можно будет их применять ниже, не так сильно удивляясь, что происходит в коде. Теперь давайте рассмотрим использование Hibernate во всех трёх ситуациях связей: один к одному, один ко многим и многие ко многим.

One to one

Один к одному – то есть каждой одной сущности соответствует единственная другая, и наоборот. Например, каждому пользователю User соответствует его собственный зашифрованный пароль EncryptedPassword, который хранится в другой таблице. В таблице user есть столбец password_id, который указывает на первичный ключ таблицы encrypted_password.

Вы можете хранить в объектах обоих сущностей ссылки друг на друга, как в примере ниже. А можете и только в одной: например, чтобы класс User имел поле encryptedPassword, но класс EncryptedPassword не имел поля user.

Причём из-за параметра cascade = CascadeType.ALL при сохранении сущности User, изменения в поле password также будут сохраняться в таблицу паролей.

Так как параметр fetch не указан, то по умолчанию для связи @OneToOne он равен FetchType.EAGER – жадная загрузка, то есть данные пароля подгружаются в объект пользователя сразу же. И наоборот.

One to many

Один ко многим – это связь, когда одному объекту определённой сущности соответствует множество объектов другой сущности. Например, сущности запись Post и комментарий Comment. Одной записи соответствует множество комментариев. Хранить данные о связи в БД будет именно таблица comment. У неё столбец post_id – идентификатор записи, к которой принадлежит комментарий.

В коде это выражается так (опять же, здесь обе сущности имеют ссылки друг на друга, но это не всегда обязательно):

Здесь параметр cascade по умолчанию пуст. Это означает, что изменения, сделанные в поле post объекта Comment, не повлияют на таблицу post. Чтобы отразить изменения в таблице post, придётся явно взять объект post и запросить для него сохранение изменений. А не сделать это через сохранение объекта Comment.

Many to many

Самая сложная связь – это многие ко многим. Означает это, что каждому объекту первой сущности может соответствовать множество объектов второй сущности, и в то же время каждому объекту второй сущности может соответствовать множество объектов первой сущности.

Рекомендуется минимизировать её использование по возможности, так как с другими двумя связями управляться попроще. Но не всегда возможно избежать использования many to many.

Итак, возьмём за пример студентов и курсы. Каждый студент может посещать множество курсов. Вместе с тем, каждый курс может посещаться множеством студентов. Many to many налицо. Получается, есть таблица student и таблица course со своими первичными ключами id. Но двумя таблицами тут не обойтись. Нужна третья таблица-посредник, называемая student_course. Она будет хранить соответствия между студентами и курсами. У неё будут столбцы student_id и course_id.

Создание классов сущностей

Итак, в статье про настройку БД и её подключение к проекту мы создали 2 таблицы: users и posts. Теперь на основе них будем создавать классы-сущности.

Таблица users в базе данных, соответствующая классу-сущности User при конвертации с помощью Hibernate

Вот так выглядят столбцы таблицы users. Их всего 4, и на основе их названий и типов данных мы создадим класс User. Пока что просто запишем туда эти поля, без создания связи с таблицей posts.

С помощью аннотаций Hibernate и сил библиотеки Lombok удалось создать такой ёмкий класс User:

Благодаря аннотациям @Data и @NoArgsConstructor не нужно писать геттеры, сеттеры, конструкторы, а также перегружать методы equals(), hashCode() и toString(). Так как поля называются ровно так же, как и столбцы, нет нужны помечать их аннотациями @Column и дополнительно указывать название столбца.

Теперь создадим класс для сущности Post, таблица posts. Пока что также не будем прописывать связь с таблицей users: просто поля, соответствующие столбцам таблицы в базе данных.

Таблица posts в базе данных, соответствующая классу-сущности Post при конвертации с помощью Hibernate

Итак, класс Post:

Между делом, аннотация @Temporal здесь указана для большей явности. На самом деле, так как у @Temporal значение по умолчанию и так TIMESTAMP, можно было не указывать это явно.

А теперь давайте добавим связь между этими двумя сущностями. Пользователь User связан с записью Post связью one to many – один ко многим. Один пользователь может иметь множество постов (записей). Давайте сделаем так, чтобы и пользователь, и запись знали друг о друге:

Вот и готовы наши сущности User и Post. И они друг о друге знают.

Кстати говоря, из-за такой вот взаимной связи, может возникать рекурсия и бум – StackOverflowError, одна из иерархии ошибок и исключений в Java. Объекты будут постоянно подгружать друг друга до бесконечности, так и не отдав ответ серверу. Для нейтрализации такого эффекта либо выдают ссылку на другую сущность только одному из двух классов, либо используют секретный приём в виде DTO и mapper-ов. Но это – уже совсем другая история.

Добавить комментарий