«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 см. в Мейвен Сентрал.

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();

As в случае аннотации @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

Чтобы продемонстрировать, как работает @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.

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

6.1. Mapping Relation With @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.baeldung.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.