JPA Entities && DB Relationships

Доброго времени суток, коллеги!
Данный материал рассчитан на тех, кто уже имеет представление об организации баз данных (дальше просто 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 и делаем как на скрине:

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

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

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

  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)
    );

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

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

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

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

    Результат:

    На 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'е:

    Результат:

  2. Также нам нужно настроить assign data:

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

One-to-Many Relationship

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

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

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

    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:

Many-to-One Relationship

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

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

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

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

  4. hibernate.cfg.xml

  5. Persistence:

Many-to-Many Relationship

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

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

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

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

  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:

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

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

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

Мои предыдущие статьи: P.S. Могут быть ошибки, ОтЧиПяТкИ в тексте. P.P.S. Автор курил что-то странное во время написания данной статьи. Спасибо за внимание!