Русский
Русский
English
Статистика
Реклама

Hibernate

Linux, suspend to RAM и ИБП

21.02.2021 02:04:48 | Автор: admin

В случае периодических, но достаточно кратковременных, отключений электроэнергии и наличии недорого ИБП с 1-2 аккумуляторами на 7 А.ч, не всегда есть смысл выключать компьютер пока не дадут свет. Особенно если вам долго и сложно поднимать рабочее окружение. Ниже будут приведены замеры потребления мощности для конкретного десктопа и стратегия работы с ИБП при нестабильном электричестве.


Если очень грубо, Linux имеет два вида спящего режима hibernate, когда состояние памяти сбрасывается на диск и при старте ОС оттуда же считывается, а второй suspend питание на планки RAM подается, но останавливаются жесткие диски, вентиляторы, в т.ч. блока питания. Hibernate, на мой взгляд, малоактуален, с текущими объемами памяти, необходимостью дополнительной настройки, вероятностью высадить аккумулятор ИБП именно во время перехода в него. Из плюсов если вы успеете перейти в hibernate, то разряд аккумулятора в ноль вам не страшен.


Suspend позволяет достаточно быстро входить и выходить из него, без дополнительной настройки. Просто по команде sudo systemctl suspend. А что с энергопотреблением?


Итак, конфигурация в режиме простоя кушающая электричество:


  • 2 монитора
  • процессор AMD FX-6300 + какая-то материнка
  • 4 планки RAM
  • 2 TB HDD 7200
  • DVD-ROM
  • внешний HDD по USB3
  • мышь, клавиатура, вебкамера

Все это потребляет 160 вт. С выключенными мониторами 118 Вт. После нажатия systemctl suspend (компьютер переводит мониторы в режим standby) 25 вт. Если выключить мониторы еще и физически 23.5 Вт.


Таким образом перевод компьютера в режим suspend позволит вам увеличить время работы от ИБП в 5-6 раз. Что может быть достаточно, если вы примерно представляете частоту и длительность отключений света в своем регионе. Я не называю конкретных цифр по времени работы, потому что это сильно зависит от состояния аккумуляторов в ИБП, которые нужно менять(по моему опыту) не реже чем раз в 2 года, а откровенную китайщину лучше раз в год.


И поскольку это линукс, то тут есть нюансы. При использовании docker, который любит менять таблицу маршрутизации, могут наблюдаться определенные проблемы с сетью. Чтобы этого не было, для ухода в suspend лучше использовать такой скрипт(который пускать из под root):


#!/bin/bash# уходим в ждущий режимsystemctl suspend# все что ниже будет выполнено сразу после включения компьютераsystemctl stop docker# примерный перезапуск сетиifconfig <интерфейс> downdhclient <интерфейс># если нужно поднять openvpn/wireguard то делаете это тутkillall openvpn && cd /etc/openvpn && openvpn client.ovpn# только теперь запускаете dockerservice docker start
Подробнее..
Категории: Ибп , Linux , Настройка linux , Hibernate , Suspend , Acpi

Перевод Проблема с N1 запросами в JPA и Hibernate

24.11.2020 20:17:07 | Автор: admin

В преддверии курса "Highload Architect" приглашаем вас посетить открытый урок по теме "Паттерны горизонтального масштабирования хранилищ".


А пока делимся традиционным переводом полезной статьи.


Введение

В этой статье я расскажу, в чем состоит проблема N + 1 запросов при использовании JPA и Hibernate, и как ее лучше всего исправить.

Проблема N + 1 не специфична для JPA и Hibernate, с ней вы можете столкнуться и при использовании других технологий доступа к данным.

Что такое проблема N + 1

Проблема N + 1 возникает, когда фреймворк доступа к данным выполняет N дополнительных SQL-запросов для получения тех же данных, которые можно получить при выполнении одного SQL-запроса.

Чем больше значение N, тем больше запросов будет выполнено и тем больше влияние на производительность. И хотя лог медленных запросов может вам помочь найти медленные запросы, но проблему N + 1 он не обнаружит, так как каждый отдельный дополнительный запрос выполняется достаточно быстро.

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

Рассмотрим следующие таблицы БД: post (посты) и post_comments (комментарии к постам), которые связаны отношением "один-ко-многим":

Вставим в таблицу post четыре строки:

INSERT INTO post (title, id)VALUES ('High-Performance Java Persistence - Part 1', 1)  INSERT INTO post (title, id)VALUES ('High-Performance Java Persistence - Part 2', 2)  INSERT INTO post (title, id)VALUES ('High-Performance Java Persistence - Part 3', 3)  INSERT INTO post (title, id)VALUES ('High-Performance Java Persistence - Part 4', 4)

А в таблицу post_comment четыре дочерние записи:

INSERT INTO post_comment (post_id, review, id)VALUES (1, 'Excellent book to understand Java Persistence', 1)  INSERT INTO post_comment (post_id, review, id)VALUES (2, 'Must-read for Java developers', 2)  INSERT INTO post_comment (post_id, review, id)VALUES (3, 'Five Stars', 3)  INSERT INTO post_comment (post_id, review, id)VALUES (4, 'A great reference book', 4)

Проблема N+1 с простым SQL

Как уже говорилось, проблема N + 1 может возникнуть при использовании любой технологии доступа к данным, даже при прямом использовании SQL.

Если вы выберете post_comments с помощью следующего SQL-запроса:

List<Tuple> comments = entityManager.createNativeQuery("""    SELECT        pc.id AS id,        pc.review AS review,        pc.post_id AS postId    FROM post_comment pc    """, Tuple.class).getResultList();

А позже решите получить заголовок (title) связанного поста (post) для каждого комментария (post_comment):

for (Tuple comment : comments) {    String review = (String) comment.get("review");    Long postId = ((Number) comment.get("postId")).longValue();     String postTitle = (String) entityManager.createNativeQuery("""        SELECT            p.title        FROM post p        WHERE p.id = :postId        """)    .setParameter("postId", postId)    .getSingleResult();     LOGGER.info(        "The Post '{}' got this review '{}'",        postTitle,        review    );}

Вы получите проблему N + 1, потому что вместо одного SQL-запроса вы выполнили пять (1 + 4):

SELECT    pc.id AS id,    pc.review AS review,    pc.post_id AS postIdFROM post_comment pc SELECT p.title FROM post p WHERE p.id = 1-- The Post 'High-Performance Java Persistence - Part 1' got this review-- 'Excellent book to understand Java Persistence'    SELECT p.title FROM post p WHERE p.id = 2-- The Post 'High-Performance Java Persistence - Part 2' got this review-- 'Must-read for Java developers'     SELECT p.title FROM post p WHERE p.id = 3-- The Post 'High-Performance Java Persistence - Part 3' got this review-- 'Five Stars'     SELECT p.title FROM post p WHERE p.id = 4-- The Post 'High-Performance Java Persistence - Part 4' got this review-- 'A great reference book'

Исправить эту проблему с N + 1 запросом очень просто. Все, что нужно сделать, это извлечь все необходимые данные одним SQL-запросом, например, так:

List<Tuple> comments = entityManager.createNativeQuery("""    SELECT        pc.id AS id,        pc.review AS review,        p.title AS postTitle    FROM post_comment pc    JOIN post p ON pc.post_id = p.id    """, Tuple.class).getResultList(); for (Tuple comment : comments) {    String review = (String) comment.get("review");    String postTitle = (String) comment.get("postTitle");     LOGGER.info(        "The Post '{}' got this review '{}'",        postTitle,        review    );}

На этот раз выполняется только один SQL-запрос и возвращаются все данные, которые мы хотим использовать в дальнейшем.

Проблема N + 1 с JPA и Hibernate

При использовании JPA и Hibernate есть несколько способов получить проблему N + 1, поэтому очень важно знать, как избежать таких ситуаций.

Рассмотрим следующие классы, которые мапятся на таблицы post и post_comments:

JPA-маппинг выглядят следующим образом:

@Entity(name = "Post")@Table(name = "post")public class Post {     @Id    private Long id;     private String title;     //Getters and setters omitted for brevity} @Entity(name = "PostComment")@Table(name = "post_comment")public class PostComment {     @Id    private Long id;     @ManyToOne    private Post post;     private String review;     //Getters and setters omitted for brevity}

FetchType.EAGER

Использование явного или неявного FetchType.EAGER для JPA-ассоциаций плохая идея, потому что будет загружаться гораздо больше данных, чем вам нужно. Более того, стратегия FetchType.EAGER также подвержена проблемам N + 1.

К сожалению, ассоциации @ManyToOne и @OneToOne по умолчанию используют FetchType.EAGER, поэтому, если ваши маппинги выглядят следующим образом:

@ManyToOneprivate Post post;

У вас используется FetchType.EAGER и каждый раз, когда вы забываете указатьJOIN FETCH при загрузке сущностей PostComment с помощью JPQL-запроса или Criteria API:

List<PostComment> comments = entityManager.createQuery("""    select pc    from PostComment pc    """, PostComment.class).getResultList();

Вы сталкиваетесь с проблемой N + 1:

SELECT    pc.id AS id1_1_,    pc.post_id AS post_id3_1_,    pc.review AS review2_1_FROM    post_comment pc SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 1SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 2SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 3SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 4

Обратите внимание на дополнительные запросы SELECT, которые появились, потому что перед возвращением списка сущностей PostComment необходимо извлечь ассоциацию с post.

В отличие от значений по умолчанию, используемых в методе find из EntityManager, в JPQL-запросах и Criteria API явно указывается план выборки (fetch plan), который Hibernate не может изменить, автоматически применив JOIN FETCH. Таким образом, вам это нужно делать вручную.

Если вам совсем не нужна ассоциация с post, то не повезло: с использованием FetchType.EAGER нет способа избежать ее получения. Поэтому по умолчанию лучше использовать FetchType.LAZY.

Но если вы хотите использовать ассоциацию с post, то можно использовать JOIN FETCH, чтобы избежать проблемы с N + 1:

List<PostComment> comments = entityManager.createQuery("""    select pc    from PostComment pc    join fetch pc.post p    """, PostComment.class).getResultList(); for(PostComment comment : comments) {    LOGGER.info(        "The Post '{}' got this review '{}'",        comment.getPost().getTitle(),        comment.getReview()    );}

На этот раз Hibernate выполнит один SQL-запрос:

SELECT    pc.id as id1_1_0_,    pc.post_id as post_id3_1_0_,    pc.review as review2_1_0_,    p.id as id1_0_1_,    p.title as title2_0_1_FROM    post_comment pcINNER JOIN    post p ON pc.post_id = p.id     -- The Post 'High-Performance Java Persistence - Part 1' got this review-- 'Excellent book to understand Java Persistence' -- The Post 'High-Performance Java Persistence - Part 2' got this review-- 'Must-read for Java developers' -- The Post 'High-Performance Java Persistence - Part 3' got this review-- 'Five Stars' -- The Post 'High-Performance Java Persistence - Part 4' got this review-- 'A great reference book'

Подробнее о том, почему следует избегать стратегии FetchType.EAGER, читайте в этой статье.

FetchType.LAZY

Даже если вы явно перейдете на использование FetchType.LAZY для всех ассоциаций, то вы все равно можете столкнуться с проблемой N + 1.

На этот раз ассоциация с post мапится следующим образом:

@ManyToOne(fetch = FetchType.LAZY)private Post post;

Теперь, когда вы запросите PostComment:

List<PostComment> comments = entityManager.createQuery("""    select pc    from PostComment pc    """, PostComment.class).getResultList();

Hibernate выполнит один SQL-запрос:

SELECT    pc.id AS id1_1_,    pc.post_id AS post_id3_1_,    pc.review AS review2_1_FROM    post_comment pc

Но если позже вы обратитесь к этой lazy-load ассоциации с post:

for(PostComment comment : comments) {    LOGGER.info(        "The Post '{}' got this review '{}'",        comment.getPost().getTitle(),        comment.getReview()    );}

Вы получите проблему с N + 1 запросом:

SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 1-- The Post 'High-Performance Java Persistence - Part 1' got this review-- 'Excellent book to understand Java Persistence' SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 2-- The Post 'High-Performance Java Persistence - Part 2' got this review-- 'Must-read for Java developers' SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 3-- The Post 'High-Performance Java Persistence - Part 3' got this review-- 'Five Stars' SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 4-- The Post 'High-Performance Java Persistence - Part 4' got this review-- 'A great reference book'

Поскольку ассоциация с post загружается лениво, при доступе к этой ассоциации будет выполняться дополнительный SQL-запрос для получения нужных данных.

Опять же, решение заключается в добавлении JOIN FETCH к запросу JPQL:

List<PostComment> comments = entityManager.createQuery("""    select pc    from PostComment pc    join fetch pc.post p    """, PostComment.class).getResultList(); for(PostComment comment : comments) {    LOGGER.info(        "The Post '{}' got this review '{}'",        comment.getPost().getTitle(),        comment.getReview()    );}

И как и в примере с FetchType.EAGER, этот JPQL-запрос будет генерировать один SQL-запрос.

Даже если вы используете FetchType.LAZY и не ссылаетесь на дочерние ассоциации двунаправленного отношения @OneToOne, вы все равно можете получить N + 1.

Подробнее о том, как преодолеть проблему N+1 c @OneToOne-ассоциациями, читайте в этой статье.

Кэш второго уровня

Проблема N + 1 также может возникать при использовании кэша второго уровня для обработки коллекций или результатов запроса.

Например, если выполните следующий JPQL-запрос, использующий кэш запросов:

List<PostComment> comments = entityManager.createQuery("""    select pc    from PostComment pc    order by pc.post.id desc    """, PostComment.class).setMaxResults(10).setHint(QueryHints.HINT_CACHEABLE, true).getResultList();

Если PostComment не находится в кэше второго уровня, то будет выполнено N запросов для получения каждого отдельного PostComment:

-- Checking cached query results in region: org.hibernate.cache.internal.StandardQueryCache-- Checking query spaces are up-to-date: [post_comment]-- [post_comment] last update timestamp: 6244574473195524, result set timestamp: 6244574473207808-- Returning cached query results  SELECT pc.id AS id1_1_0_,       pc.post_id AS post_id3_1_0_,       pc.review AS review2_1_0_FROM post_comment pcWHERE pc.id = 3  SELECT pc.id AS id1_1_0_,       pc.post_id AS post_id3_1_0_,       pc.review AS review2_1_0_FROM post_comment pcWHERE pc.id = 2  SELECT pc.id AS id1_1_0_,       pc.post_id AS post_id3_1_0_,       pc.review AS review2_1_0_FROM post_comment pcWHERE pc.id = 1

В кэше запросов хранятся только идентификаторы сущностей PostComment. Таким образом, если сущности PostComment не находятся в кэше, они будут извлечены из базы данных и вы получите N дополнительных SQL-запросов.


Подробнее о курсе "Highload Architect".

Подробнее..

Односторонние и двусторонние отношения в Hibernate

14.02.2021 16:19:02 | Автор: admin

Всем нам хорошо известен ответ на вопрос, какими могут быть отношения между сущностями в Hibernate и JPA. Вариантов всего четыре:

  • OneToOne - один к одному

  • OneToMany - один ко многим

  • ManyToOne - многие к одному

  • ManyToMany - многие ко многим

Для каждого из отношений есть своя аннотация и, казалось бы, на этом можно закончить разговор, но все не так просто. Да и вообще, может ли быть что-то просто в Hibernate ;) Каждое из выше перечисленных отношений может быть односторонним (unidirectional) или двусторонним (bidirectional), и если не принимать это во внимание, то можно столкнуться с массой проблем и странностей.

Для примера возьмем две простейшие сущности: пользователь и контакт. Очевидно, что каждый контакт связан с пользователем отношением многие к одному, а пользователь с контактами отношением один ко многим.

Односторонние отношения

Односторонним называется отношение, владельцем которого является только одна из двух сторон. Отсюда и название. Следует заметить, что при этом вторая сторона об этом отношении ничего не знает. HIbernate будет считать владельцем отношения ту сущность, в которой будет поставлена аннотация отношения.

Давайте попробуем сделать владельцем отношения сторону контакта. При этом сущности будут выглядеть следующим образом.

@Entity@Table(name = "contacts")public class Contact {    @Id    @GeneratedValue(strategy = GenerationType.IDENTITY)    private Long id;    @Column    private String type;    @Column    private String data;    @ManyToOne    private User user;        // Конструктор по умолчанию, геттеры, сеттеры и т.д.}@Entity@Table(name = "users")public class User {    @Id    @GeneratedValue(strategy = GenerationType.IDENTITY)    private Long id;    @Column    private String username;    // Конструктор по умолчанию, гетеры, сеттеры и т.д.}

Если запустить этот код, то Hibernate создаст следующую структуру таблиц, которая выглядит для нас вполне привычно. Отношение между таблицами создается при помощи ссылочного поля user_id в таблице contacts.

create table contacts (    id bigint not null auto_increment,    data varchar(255),    type varchar(255),    user_id bigint,    primary key (id)) engine=InnoDB;    create table users (    id bigint not null auto_increment,    email varchar(255),    password varchar(512) not null,    username varchar(128) not null,    primary key (id)) engine=InnoDB

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

@Entity@Table(name = "contacts")public class Contact {    @Id    @GeneratedValue(strategy = GenerationType.IDENTITY)    private Long id;    @Column    private String type;    @Column    private String data;        // Конструктор по умолчанию, геттеры, сеттеры и т.д.}@Entity@Table(name = "users")public class User {    @Id    @GeneratedValue(strategy = GenerationType.IDENTITY)    private Long id;    @Column    private String username;    @OneToMany    private List<Contact> contacts;    // Конструктор по умолчанию, гетеры, сеттеры и т.д.}

Теперь владельцем отношения является сущность пользователя, что более логично, но если запустить данный код и посмотреть на созданную Hibernate структуру таблиц, то мы столкнёмся с одной хорошо известной почти каждому кто использовал эту библиотеку проблемой.

create table contacts (    id bigint not null auto_increment,    data varchar(255),    type varchar(255),    primary key (id)) engine=InnoDB;create table users (    id bigint not null auto_increment,    email varchar(255),    password varchar(512) not null,    username varchar(128) not null,    primary key (id)) engine=InnoDB;create table users_contacts (    User_id bigint not null,    contacts_id bigint not null) engine=InnoDB;

Чтобы связать сущности Hibernate создал дополнительную таблицу связи (join table) с именем users_contacts, хотя сущности вполне можно было бы связать через ссылочное поле в таблице contacts, как в предыдущем случае. Честно говоря, я не совсем понимаю, почему Hibernate поступает именно так. Буду рад, если кто-то поможет с этим разобраться в комментариях к статье.

Проблему можно легко решить добавив аннотацию JoinColumn к полю contacts.

    @OneToMany    @JoinColumn(name = "user_id")    private List<Contact> contacts;

При таких настройках связь будет проводиться при помощи колонки user_id в таблице contacts, а таблица связи создаваться не будет.

Двусторонние отношения

У двусторонних отношений помимо стороны - владельца (owning side) имеется ещё и противоположная сторона (inverse side). Т.е. обе стороны отношения обладают информацией о связи. Логично предположить, что из одностороннего отношения можно сделать двустороннее просто добавив поле и аннотацию в класс сущности противоположной стороны, но не все так просто. В чем именно тут проблема очень хорошо видно на примере отношения многие ко многим. Давайте создадим пример такого отношения между сущностями пользователя и роли этого пользователя.

@Entity@Table(name = "users")public class User {    @Id    @GeneratedValue(strategy = GenerationType.IDENTITY)    private Long id;    @Column    private String username;    @ManyToMany    private List<Role> roles;    // Конструктор по умолчанию, гетеры, сеттеры и т.д.}@Entity@Table(name = "roles")public class Role {    @Id    @GeneratedValue(strategy = GenerationType.IDENTITY)    private Long id;    @Column    private String name;    @ManyToMany    private List<User> users;    // Конструктор по умолчанию, гетеры, сеттеры и т.д.}

Запускаем код и смотрим на структуру таблиц. Помимо таблиц для пользователей и ролей Hibernate создаст две таблицы связи, хотя нам хватило бы и одной.

create table roles_users (    Role_id bigint not null,    users_id bigint not null) engine=InnoDB;create table users_roles (    User_id bigint not null,    roles_id bigint not null) engine=InnoDB;

Дело в том, что вместо одного одностороннего отношения мы с вами сейчас создали два односторонних. Тоже самое произойдет и для отношения один ко многим. Чтобы Hibernate понял, что мы хотим создать именно одностороннее отношение нам нужно указать, какая из двух сторон является владельцем отношений, а какая сторона является обратной. Это делается при помощи атрибута mappedBy. Важно отметить, что указывается этот параметр в аннотации, которая находится на противоположной стороне отношения.

Для отношения многие ко многим любая из сторон может быть владельцем. В случае с ролями и пользователями выберем сущность пользователя в качестве владельца. Для этого изменим описание поля users в классе Role следующим образом.

    // значение атрибута mappedBy - имя поля связи в классе сущности-владельца отношений    @ManyToMany(mappedBy = "roles")    private List<User> users;

Теперь Hibernate создаст только одну таблицу связи users_roles.

И напоследок давайте сделаем двусторонним отношение между пользователями и контактами. Следует отметить, что в отношении один ко многим стороной-владельцем может быть только сторона многих (many), поэтому атрибут mappedBy есть только в аннотации @OneToMany . В нашем случае владельцем отношения будет сторона контакта (класс Contact).

@Entity@Table(name = "contacts")public class Contact {    @Id    @GeneratedValue(strategy = GenerationType.IDENTITY)    private Long id;    @Column    private String type;    @Column    private String data;    @ManyToOne    private User user;        // Конструктор по умолчанию, геттеры, сеттеры и т.д.}@Entity@Table(name = "users")public class User {    @Id    @GeneratedValue(strategy = GenerationType.IDENTITY)    private Long id;    @Column    private String username;    @OneToMany(mappedBy = "user")    private List<Contact> contacts;    // Конструктор по умолчанию, гетеры, сеттеры и т.д.}

Для такого кода Hibernate создаст привычную нам структуру из двух таблиц со ссылкой на пользователя в таблице контактов.

На этом все на этот раз! Благодарю, что дочитали до конца и надеюсь, что статья была полезной! Разумеется, очень жду от вас обратную связь в виде голосов и комментариев!

Возможно, будет продолжение ;

Подробнее..

JPA Buddy Умный помощник половина работы

17.03.2021 14:12:14 | Автор: admin

От переводчика: это статья моего коллеги @aleksey-stukalov, которую мы опубликовали в блоге JPA Buddy пару месяцев назад. С тех пор мы выпустили JPA Buddy 2.0, но все сказанное в этой статье актуальности не потеряло.

Ну что ж, Hello World... После почти года разработки наконец-то вышла первая версия JPA Buddy! Это инструмент, который должен стать вашим верным помощником по написанию кода для проектов с JPA и всем, что с этим связано: Hibernate, Spring Data, Liquibase и другим ПО из типичного стека разработки.

Чем он вам поможет? Если кратко, JPA Buddy упростит работу с JPA и тем самым сэкономит ваше время. В этой статье мы взглянем на основные фичи JPA Buddy, немного обсудим его историю и поговорим о его преимуществах. Надеюсь, он займет достойное место среди любимых инструментов Java-разработчиков, которые пользуютсяJPA, Spring, Liquibase и, конечно же, самой продвинутой Java IDE IntelliJ IDEA.

Откуда растут ноги

Мы создатели CUBA Platform (кстати, не так давно мы переименовали ее в Jmix :)) среды быстрой разработки приложений на Java. Платформа CUBA уникальный продукт. Он состоит из двух частей: фреймворка и инструмента разработки CUBA Studio. Одной из самых полюбившихся частей CUBA Studio стал Entity Designer. В сообществе CUBA более 20 000 разработчиков, и почти все они отмечают, насколько легко и быстро он дает создавать модель данных. Даже для тех, кто никогда не слышал о JPA, такие вещи как создание JPA-сущностей, ограничений модели данных и DDL-скриптов становятся легкой задачей.

В 2016 году CUBA стала open-source проектом. С того момента мы приняли участие в десятках конференций по всему миру, собрали много отзывов и лучше поняли, что именно нужно разработчикам. Они нас часто спрашивали: Можно ли использовать ваш конструктор сущностей без CUBA Platform?. Рады сообщить, что с появлением JPA Buddy мы теперь можем смело ответить да!.

JPA Buddy не требует дополнительных зависимостей это просто инструмент. Вы можете пользоваться им не только на новых, но и на уже существующих проектах, которые используют JPA. Это плагин для IntelliJ IDEA, который помогает вам писать связанный с JPA код, находить и исправлять потенциальные ошибки, проводить рефакторинг и выполнять другие трудоемкие, но достаточно шаблонные операции.

Область применения

Перед тем, как написать первую строчку исходного кода JPA Buddy, мы провели опрос и собрали различные сценарии использования JPA и сопутствующих технологий. Результат оказался достаточно предсказуемым: среднестатистическое приложение в настоящее время это приложение на Spring Boot с Hibernate в качестве реализации ORM, Spring Data JPA в качестве механизма управления данными и Flyway или Liquibase для системы миграции базы данных. Ах да, чуть не забыли про Lombok... В общем, на этом стеке мы и сконцентрировались в первом релизе.

Говоря о предназначении JPA Buddy, мы поставили перед собой следующие цели:

  • Сократить написание шаблонного кода вручную: инструмент должен генерировать код быстрее ручного ввода

  • Не заставлять тратить время на чтение документации: инструмент должен предоставлять интуитивно понятные визуальные конструкторы

  • Оставить свободу выбора: инструмент должен не диктовать какойлибо конкретный стиль написания кода, а поддерживать его различные варианты

  • Выявлять потенциальные проблемы и предлагать решения для наиболее распространенных: инструмент должен предупреждать разработчика о проблемах на этапе написания кода, а не в рантайме

  • Обеспечить обзор проекта с точки зрения данных и удобную навигацию между связанными сущностями

В первом релизе мы смогли реализовать достаточно значительный объем функциональности, охватывающей большинство аспектов разработки модели данных. Хорошая новость для тех, кто использует Liquibase: инструменты для работы с ним уже реализованы и доступны. Не очень хорошая новость для пользователей Flyway: он входит в список наиболее приоритетных фич :)

В долгосрочной перспективе мы собираемся реализовать следующие функции:

  • Аннотации Hibernate: @Where, @NaturalId, @Formula, поисковые аннотации и т. п.

  • Визуальный конструктор запросов

  • Аудит с использованием Envers и Spring Data JPA

  • Генерация модели по существующей схеме базы данных

  • Поддержка Kotlin

В дальнейшем мы учтем и другие функции: поддержка Quarkus и Micronaut, REST API и генерация пользовательского интерфейса для CRUD-операций.

Как видите, впереди еще долгий путь, и мы были бы очень признательны за вашу помощь в расстановке приоритетов разработки не стесняйтесь высказывать свою точку зрения через нашу форму, Twitter или Discord.

Общий обзор

Давайте посмотрим, как выглядит JPA Buddy. После его установки у вас появятся 3 новые панели инструментов: JPA Structure, JPA Palette и JPA Inspector.

JPA Structure

Панель JPA Structure расположена в левом нижнем углу. Она позволяет посмотреть на проект с точки зрения модели данных. С ее помощью можно:

  1. Перемещаться по модели данных. Структура сущностей представлена в иерархическом виде. По ней легко перейти к объектам, ссылающимся на текущий объект, и к тем, на которые ссылается текущий объект. Это особенно полезно для тех, кто только начинает погружаться в существующий проект с большим графом сущностей, или для код-ревьюеров, которые видят часть модели впервые и им нужно в ней быстро разобраться.

  2. Создавать объекты, связанные с данными: сущности, JPA-конвертеры/Hibernate-типы, Spring Data репозитории и Liquibase-скрипты.

  3. Увидеть, для каких сущностей созданы какие репозитории.

  4. Просматривать скрипты Liquibase и их внутреннюю структуру.

  5. Настраивать параметры плагина, такие как соединение с БД, Persistence Units и некоторые другие, которые плагин не обнаружил автоматически.

JPA Palette и JPA Inspector

Панель JPA Palette находится справа вверху, и ее содержимое зависит от контекста. Она доступна только тогда, когда Buddy готов предложить чтото для быстрой генерации кода. Сейчас панель появляется при редактировании следующих объектов: JPA Entity, Spring Data репозиториев и Liquibase-скриптов. Для генерации какого-либо элемента просто выберите нужный вариант в списке и кликните по нему дважды.

JPA Inspector размещается справа внизу, под JPA Palette, и открывается вместе с ним. С помощью JPAPalette можно генерировать новый код, а с помощью JPAInspector редактировать уже существующий. В нем видно, как можно сконфигурировать выбранную часть кода, будь то поле сущности или выражение изLiquibase-скрипта.

Все это будет полезно как для новичков, так и для опытных разработчиков. Первые могут посмотреть на полный список вариантов, выбрать подходящий и сгенерировать корректный код с помощью понятных визуальных конструкторов. Сеньоры, в свою очередь, часто забывают мелочи, которые не используются в повседневной жизни, и Buddy сэкономит несколько секунд драгоценного времени на поиск в документации правильной аннотации или синтаксиса.

Интеграция с Liquibase

Хорошие новости для тех, кто использует Liquibase для управления версиями схемы базы данных: JPA Buddy тесно с ним интегрирован, он позволяет удобно редактировать и создавать Liquibase-скрипты, а также гибко управлять типами атрибутов. Давайте посмотрим, как именно JPA Buddy поможет вам работать с Liquibase.

Во-первых, в JPA Buddy есть визуальный конструктор для редактирования Liquibase-скриптов. Он показывает различные доступные команды плагина в JPAPalette, а панель инструментов JPAInspector дает увидеть настройки, которые можно применить к выбранному выражению.

Во-вторых, JPA Buddy дает определить свои собственные маппинги для сопоставления Java-типов (Конвертеров/Hibernateтипов) и типов, которые должны использоваться для каждой конкретной СУБД. Приведем несколько примеров, когда эта функция будет полезна:

  • Допустим, у вас в сущности есть поле типа byte[]. Что генератор схемы Hibernate, что Liquibase сопоставят ваш массив с довольно экзотическим типом OID. Думаю, многие разработчики предпочли бы использовать bytea вместо предлагаемого по умолчанию типа. Это можно легко сделать, создав маппинг с Java-типа byte[] на БД-тип bytea в настройках проекта в JPA Buddy.

  • Использование JPA-конвертера или кастомного Hibernate-типа вызывает непредсказуемое поведение или даже ошибки в стандартных генераторе схемы Hibernate и генераторе скриптов Liquibase. С помощью JPA Buddy можно явно указать нужный тип, чтобы решить эту проблему.

  • Поля типа String по-умолчанию хранятся как varchar, то есть не поддерживают Юникод. С помощью JPA Buddy легко изменить этот тип на nvarchar, с которым этой проблемы нет.

Все эти случаи обычно решаются с помощью атрибута columnDefinition, однако это решение не будет работать для проектов, где одновременно используется несколько СУБД. JPA Buddy позволяет указать, какой именно маппинг использовать для конкретной СУБД.

Наконец, функция, экономящая наибольшее количество времени наш генератор Liquibase-скриптов. Исходя из нашего опыта (который совпадает с опытом разработчиков принявших участие в ранее упомянутом опросе), есть два основных способа создания Liquibase-скриптов:

  • Написание вручную

  • Создание скриптов путем сравнения двух баз данных: исходной (представляющей фактическое состояние модели) и целевой (с предыдущим состоянием модели)

Для тех, кто предпочитает первый вариант, JPA Buddy включает уже упомянутый ранее конструктор Liquibase-скриптов.

Однако, второй вариант наиболее распространен. Итак, сравниваем две базы данных. Целевую базу данных относительно легко получить, например, можно просто создать дамп базы данных продакшена. Сложности возникают с получением исходной базы данных.

Начнем с небольшой проблемы: может случиться так, что база данных, используемая для разработки на ноутбуке разработчика, не того же типа, что и база данных на продакшене. Это можно исправить, например, запустив MS SQL в Docker на Mac.

Более серьезная проблема состоит втом, что трудно найти чистую исходную базу данных, поскольку она обычно формируется впроцессе разработки генератором схемы Hibernate. Часто это приводит кбольшому количеству мусорных таблиц иатрибутов, которые вконечном итоге появляются вбазе данных продакшена. Чтобы избежать подобного рода мусора, приходится тратить еще час или два напросмотр схемы базы данных перед запуском генерации Liquibase-скриптов.

ВJPA Buddy есть замечательная функция генерации Liquibase-скриптов напрямую, путем сравнения ваших объектов JPA сцелевой базой данных (или snapshotом целевой базы данных). Выможете использовать H2в целях разработки ипо-прежнему генерировать правильные журналы изменений для Oracle, MSSQL или того, что выиспользуете впродакшене. Подобный подход гарантирует, что если увас есть мусор вжурналах изменений, это именно тот мусор, который есть увас висходном коде. Все, что вам нужно, это содержать модель данных вчистоте, итогда миграция неприведет кнежелательным артефактам вбазе данных продакшена.

Еще одна особенность генератора журнала изменений это возможность фильтровать полученные выражения по 3 категориям:

  • содержащие только безопасные операторы, которые не могут дать сбой при обновлении или полностью уронить ваше приложение, например, добавление нового столбца

  • содержащие инструкции, которые могут завершиться ошибкой при обновлении, но не привести к потере данных, например, применение ограничения уникальности

  • содержащие операторы, которые вызовут потерю данных, например, удаление таблицы или столбца

Вы сами решаете, как разделить такие утверждения: поместить их в отдельные скрипты Liquibase или просто выбрать им нужную метку или контекст в редакторе.

Заключение

В этой статье мы посмотрели на первый релиз JPA Buddy. Очевидно, что как и любой новый продукт, JPA Buddy не может полностью избежать проблем и багов, особенно в своем первом релизе. Сейчас мы концентрируемся на устранении замечаний от пользователей, улучшении существующей функциональности, определении наиболее приоритетных функций, которые мы могли упустить из виду, и их реализации.

Говоря прямо, мы хотели бы вдохновить вас стать первыми пользователями JPA Buddy и сформировать сообщество энтузиастов. Установите JPA Buddy, попользуйтесь им и поделитесь своими отзывами с нашей командой разработчиков: это очень поможетнам выбрать правильное направление развития продукта. Также подпишитесь на наш Twitter: там мы показываем, какие еще фичи есть у JPA Buddy и как ими пользоваться. Думаю, вы найдете что-то полезное именно для вас.

Подробнее..

Hibernate и Spring Boot кто отвечает за имена таблиц?

28.04.2021 16:12:49 | Автор: admin

Когда мы добавляем зависимость в проект, мы подписываем контракт. Зачастую, многие условия в нем написаны мелким шрифтом. В этой статье мы рассмотрим кое-что, что легко пропустить при подписании трехстороннего контракта между вами, Hibernate и Spring Boot. Речь пойдет о стратегиях именования.

Значения по умолчанию в JPA

Главное правило для значений по умолчанию: они должны быть интуитивно понятными. Давайте проверим, следует ли этому правилу обычное приложение на Spring Boot с конфигурацией по умолчанию, Hibernate в качестве реализации JPA и PostgreSQL в качестве БД. Допустим, у нас есть сущность PetType. Давайте угадаем, как будет называться ее таблица в базе данных.

Первый пример:

@Entitypublic class PetType {    // fields omitted}

Я бы предположил, что именем таблицы станет имя класса, то есть PetType. Однако, после запуска приложения оказалось, что имя таблицы на самом деле pet_type.

Явно зададим имя с помощью @Table:

@Entity@Table(name = "PetType")public class PetType {    // fields omitted}

В этот раз имя точно должно быть PetType. Запустим приложение Таблица снова называется pet_type!

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

@Entity@Table(name = "\"PetType\"")public class PetType {  // fields omitted}

И опять наши ожидания не оправдались, имя снова "pet_type", но теперь в кавычках!

Стратегии именования Hibernate

На запрос имя таблицы поумолчанию для JPA-сущностей Google выдает следующий результат (англ.):

По умолчанию, имя таблицы в JPA это имя класса (без пакета) с заглавной буквы. Каждый атрибут класса хранится в столбце таблицы.

Именно это мы ожидали увидеть в первом примере, не так ли? Очевидно, что-то нарушает стандарт.

Углубимся в Hibernate. Согласно документации (англ.), в Hibernate есть два интерфейса, отвечающих за именование таблиц, столбцов и пр.: ImplicitNamingStrategy и PhysicalNamingStrategy.

ImplicitNamingStrategy отвечает за генерацию имен для всех объектов, которые не были явно названы разработчиком: имя сущности, имя таблицы, имя столбца, индекса, внешнего ключа и т.д.. Получившееся имя называется логическим, оно используется внутри Hibernate для идентификации объекта. Это не то имя, которое будет использовано в базе данных.

PhysicalNamingStrategy создает подлинное физическое имя на основе логического имени объекта JPA. Именно физическое имя используется в базе данных. Фактически, это означает, что с помощью Hibernate нельзя напрямую указать физическое имя объекта в БД, можно указать только логическое. Чтобы лучше понять, как это все работает, посмотрите на схему ниже.

По умолчанию, в Hibernate используются следующие реализации этих интерфейсов: ImplicitNamingStrategyJpaCompliantImpl и PhysicalNamingStrategyStandardImpl. Первый генерирует логические имена в соответствии со спецификацией JPA, а второй использует их как физические имена без каких-либо изменений. Лучше всего это описано в документации:

JPA определяет четкие правила автоматического именования. Если вам важна независимость от конкретной реализации JPA, или если вы хотите придерживаться определенных в JPA правил именования, используйте ImplicitNamingStrategyJpaCompliantImpl (стратегия по умолчанию). Кроме того, в JPA нет разделения между логическим и физическим именем. Согласно спецификации JPA, логическое имя это и есть физическое имя. Если вам важна независимость от реализации JPA, не переопределяйте PhysicalNamingStrategy.

Однако наше приложение ведет себя по-другому. И вот почему. Spring Boot переопределяет реализации Hibernate по умолчанию для обоих интерфейсов и вместо них использует SpringImplicitNamingStrategy и SpringPhysicalNamingStrategy.

SpringImplicitNamingStrategy фактически копирует поведение ImplicitNamingStrategyJpaCompliantImpl, есть только незначительное различие в именовании join таблиц. Значит, дело в SpringPhysicalNamingStrategy. В документации (англ.) указано следующее:

По умолчанию, в Spring Boot используется SpringPhysicalNamingStrategy в качестве физической стратегии именования. Эта реализация использует те же правила именования, что и Hibernate 4:
1. Все точки заменяются символами подчеркивания.
2. Заглавные буквы CamelCase приводятся к нижнему регистру, и между ними добавляются символы подчеркивания. Кроме того, все имена таблиц генерируются в нижнем регистре. Например, сущности TelephoneNumber соответствует таблица с именем telephone_number.

По сути, Spring Boot всегда преобразовывает camelCase и PascalCase в snake_case. Более того, использование чего-либо помимо snake_case вообще невозможно. Я бы не стал использовать camelCase или PascalCase для именования объектов базы данных, но иногда у нас нет выбора. Если ваше приложение на Spring Boot работает со сторонней базой данных, где используется PascalCase или camelCase, конфигурация Spring Boot по умолчанию для вас не подойдет. Обязательно убедитесь, что используемая PhysicalNamingStrategy совместима с именами в базе данных.

Получается, Hibernate соответствует спецификации JPA, а Spring Boot нет. Можно подумать, что это ошибка. Однако Spring Boot фреймворк, в котором множество решений уже принято за разработчика, в этом и есть его ценность. Другими словами, он имеет полное право по-своему реализовывать стандарты и спецификации тех технологий, которые он использует. Для разработчика это означает следующее:

  • Конечное поведение всегда определяется конкретной реализацией и может отличаться от спецификации.

  • Если что-то и работает изкоробки, всегда нужно разбираться, что именно оно делает под капотом.

  • Поведение по умолчанию может измениться с обновлением версии библиотеки, что может привести к непредсказуемым побочным эффектам;

Заключение

Магия конфигураций изкоробки это отлично, но может привести к неожиданному поведению. Один из способов избежать подобных проблем явно задавать все значения и не полагаться на автогенерацию. Для именования объектов JPA рекомендации следующие:

  1. Всегда называйте свои объекты JPA явно, чтобы никакая автоматическая стратегия именования не повлияла на ваш код.

  2. Используйте snake_case для имен столбцов, таблиц, индексов и других объектов JPA, чтобы все реализации PhysicalNamingStrategy никак их не преобразовывали.

  3. Если нельзя использовать snake_case (например, при использовании сторонней базы данных), используйте PhysicalNamingStrategyStandardImpl в качестве PhysicalNamingStrategy.

Еще один плюс явного именования объектов JPA вы никогда случайно не переименуете таблицу или атрибут в самой базе при рефакторинге Java-модели. Для этого придется менять имя в @Table или @Column.

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

Если вы пользуетесь IntelliJ IDEA, попробуйте JPA Buddy плагин, который упрощает работу с JPA, Hibernate, Spring Data JPA, Liquibase и подобными технологиями. В настройках JPA Buddy есть специальная секция, в которой можно установить шаблоны для имен сущностей, атрибутов и пр.. Эти шаблоны применяются каждый раз, когда разработчики создают сущность или атрибут:

По задумке, настройки JPA Buddy хранятся в Git, таким образом все разработчики используют одни и те же шаблоны и следуют одним и тем же правилам.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru