Автор оригинала: 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 .