JPA Entities && DB Relationships
Доброго часу доби, колеги!
What is Entity?
Сутність ( Entity ) - це якийсь об'єкт із реального життя (наприклад, машина), який має атрибути (двері, КОЛЕСА , двигун). DB Entity: у цьому випадку наша сутність зберігається у DB, все просто. Навіщо і як ми засунули машину в базу даних - розглянемо пізніше.What is DB Relationships?
Давним давно, у тридев'ятому королівстві була створена реляційна DB . У цьому DB дані представлялися як табличок. Але й ослу із Шрека було зрозуміло, що треба було зробити механізм взаємозв'язку даних таблиць. У результаті з'явилося 4 DB відносини : Якщо ви все це бачите вперше, попереджаю ще раз — далі буде гірше: подумайте про те, щоб піти погуляти. Всі ці відносини ми розберемо на прикладі і зрозуміємо різницю між ними.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, решта – лише ключові моменти. Наприкінці ми підіб'ємо підсумки. Поїхали. Це посилання на 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, і згенерований доString метод.
- @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.forcodegympublication2.entity.Book"/> <mapping class="com.qthegamep.forcodegympublication2.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. Матеріал є ознайомлювальним, деталі - у повідомленні.
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
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ