JPA Entities && DB Relationships
Доброго времени суток, коллеги!
What is Entity?
Сущность (Entity) — это некий объект из реальной жизни (например, машина), который имеет атрибуты (двери, КОЛЁСА, двигатель). DB Entity: в этом случае наша сущность хранится в DB, все просто. Зачем и каким образом мы засунули машину в базу данных — рассмотрим позже.What is DB Relationships?
Давным давно, в тридевятом королевстве была создана реляционная DB. В этой DB данные представлялись в виде табличек. Но и ослу из Шрека было понятно, что нужно было сделать механизм взаимосвязи данных таблиц. В итоге появилось 4 DB relationships: Если вы все это видите впервые, предупреждаю еще раз — дальше будет хуже: задумайтесь о том, чтобы пойти погулять. Все эти отношения мы разберем на примере, и поймем разницу между ними.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 тут.Нужно подключить H2 DB к нашему проекту. Здесь нужно подчеркнуть то, что нам нужна Ultimate IDEA для комфортной работы с DB и другими вещами. Если она у вас уже есть тогда идем непосредственно к подключению DB. Заходим в tab Database и делаем как на скрине:
Далее переходим к настройкам DB. Вы можете ввести свои данные, и даже свою СУБД, повторюсь, H2 DB я использую для простоты.
Далее настроим схему. Этот шаг не обязателен но желателен, если у вас несколько схем в DB.
Применяем настройки, и в итоге у нас должно получиться что то вроде этого:
Базу данных мы создали, и настроили доступ к ней из 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:
Давайте посмотрим на диаграмму наших таблиц. Для этого ПКМ на нашу DB:
Результат:
На UML диаграмме мы можем видеть все primary keys и foreign keys, также видим связь наших таблиц.
Напишем скрипт который заполняет нашу 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). В нашем примере у одной книжки ДОЛЖЕН быть один автор. Никак иначе.
Теперь самое интересное, как связать 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; }
- Все поля в классе повторяют атрибуты DB сущности.
- @Data (из Lombok'a) говорит, что для каждого поля будет создан геттер и сеттер, будет переопределен equals, hashcode, и сгенерирован toString метод.
- @Entity говорит, что данный класс — сущность и связан с сущностью DB.
- @DynamicInsert и @DynamicUpdate говорят, что будут выполнятся динамические вставки и обновления в DB. Это более глубокие настройки Hibernate, которые пригодятся вам, что бы у вас был ПРАВИЛЬНЫЙ батчинг.
- @Table(name = "AUTHOR") связывает класс Book с таблицей DB AUTHOR.
- @Id говорит, что данное поле — primary key.
- @GeneratedValue(strategy = GenerationType.IDENTITY) — стратегия генерации primary key.
- @Column(name = "ID", nullable = false) связывает поле с атрибутом DB, и также говорит, что данное поле DB не может быть null. Это также полезно при генерации таблиц из сущностей. Обратный процесс тому, как мы сейчас создаем наш проект, это нужно в тестовых DB для Unit тестов.
- @OneToOne говорит, что данное поле является полем отношения One-to-One.
- @JoinColumn(name = "BOOK_ID", unique = true, nullable = false) — будет создана колонка BOOK_ID, которая является уникальной и not null.
Теперь настроим 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>
- hibernate.dialect — диалект СУБД которую мы выбрали.
- hibernate.connection.driver_class — Driver класс нашей DB.
- hibernate.connection.url — utl нашей DB. Можно взять из первого пункта, где мы настраивали DB.
- hibernate.connection.username — имя юзера DB.
- hibernate.connection.password — пароль юзера DB.
- hibernate.hbm2ddl.auto — настройка генерации таблиц. Если update, то не генерирует, если она уже создана а лишь обновляет ее.
- hibernate.show_sql — показывать ли запросы DB.
- hibernate.format_sql — форматировать ли запросы DB. Если нет то они будт все в одну строчку. Рекомендую включать.
- hibernate.use_sql_comments — комментирует запросы DB. Если это Insert то пишет над запросом комментарий что запрос типа Insert.
- hibernate.generate_statistics - генерирует логи. Рекомендую, и рекомендую настроить логирование по максимуму. Чтение логов увеличит ваши шансы правильной работы с ORM.
- hibernate.jdbc.batch_size — Максимальный размер батча.
- hibernate.jdbc.fetch_size — Максимальный размер фетча.
- hibernate.order_inserts — разрешает динамические вставки.
- hibernate.order_updates — разрешает динамические обновления.
- hibernate.jdbc.batch_versioned_data — разрешает батчинг. Смотрите по своей СУБД: не все это поддерживают.
- mapping class — классы, которые являются нашими сущностями. Перечислять нужно все.
Теперь у нас сущность должна определиться. Можем это проверить в persistence tab'е:
Результат:
Также нам нужно настроить assign data:
Итоги: Мы сделали One-to-One mapping. Материал является ознакомительным, детали - в references.
One-to-Many Relationship
Ссылка на branch тут. Код я больше не буду выкладывать в статье, так как она и так уже слишком большая. Весь код смотрим на GitHub'e.В результате выполнения инициализирующего скрипта у нас получится следующее:
Чувствуете разницу с предыдущей таблицей?
Диаграмма:
One-to-Many Relationship — у нас у одного автора может быть несколько книг. Левой сущности соответствует одна или несколько правой.
Отличие в 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 нечего не знает о двунаправленных связях. Для него это две разные связи: одна в одну сторону, другая — в противоположную.
В hibernate.cfg.xml особо нечего не поменялось.
Persistence:
Many-to-One Relationship
Так как Many-to-One является зеркальным отображением One-to-Many, отличий будет немного. Ссылка на branch тут.В результате выполнения инициализирующего скрипта получим результат:
Диаграмма:
Отличие в mapping'e будет в аннотациях и полях:
В классе Author больше нет сета, так как он переместился в Book класс.
Persistence:
Many-to-Many Relationship
Перейдем к самому интересному отношению. Это отношение по всем правилам приличия и неприличия создается через дополнительную таблицу. Но данная таблица не является сущностью. Интересно, да? Взглянем на сей shit. Ссылка на branch тут.Посмотрите на инициализирующий скрипт, здесь появляется дополнительная таблица HAS. У нас получается что то вроде author-has-book.
В результате выполнения скрипта мы получим такие таблицы:
Диаграмма:
В нашем примере получается, что у книги может быть много автором, и у автора может быть много книг. Они могут пересекаться.
В классах 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 и поле, по которому будем мапиться.
Наш hibernate.cfg.xml снова-таки не притерпел изменений (я не учитываю то, что мы к каждой branch создавали новую DB).
Persistence:
Разбор полётов
Итак, мы поверхностно рассмотрели виды DB relationships и разобрались как их реализовать в ORM модели. Мы написали тестовый проект, который демонстрирует все связи, и разобрались как конфигить hibernate / jpa. Фух.Полезные ссылки
- Собственно сам проект
- One-to-One branch
- One-to-Many branch
- Many-to-One branch
- Many-to-Many branch
- Read it
- And read it
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ