Перейти к основному содержимому

Динамическое сопоставление с Hibernate

· 8 мин. чтения

1. Введение

В этой статье мы рассмотрим некоторые возможности динамического сопоставления Hibernate с аннотациями @Formula , @Where , @Filter и @Any .

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

2. Настройка проекта

Чтобы продемонстрировать функции, нам понадобится только библиотека hibernate-core и резервная база данных H2:

<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>5.4.12.Final</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.194</version>
</dependency>

Текущую версию библиотеки hibernate-core можно найти на Maven Central .

3. Вычисляемые столбцы с помощью @Formula

Предположим, мы хотим вычислить значение поля объекта на основе некоторых других свойств. Один из способов сделать это — определить вычисляемое поле только для чтения в нашей сущности Java:

@Entity
public class Employee implements Serializable {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;

private long grossIncome;

private int taxInPercents;

public long getTaxJavaWay() {
return grossIncome * taxInPercents / 100;
}

}

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

Было бы намного проще получить уже рассчитанное значение из базы данных. Это можно сделать с помощью аннотации @Formula :

@Entity
public class Employee implements Serializable {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;

private long grossIncome;

private int taxInPercents;

@Formula("grossIncome * taxInPercents / 100")
private long tax;

}

С @Formula мы можем использовать подзапросы, вызывать собственные функции базы данных и хранимые процедуры и в основном делать все, что не нарушает синтаксис предложения выбора SQL для этого поля.

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

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

Employee employee = new Employee(10_000L, 25);
session.save(employee);

session.flush();
session.clear();

employee = session.get(Employee.class, employee.getId());
assertThat(employee.getTax()).isEqualTo(2_500L);

4. Фильтрация объектов с помощью @Where

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

Например, нам нужно реализовать «обратимое удаление». Это означает, что сущность никогда не удаляется из базы данных, а только помечается как удаленная с помощью логического поля.

Мы должны быть очень осторожны со всеми существующими и будущими запросами в приложении. Нам пришлось бы предоставлять это дополнительное условие для каждого запроса. К счастью, Hibernate предоставляет возможность сделать это в одном месте:

@Entity
@Where(clause = "deleted = false")
public class Employee implements Serializable {

// ...
}

Аннотация @Where к методу содержит предложение SQL, которое будет добавлено к любому запросу или подзапросу к этому объекту:

employee.setDeleted(true);

session.flush();
session.clear();

employee = session.find(Employee.class, employee.getId());
assertThat(employee).isNull();

Как и в случае с аннотацией @Formula , поскольку мы имеем дело с необработанным SQL, условие @Where не будет переоценено до тех пор, пока мы не сбросим сущность в базу данных и не исключим ее из контекста .

До этого момента объект будет оставаться в контексте и будет доступен для запросов и поиска по идентификатору .

Аннотацию @Where также можно использовать для поля коллекции. Предположим, у нас есть список удаляемых телефонов:

@Entity
public class Phone implements Serializable {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;

private boolean deleted;

private String number;

}

Затем со стороны сотрудника мы могли бы отобразить набор удаляемых телефонов следующим образом:

public class Employee implements Serializable {

// ...

@OneToMany
@JoinColumn(name = "employee_id")
@Where(clause = "deleted = false")
private Set<Phone> phones = new HashSet<>(0);

}

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

employee.getPhones().iterator().next().setDeleted(true);
session.flush();
session.clear();

employee = session.find(Employee.class, employee.getId());
assertThat(employee.getPhones()).hasSize(1);

List<Phone> fullPhoneList
= session.createQuery("from Phone").getResultList();
assertThat(fullPhoneList).hasSize(2);

5. Параметризованная фильтрация с помощью @Filter

Проблема с аннотацией @Where заключается в том, что она позволяет нам указать только статический запрос без параметров, и ее нельзя отключить или включить по требованию.

Аннотация @Filter работает так же, как @Where , но ее также можно включить или отключить на уровне сеанса, а также параметризовать.

5.1. Определение @фильтра

Чтобы продемонстрировать, как работает @Filter , давайте сначала добавим следующее определение фильтра к сущности Employee :

@FilterDef(
name = "incomeLevelFilter",
parameters = @ParamDef(name = "incomeLimit", type = "int")
)
@Filter(
name = "incomeLevelFilter",
condition = "grossIncome > :incomeLimit"
)
public class Employee implements Serializable {

Аннотация @FilterDef определяет имя фильтра и набор его параметров, которые будут участвовать в запросе. Тип параметра — имя одного из типов Hibernate ( Type , UserType или CompositeUserType ), в нашем случае — int .

Аннотация @FilterDef может быть размещена либо на уровне типа, либо на уровне пакета. Обратите внимание, что в нем не указывается само условие фильтра (хотя мы могли бы указать параметр defaultCondition ).

Это означает, что мы можем определить фильтр (его имя и набор параметров) в одном месте, а затем по-разному определить условия для фильтра в нескольких других местах.

Это можно сделать с помощью аннотации @Filter . В нашем случае мы поместили его в тот же класс для простоты. Синтаксис условия представляет собой необработанный SQL с именами параметров, перед которыми стоят двоеточия.

5.2. Доступ к отфильтрованным объектам

Еще одно отличие @Filter от @Where заключается в том, что @Filter не включен по умолчанию. Мы должны включить его на уровне сеанса вручную и указать для него значения параметров:

session.enableFilter("incomeLevelFilter")
.setParameter("incomeLimit", 11_000);

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

session.save(new Employee(10_000, 25));
session.save(new Employee(12_000, 25));
session.save(new Employee(15_000, 25));

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

List<Employee> employees = session.createQuery("from Employee")
.getResultList();
assertThat(employees).hasSize(2);

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

session = HibernateUtil.getSessionFactory().openSession();
employees = session.createQuery("from Employee").getResultList();
assertThat(employees).hasSize(3);

Также при прямой выборке сущности по id фильтр не применяется:

Employee employee = session.get(Employee.class, 1);
assertThat(employee.getGrossIncome()).isEqualTo(10_000);

5.3. @Filter и кэширование второго уровня

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

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

Вот почему аннотация @Filter в основном отключает кэширование объекта.

6. Отображение любой ссылки на сущность с помощью @Any

Иногда нам нужно отобразить ссылку на любой из нескольких типов объектов, даже если они не основаны на одном @MappedSuperclass . Их даже можно сопоставить с разными несвязанными таблицами. Мы можем добиться этого с помощью аннотации @Any .

В нашем примере нам нужно будет прикрепить описание к каждому объекту в нашей единице персистентности , а именно к Employee и Phone . Было бы неразумно наследовать все сущности от одного абстрактного суперкласса только для этого.

6.1. Отображение отношения с @Any

Вот как мы можем определить ссылку на любой объект, реализующий Serializable (т. е. на любой объект вообще):

@Entity
public class EntityDescription implements Serializable {

private String description;

@Any(
metaDef = "EntityDescriptionMetaDef",
metaColumn = @Column(name = "entity_type"))
@JoinColumn(name = "entity_id")
private Serializable entity;

}

Свойство metaDef — это имя определения, а metaColumn — это имя столбца, который будет использоваться для различения типа объекта (в отличие от столбца-дискриминатора в сопоставлении иерархии одной таблицы).

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

Столбец entity_id также не может быть уникальным, потому что разные таблицы могут иметь повторяющиеся идентификаторы.

Однако пара entity_type / entity_id должна быть уникальной, поскольку она однозначно описывает объект, на который мы ссылаемся.

6.2. Определение сопоставления @Any с помощью @AnyMetaDef

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

Чтобы это работало, нам нужно добавить метаопределение сопоставления с аннотацией @AnyMetaDef . Лучше всего разместить его на уровне пакета, чтобы мы могли повторно использовать его в других сопоставлениях.

Вот как будет выглядеть файл package-info.java с аннотацией @AnyMetaDef :

@AnyMetaDef(
name = "EntityDescriptionMetaDef",
metaType = "string",
idType = "int",
metaValues = {
@MetaValue(value = "Employee", targetEntity = Employee.class),
@MetaValue(value = "Phone", targetEntity = Phone.class)
}
)
package com.foreach.hibernate.pojo;

Здесь мы указали тип столбца entity_type ( string ), тип столбца entity_id ( int ), допустимые значения в столбце entity_type ( «Сотрудник» и «Телефон» ) и соответствующие типы сущностей.

Теперь предположим, что у нас есть сотрудник с двумя телефонами, описанный так:

Employee employee = new Employee();
Phone phone1 = new Phone("555-45-67");
Phone phone2 = new Phone("555-89-01");
employee.getPhones().add(phone1);
employee.getPhones().add(phone2);

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

EntityDescription employeeDescription = new EntityDescription(
"Send to conference next year", employee);
EntityDescription phone1Description = new EntityDescription(
"Home phone (do not call after 10PM)", phone1);
EntityDescription phone2Description = new EntityDescription(
"Work phone", phone1);

7. Заключение

В этой статье мы рассмотрели некоторые аннотации Hibernate, которые позволяют точно настроить сопоставление сущностей с помощью необработанного SQL.

Исходный код статьи доступен на GitHub .