Рубрики
Без рубрики

Как настроить предложение о присоединении к ассоциации сущностей с помощью Hibernate @JoinFormula

Автор оригинала: Vlad Mihalcea.

Как я объяснил в этой предыдущей статье , вы можете сопоставить вычисляемые свойства с помощью Hibernate @Formula , и значение генерируется во время запроса.

В этом посте вы увидите, как можно использовать пользовательский фрагмент SQL для настройки связи СОЕДИНЕНИЯ между двумя сущностями или сущностью и коллекцией встраиваемых типов.

Предполагая, что у нас есть следующие сущности:

Сущность Language используется для того, чтобы мы знали, как отображать пользовательский интерфейс приложения. Объект Страна используется для того, чтобы мы могли загружать различные настройки для конкретной страны, такие как связанная Ставка НДС .

Однако таблица Учетная запись не содержит никакого ВНЕШНЕГО КЛЮЧА к таблицам Язык или Страна|/. Вместо этого сущность Account определяет только атрибут Locale , а таблица Account выглядит следующим образом:

CREATE TABLE Account (
    id int8 NOT NULL ,
    credit float8 ,
    locale VARCHAR(255) ,
    rate float8 ,
    PRIMARY KEY (id)
)

Хотя было бы лучше иметь два столбца ВНЕШНЕГО КЛЮЧА: country_id и locale_id , мы предположим, что устаревшая схема базы данных не может быть легко изменена. По этой причине нам нужно использовать столбец locale , поскольку в нем хранится информация как о языке, так и о стране. Что нам нужно сделать, так это проанализировать его и извлечь коды стран и языков, которые затем можно использовать для объединения связанных таблиц Страна и Язык .

Хотя JPA не предлагает никакой поддержки для такого сопоставления, Hibernate уже давно предлагает аннотацию @JoinFormula .

Таким образом, сопоставление Учетной записи становится:

@Entity(name = "Account")
public class Account {

    @Id
    private Long id;

    private Double credit;

    private Double rate;

    private Locale locale;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinFormula(
        "REGEXP_REPLACE(locale, '\\w+_(\\w+)[_]?', 
        '\\1')" 
    )
    private Country country;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinFormula(
        "REGEXP_REPLACE(locale, '(\\w+)_.*', 
        '\\1')"
    )
    private Language language;

    //Getters and setters omitted for brevity
}

Ассоциации @ManyToOne используют FetchType.ЛЕНИВЫЙ потому что НЕТЕРПЕЛИВАЯ выборка-это запах кода .

@JoinFormula использует потрясающую функцию REGEXP_REPLACE , которая поддерживается PostgreSQL , Oracle или MariaDB .

Для следующих тестов мы будем использовать PostgreSQL.

Предполагая, что у нас есть следующие сущности:

Country _US = new Country();
_US.setId( "US" );
_US.setName( "United States" );
_US.setVatRate(0.1);

Country _UK = new Country();
_UK.setId( "UK" );
_UK.setName( "United Kingdom" );
_UK.setVatRate(0.2);

Country _Spain = new Country();
_Spain.setId( "ES" );
_Spain.setName( "Spain" );
_Spain.setVatRate(0.21);

Country _Mexico = new Country();
_Mexico.setId( "MX" );
_Mexico.setName( "Mexico" );
_Mexico.setVatRate(0.16);

Language _English = new Language();
_English.setId( "en" );
_English.setName( "English" );

Language _Spanish = new Language();
_Spanish.setId( "es" );
_Spanish.setName( "Spanish" );

doInJPA( entityManager -> {
    entityManager.persist( _US );
    entityManager.persist( _UK );
    entityManager.persist( _Spain );
    entityManager.persist( _Mexico );
    entityManager.persist( _English );
    entityManager.persist( _Spanish );
} );

doInJPA( entityManager -> {
    Account account1 = new Account( );
    account1.setId( 1L );
    account1.setCredit( 5000d );
    account1.setRate( 1.25 / 100 );
    account1.setLocale( Locale.US );
    entityManager.persist( account1 );

    Account account2 = new Account( );
    account2.setId( 2L );
    account2.setCredit( 200d );
    account2.setRate( 1.25 / 100 );
    account2.setLocale( new Locale( "es", "MX" ) );
    entityManager.persist( account2 );
} );

Ленивое извлечение

При извлечении Учетной записи сущности Hibernate удалось получить связанные Страны и Язык сущности:

doInJPA( entityManager -> {
    LOGGER.info("Fetch first Account");
    Account account1 = entityManager.find( 
        Account.class, 1L 
    );
    assertEquals( _English, account1.getLanguage());
    assertEquals( _US, account1.getCountry());

    LOGGER.info("Fetch second Account");
    Account account2 = entityManager.find( 
        Account.class, 2L 
    );
    assertEquals( _Spanish, account2.getLanguage());
    assertEquals( _Mexico, account2.getCountry());
} );

За кулисами Hibernate выполняет следующие SQL – запросы:

-- Fetch first Account

SELECT a.id AS id1_0_0_,
       a.credit AS credit2_0_0_,
       a.locale AS locale3_0_0_,
       a.rate AS rate4_0_0_,
       REGEXP_REPLACE(
           a.locale, 
           'w+_(w+)[_]?', 
           '\1'
       ) AS formula2_0_,
       REGEXP_REPLACE(
           a.locale, 
           '(w+)_.*', 
           '\1'
       ) AS formula3_0_
FROM   Account a
WHERE  a.id = 1

SELECT l.id AS id1_2_0_,
       l.name AS name2_2_0_
FROM   Language l
WHERE  l.id = 'en'

SELECT c.id AS id1_1_0_,
       c.name AS name2_1_0_,
       c.vatRate AS vatRate3_1_0_
FROM   Country c
WHERE  c.id = 'US'

-- Fetch second Account

SELECT a.id AS id1_0_0_,
       a.credit AS credit2_0_0_,
       a.locale AS locale3_0_0_,
       a.rate AS rate4_0_0_,
       REGEXP_REPLACE(
           a.locale, 
           'w+_(w+)[_]?', 
           '\1'
       ) AS formula2_0_,
       REGEXP_REPLACE(
           a.locale, 
           '(w+)_.*', 
           '\1'
       ) AS formula3_0_
FROM   Account a
WHERE  a.id = 2

SELECT l.id AS id1_2_0_,
       l.name AS name2_2_0_
FROM   Language l
WHERE  l.id = 'es'

SELECT c.id AS id1_1_0_,
       c.name AS name2_1_0_,
       c.vatRate AS vatRate3_1_0_
FROM   Country c
WHERE  c.id = 'MX'

Нетерпеливое извлечение

Когда ПРИСОЕДИНЯЙТЕСЬ к FETCH -на языке и стране ассоциации:

Account account1 = entityManager.createQuery(
    "select a " +
    "from Account a " +
    "join a.language l " +
    "join a.country c " +
    "where a.id = :accountId", Account.class )
.setParameter("accountId", 1L)
.getSingleResult();

assertEquals( _English, account1.getLanguage());
assertEquals( _US, account1.getCountry());

Hibernate выполняет следующий SQL-запрос:

SELECT a.id                                            AS id1_0_, 
       a.credit                                        AS credit2_0_, 
       a.locale                                        AS locale3_0_, 
       a.rate                                          AS rate4_0_, 
       REGEXP_REPLACE(a.locale, '\w+_(\w+)[_]?', '\1') AS formula2_, 
       REGEXP_REPLACE(a.locale, '(\w+)_.*', '\1')      AS formula3_ 
FROM   Account a 
INNER JOIN 
       Language l 
ON REGEXP_REPLACE(a.locale, '(\w+)_.*', '\1') = l.id 
INNER JOIN 
       Country c 
ON REGEXP_REPLACE(a.locale, '\w+_(\w+)[_]?', '\1') = c.id 
WHERE  a.id = 1 

Убедитесь, что вы используете индекс функции для выражений @JoinFormula , чтобы ускорить выполнение запроса на ОБЪЕДИНЕНИЕ.

В противном случае предложение JOIN ON потребует сканирования всей таблицы, поэтому оно будет медленным.

Поэтому нам необходимо добавить следующие два индекса в наши сценарии переноса базы данных:

CREATE INDEX account_language_idx 
ON Account (REGEXP_REPLACE(locale, '(\w+)_.*', '\1'));

CREATE INDEX account_country_idx 
ON Account (REGEXP_REPLACE(locale, '\w+_(\w+)[_]?', '\1'));

Таким образом, когда мы ОБЪЯСНЯЕМ анализируем предыдущий SQL-запрос, который соединяет Язык и Страну таблицы с Учетной записью ,//PostgreSQL генерирует следующий план выполнения:

Вложенный цикл.43..24.51) (фактический.094..0.095)
-> Вложенный цикл.29..16.34) (фактический.064..0.065)
-> Сканирование индекса с использованием ключа учетной записи на учетной записи a.14..8.16) (фактический.015..0.015)
Индекс Cond:)
-> Сканирование индекса только с использованием ключа language_pkey на языке l.14..8.16) (фактический.012..0.012)
Индекс Cond: ((a.языковой стандарт)::текст, ‘(\w+)_.*’::текст, ‘\1’::текст))
Выборка Кучи: 1
-> Сканирование индекса только с использованием ключа страны в стране c.14..8.16) (фактический.005..0.005)
-> Индекс Cond: ((a.языковой стандарт)::текст, ‘\w+_(\w+)[_]?’::текст, ‘\1’::текст))
-> Выборки Кучи: 1
Время планирования: 0,229 мс
Время выполнения: 0,171 мс

Как показано в приведенном выше плане выполнения, PostgreSQL может извлечь выгоду из этих двух индексов функций, поскольку план выполнения использует Только сканирование индекса для каждого СОЕДИНЕНИЯ отношения.

Хотя в большинстве случаев аннотация @JoinColumn является лучшим способом выражения ассоциации сущностей, если в схеме базы данных нет столбца ВНЕШНЕГО КЛЮЧА, который вы могли бы использовать, то @JoinFormula становится очень полезным.

Код доступен на GitHub .