JavaRush/Java блог/Random/JPA Entities and DB Relationships
Nikita Koliadin
40 уровень

JPA Entities and DB Relationships

Статья из группы Random
участников

JPA Entities && DB Relationships

Доброго времени суток, коллеги!
JPA Entities and DB Relationships - 1
Данный материал рассчитан на тех, кто уже имеет представление об организации баз данных (дальше просто DB - "Database"), минимальные знания о том, как работает Object-Relational Mapping (дальше просто ORM), и его реализациях, таких как Hibernate / JPA. Если вы не знакомы с этим, советую начать с JDBC, и только потом переходить к ORM-модели. Я предупредил, и ответственность за вашу психику после прочтения данной статьи без должной подготовки не несу! :) Начнем разбираться со всем по порядку. Во-первых, мы немного копнем в теорию, совсем чутка. Во-вторых, мы разберемся как этот shit проделать во всеми любимой Java. Также мы напишем с вами некий проект-шпаргалку, который закрепит наше понимание темы и послужит шаблоном КАК надо делать mapping. Итак, Let's do it!

What is Entity?

Сущность (Entity) — это некий объект из реальной жизни (например, машина), который имеет атрибуты (двери, КОЛЁСА, двигатель). DB Entity: в этом случае наша сущность хранится в DB, все просто. Зачем и каким образом мы засунули машину в базу данных — рассмотрим позже.

What is DB Relationships?

Давным давно, в тридевятом королевстве была создана реляционная DB. В этой DB данные представлялись в виде табличек. Но и ослу из Шрека было понятно, что нужно было сделать механизм взаимосвязи данных таблиц. В итоге появилось 4 DB relationships:
  1. One-to-One
  2. One-to-Many
  3. Many-to-One
  4. Many-to-Many
Если вы все это видите впервые, предупреждаю еще раз — дальше будет хуже: задумайтесь о том, чтобы пойти погулять. Все эти отношения мы разберем на примере, и поймем разницу между ними.

Horror Example

У нас будет один проект у которого будет 5 branches: master, где будет описание проекта, и по 1 branch на каждую DB relationship. В каждой branch'e будут SQL скрипты создания DB и ее заполнение тестовыми данными, плюс Entity класс с annotation mapping'ом. Также будет для каждой branch'и Hibernate config файл. Я буду использовать H2 embeded DB для проекта, чтобы не отвлекаться на отдельные моменты облачных DB или внешних DB. Перейдя по ссылке, установите H2 DB себе на пылесос. Я опишу каждый шаг на 1 branch'e, остальные — лишь ключевые моменты. В конце мы подведем итоги. Поехали. This ссылка на master branch моего проекта.

One-to-One Relationship

Ссылка на branch тут.
  1. Нужно подключить H2 DB к нашему проекту. Здесь нужно подчеркнуть то, что нам нужна Ultimate IDEA для комфортной работы с DB и другими вещами. Если она у вас уже есть тогда идем непосредственно к подключению DB. Заходим в tab Database и делаем как на скрине:

    JPA Entities and DB Relationships - 2

    Далее переходим к настройкам DB. Вы можете ввести свои данные, и даже свою СУБД, повторюсь, H2 DB я использую для простоты.

    JPA Entities and DB Relationships - 3

    Далее настроим схему. Этот шаг не обязателен но желателен, если у вас несколько схем в DB.

    JPA Entities and DB Relationships - 4

    Применяем настройки, и в итоге у нас должно получиться что то вроде этого:

    JPA Entities and DB Relationships - 5
  2. Базу данных мы создали, и настроили доступ к ней из IDEA. Теперь нужно создать таблички в ней и заполнить какими-то данными. Для примера я возьму две сущности: Author и Book. У книги может быть автор, может быть несколько авторов, а может и не быть. На этом примере мы создадим все виды связей. Но в данном пункте — One-to-One relationship. Создадим соответствующий скрипт, который создает DB Tables:

    DROP TABLE IF EXISTS PUBLIC.BOOK;
    
    CREATE TABLE PUBLIC.BOOK (
      ID         INTEGER      NOT NULL AUTO_INCREMENT,
      NAME       VARCHAR(255) NOT NULL,
      PRINT_YEAR INTEGER(4)   NOT NULL,
      CONSTRAINT BOOK_PRIMARY_KEY PRIMARY KEY (ID)
    );
    
    DROP TABLE IF EXISTS PUBLIC.AUTHOR;
    
    CREATE TABLE PUBLIC.AUTHOR (
      ID          INTEGER      NOT NULL AUTO_INCREMENT,
      FIRST_NAME  VARCHAR(255) NOT NULL,
      SECOND_NAME VARCHAR(255) NOT NULL,
      BOOK_ID     INTEGER      NOT NULL UNIQUE,
      CONSTRAINT AUTHOR_PRIMARY_KEY PRIMARY KEY (ID),
      CONSTRAINT BOOK_FOREIGN_KEY FOREIGN KEY (BOOK_ID) REFERENCES BOOK (ID)
    );

    И выполним его:

    JPA Entities and DB Relationships - 6

    Результат выполнения в консоле:

    JPA Entities and DB Relationships - 7

    Результат в DB:

    JPA Entities and DB Relationships - 8
  3. Давайте посмотрим на диаграмму наших таблиц. Для этого ПКМ на нашу DB:

    JPA Entities and DB Relationships - 9

    Результат:

    JPA Entities and DB Relationships - 10

    На UML диаграмме мы можем видеть все primary keys и foreign keys, также видим связь наших таблиц.

  4. Напишем скрипт который заполняет нашу DB тестовыми данными:

    INSERT INTO PUBLIC.BOOK (NAME, PRINT_YEAR)
    VALUES ('First book', 2010),
           ('Second book', 2011),
           ('Third book', 2012);
    
    INSERT INTO PUBLIC.AUTHOR (FIRST_NAME, SECOND_NAME, BOOK_ID)
    VALUES ('Pablo', 'Lambado', 1),
           ('Pazo', 'Zopa', 2),
           ('Lika', 'Vika', 3);

    То бишь, что получается? One-to-One relationship нужно тогда, когда сущность одной таблицы связанная с одной сущностью другой ( или вообще не связанная если NOT NULL убрать у BOOK_ID). В нашем примере у одной книжки ДОЛЖЕН быть один автор. Никак иначе.

  5. Теперь самое интересное, как связать Java класс с DB сущностями? Очень просто. Создадим два класса Book и Author. На примере я разберу 1 класс, и ключевые поля связи. Возьму за пример Author класс:

    @Data
    @Entity
    @DynamicInsert
    @DynamicUpdate
    @Table(name = "AUTHOR")
    public class Author {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        @Column(name = "ID", nullable = false)
        private Long id;
    
        @Column(name = "FIRST_NAME", nullable = false)
        private String firstName;
    
        @Column(name = "SECOND_NAME", nullable = false)
        private String secondName;
    
        @OneToOne
        @JoinColumn(name = "BOOK_ID", unique = true, nullable = false)
        private Book book;
    }
Разберемся по порядку:
  1. Все поля в классе повторяют атрибуты DB сущности.
  2. @Data (из Lombok'a) говорит, что для каждого поля будет создан геттер и сеттер, будет переопределен equals, hashcode, и сгенерирован toString метод.
  3. @Entity говорит, что данный класс — сущность и связан с сущностью DB.
  4. @DynamicInsert и @DynamicUpdate говорят, что будут выполнятся динамические вставки и обновления в DB. Это более глубокие настройки Hibernate, которые пригодятся вам, что бы у вас был ПРАВИЛЬНЫЙ батчинг.
  5. @Table(name = "AUTHOR") связывает класс Book с таблицей DB AUTHOR.
  6. @Id говорит, что данное поле — primary key.
  7. @GeneratedValue(strategy = GenerationType.IDENTITY) — стратегия генерации primary key.
  8. @Column(name = "ID", nullable = false) связывает поле с атрибутом DB, и также говорит, что данное поле DB не может быть null. Это также полезно при генерации таблиц из сущностей. Обратный процесс тому, как мы сейчас создаем наш проект, это нужно в тестовых DB для Unit тестов.
  9. @OneToOne говорит, что данное поле является полем отношения One-to-One.
  10. @JoinColumn(name = "BOOK_ID", unique = true, nullable = false) — будет создана колонка BOOK_ID, которая является уникальной и not null.
С обратной стороны (в классе Book) нам также нужно сделать связь One-to-One и указать поле, по которому происходит mapping. @OneToOne(mappedBy = "book") — в данном примере это поле book класса Author. JPA сам их свяжет. С первого взгляда может показаться что тут каша из аннотаций, но на самом деле это очень удобно и с опытом вы автоматом будете их ставить, даже не задумываясь.
  1. Теперь настроим Hibernate. Для этого создадим hibernate.cfg.xml файл:

    <?xml version='1.0' encoding='utf-8'?>
    <!DOCTYPE hibernate-configuration PUBLIC
            "-//Hibernate/Hibernate Configuration DTD//EN"
            "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
    
    <hibernate-configuration>
        <session-factory>
            <property name="hibernate.dialect">org.hibernate.dialect.H2Dialect</property>
            <property name="hibernate.connection.driver_class">org.h2.Driver</property>
    
            <property name="hibernate.connection.url">jdbc:h2:~/db/onetoone</property>
            <property name="hibernate.connection.username">root</property>
            <property name="hibernate.connection.password"/>
    
            <property name="hibernate.hbm2ddl.auto">update</property>
    
            <property name="hibernate.show_sql">true</property>
            <property name="hibernate.format_sql">true</property>
            <property name="hibernate.use_sql_comments">true</property>
    
            <property name="hibernate.generate_statistics">true</property>
    
            <property name="hibernate.jdbc.batch_size">50</property>
            <property name="hibernate.jdbc.fetch_size">50</property>
    
            <property name="hibernate.order_inserts">true</property>
            <property name="hibernate.order_updates">true</property>
            <property name="hibernate.jdbc.batch_versioned_data">true</property>
    
            <mapping class="com.qthegamep.forjavarushpublication2.entity.Book"/>
            <mapping class="com.qthegamep.forjavarushpublication2.entity.Author"/>
        </session-factory>
    </hibernate-configuration>
Описание свойств:
  1. hibernate.dialect — диалект СУБД которую мы выбрали.
  2. hibernate.connection.driver_class — Driver класс нашей DB.
  3. hibernate.connection.url — utl нашей DB. Можно взять из первого пункта, где мы настраивали DB.
  4. hibernate.connection.username — имя юзера DB.
  5. hibernate.connection.password — пароль юзера DB.
  6. hibernate.hbm2ddl.auto — настройка генерации таблиц. Если update, то не генерирует, если она уже создана а лишь обновляет ее.
  7. hibernate.show_sql — показывать ли запросы DB.
  8. hibernate.format_sql — форматировать ли запросы DB. Если нет то они будт все в одну строчку. Рекомендую включать.
  9. hibernate.use_sql_comments — комментирует запросы DB. Если это Insert то пишет над запросом комментарий что запрос типа Insert.
  10. hibernate.generate_statistics - генерирует логи. Рекомендую, и рекомендую настроить логирование по максимуму. Чтение логов увеличит ваши шансы правильной работы с ORM.
  11. hibernate.jdbc.batch_size — Максимальный размер батча.
  12. hibernate.jdbc.fetch_size — Максимальный размер фетча.
  13. hibernate.order_inserts — разрешает динамические вставки.
  14. hibernate.order_updates — разрешает динамические обновления.
  15. hibernate.jdbc.batch_versioned_data — разрешает батчинг. Смотрите по своей СУБД: не все это поддерживают.
  16. mapping class — классы, которые являются нашими сущностями. Перечислять нужно все.
  1. Теперь у нас сущность должна определиться. Можем это проверить в persistence tab'е:

    JPA Entities and DB Relationships - 11

    Результат:

    JPA Entities and DB Relationships - 12
  2. Также нам нужно настроить assign data:

    JPA Entities and DB Relationships - 13 JPA Entities and DB Relationships - 14

    Итоги: Мы сделали One-to-One mapping. Материал является ознакомительным, детали - в references.

One-to-Many Relationship

Ссылка на branch тут. Код я больше не буду выкладывать в статье, так как она и так уже слишком большая. Весь код смотрим на GitHub'e.
  1. В результате выполнения инициализирующего скрипта у нас получится следующее:

    JPA Entities and DB Relationships - 15

    Чувствуете разницу с предыдущей таблицей?

  2. Диаграмма:

    JPA Entities and DB Relationships - 16

    One-to-Many Relationship — у нас у одного автора может быть несколько книг. Левой сущности соответствует одна или несколько правой.

  3. Отличие в mapping'e будет в аннотациях и полях:

    В Author класса появляется поле:

    @OneToMany(fetch = FetchType.LAZY, mappedBy = "author")
    private Set<Book> books;

    Оно уже является сетом, так как у нас может быть несколько книг. @OneToMany говорит о типе отношения. FetchType.Lazy говорит, что не нужно нам подгружать весь список книг если это не указанно в запросе. Также следует сказать, что данное поле НЕЛЬЗЯ добавлять в toString, иначе пойдем курить StackOverflowError. Об этом у меня заботится мой любимый Lombok:

    @ToString(exclude = "books")

    В классе Book мы делаем обратную связь (Many-to-One):

    @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    @JoinColumn(name = "AUTHOR_ID", nullable = false)
    private Author author;

    Здесь мы делаем вывод, что One-to-Many является зеркальным отображением Many-to-One и наоборот. Следует подчеркнуть, что Hibernate нечего не знает о двунаправленных связях. Для него это две разные связи: одна в одну сторону, другая — в противоположную.

  4. В hibernate.cfg.xml особо нечего не поменялось.

  5. Persistence:

    JPA Entities and DB Relationships - 17

Many-to-One Relationship

Так как Many-to-One является зеркальным отображением One-to-Many, отличий будет немного. Ссылка на branch тут.
  1. В результате выполнения инициализирующего скрипта получим результат:

    JPA Entities and DB Relationships - 18
  2. Диаграмма:

    JPA Entities and DB Relationships - 19
  3. Отличие в mapping'e будет в аннотациях и полях:

    В классе Author больше нет сета, так как он переместился в Book класс.

  4. hibernate.cfg.xml

  5. Persistence:

    JPA Entities and DB Relationships - 20

Many-to-Many Relationship

Перейдем к самому интересному отношению. Это отношение по всем правилам приличия и неприличия создается через дополнительную таблицу. Но данная таблица не является сущностью. Интересно, да? Взглянем на сей shit. Ссылка на branch тут.
  1. Посмотрите на инициализирующий скрипт, здесь появляется дополнительная таблица HAS. У нас получается что то вроде author-has-book.

    В результате выполнения скрипта мы получим такие таблицы:

    JPA Entities and DB Relationships - 21
  2. Диаграмма:

    JPA Entities and DB Relationships - 22

    В нашем примере получается, что у книги может быть много автором, и у автора может быть много книг. Они могут пересекаться.

  3. В классах mapping'a будут присутствовать сеты в классах. Но, как я уже сказал, таблица HAS — это не сущность.

    Класс Author:

    @ManyToMany
    @JoinTable(name = "HAS",
            joinColumns = @JoinColumn(name = "AUTHOR_ID", referencedColumnName = "ID"),
            inverseJoinColumns = @JoinColumn(name = "BOOK_ID", referencedColumnName = "ID")
    )
    private Set<Book> books;

    @ManyToMany — вид отношения.

    @JoinTable — как раз таки и будет связывать атрибут с дополнительной таблицей HAS. В ней мы указываем два атрибута, которые будут указывать на primary keys двух сущностей.

    Класс Book:

    @ManyToMany(fetch = FetchType.LAZY, mappedBy = "books")
    private Set<Author> authors;

    Тут указываем FetchType и поле, по которому будем мапиться.

  4. Наш hibernate.cfg.xml снова-таки не притерпел изменений (я не учитываю то, что мы к каждой branch создавали новую DB).

  5. Persistence:

    JPA Entities and DB Relationships - 23

Разбор полётов

Итак, мы поверхностно рассмотрели виды DB relationships и разобрались как их реализовать в ORM модели. Мы написали тестовый проект, который демонстрирует все связи, и разобрались как конфигить hibernate / jpa. Фух.

Полезные ссылки

Мои предыдущие статьи: P.S. Могут быть ошибки, ОтЧиПяТкИ в тексте. P.P.S. Автор курил что-то странное во время написания данной статьи. Спасибо за внимание!
Комментарии (17)
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны авторизоваться
Abrorbek Ibrokhimov
Уровень 3
10 декабря 2021, 16:16
А как в консоль вывести универсальное отношение (данные из всех таблиц, представленные в виде одной)?
Igor Java/Kotlin Developer
10 января 2022, 16:57
Нужно выполнить SELECT через JOIN таблиц и указать ключи объединения
Олег
Уровень 1
25 августа 2019, 11:43
С П А С И Б О
Lein Backend Developer в ЛАНИТ
17 июля 2019, 16:02
Божечки, автора надо просто закидать печенюшками! Я перерыла много в гугле ссылок, даже на английском на many-to-many (пока делаю свое приложение с книжной библиотекой) и всё тесты не взлетали... Вот как прочла, нашла у себя тот самый косяк - СПАСИБО тебе, добрый человек!
Nikita Koliadin Full Stack Developer в Приватбанк
20 июля 2019, 10:34
Рад помочь!)
4 ноября 2018, 20:52
Статья очень хорошая для понимания с нуля. Если бы она попалась мне раньше, когда на работе самостоятельно пришлось hibernate-проект после уволившегося разработчика рефакторить, то я бы сэкономил время на понимание. С другой стороны узнал сильно много больше, гугля самостоятельно )) По статье небольшие дополнения: @DynamicInsert и @DynamicUpdate - по дефолту true поэтому в таком виде их можно не указывать (только если хотите их перевести в false - но нужно точно знать, нужно ли оно вам) В высоконагруженных проектах и промышленных БД для сохранения консистентности в больших объёмах обрабатываемых данных конфигурация помимо блока session factory должна содержать обязательно настройки transactional manager (также есть специальная аннотация для java-объектов, используется в Spring). Но транзакции это большая отдельная тема. А данная статья для понимания основ - очень хорошая!! PS: кому интересно, информация по транзакциям https://www.ibm.com/developerworks/ru/library/j-ts2/index.html?ca=drs- (EJB - устарело, но в больших корпорациях ещё можно встретить, сейчас всё новое на Spring, поэтому на нём и акцентируйтесь при прочтении)
Nikita Koliadin Full Stack Developer в Приватбанк
5 ноября 2018, 18:18
Спасибо за хороший комментарий) По поводу замечаний: 1) @DynamicInsert и @DynamicUpdate использовал для наглядности, но вы правы - они true по дефолту 2) Транзакции - действительно глубокая тема, и их нужно настроить. Я лично не углублялся в эту тему и не стал показывать настройки ведь тут можно долго обьяснять. Да и вообще суть статьи не о транзакция. Но за доп литературу спасибо )
Евгений Гродно
Уровень 36
Expert
13 октября 2018, 01:25
Молодца, хороший материал)) отсыпь немного покурить ;)
Nikita Koliadin Full Stack Developer в Приватбанк
13 октября 2018, 10:18
Спасибо) Хорошо, только тссс, никому не говори )))
Маша Сокур
Уровень 0
10 октября 2018, 22:21
Я хоть и не осел из Шрека, но тоже всё поняла Спасибо за столь полные примеры =)
Nikita Koliadin Full Stack Developer в Приватбанк
11 октября 2018, 19:27
=)
Lenar Khabibullin
Уровень 23
10 октября 2018, 09:36
Спасибки
Nikita Koliadin Full Stack Developer в Приватбанк
10 октября 2018, 13:44
И Вам спасибки )
Ирина Кравец
Уровень 0
9 октября 2018, 08:36
Автор молодец , представил все внятно и доходчиво, спасибо!)
Nikita Koliadin Full Stack Developer в Приватбанк
10 октября 2018, 13:44
Спасибо =)
Artem Murk
Уровень 35
7 октября 2018, 11:21
Интересная статья! Спасибо автору за труд)
Nikita Koliadin Full Stack Developer в Приватбанк
7 октября 2018, 19:10
Спасибо земляку за приятный комментарий )