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

Динамическое отображение с Hibernate

1. Вступление

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

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

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

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


    org.hibernate
    hibernate-core
    5.2.12.Final


    com.h2database
    h2
    1.4.194

Для текущей версии библиотеки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;
    }

}

Очевидный недостаток -we’d have to do the recalculation each time we access this virtual field by the getter.

Было бы намного проще получить уже рассчитанное значение из базы данных. Это можно сделать с помощью аннотации@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 select для этого поля.

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

Также имейте в виду, чтоthe value is calculated when the entity is fetched from the database. Следовательно, когда мы сохраняем или обновляем сущность, значение не будет пересчитываться до тех пор, пока сущность не будет удалена из контекста и снова загружена:

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

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

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

Нам нужно очень внимательно относиться ко всем существующим и будущим запросам в приложении. Это дополнительное условие нужно указывать для каждого запроса. К счастью, 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,since we’re dealing with raw SQL, the @Where condition won’t be reevaluated until we flush the entity to the database and evict it from the context.

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

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

@Entity
public class Phone implements Serializable {

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

    private boolean deleted;

    private String number;

}

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

public class Employee implements Serializable {

    // ...

    @OneToMany
    @JoinColumn(name = "employee_id")
    @Where(clause = "deleted = false")
    private Set 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 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.

АннотацияThe @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 employees = session.createQuery("from Employee")
  .getResultList();
assertThat(employees).hasSize(2);

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

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

Кроме того, при непосредственном извлечении объекта по идентификатору фильтр не применяется:

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

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

Если у нас есть высоконагруженное приложение, мы определенно захотим включить кеш второго уровня Hibernate, что может дать огромное преимущество в производительности. Следует иметь в виду, чтоthe @Filter annotation does not play nicely with caching.

The second-level cache only keeps full unfiltered collections. В противном случае мы могли бы прочитать коллекцию в одном сеансе с включенным фильтром, а затем получить ту же кэшированную отфильтрованную коллекцию в другом сеансе даже с отключенным фильтром.

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

6. Сопоставление любой ссылки на объект с@Any

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

В нашем примереwe’ll need to attach some description to every entity in our persistence unit, а именно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 - это имя столбца, который будет использоваться для различения типа объекта (аналогично столбцу дискриминатора в отображении иерархии одной таблицы).

Мы также указываем столбец, который будет ссылаться наid объекта. Стоит отметить, чтоthis column will not be a foreign key, потому что он может ссылаться на любую таблицу, которую мы хотим.

Столбец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.example.hibernate.pojo;

Здесь мы указали тип столбцаentity_type (string), тип столбцаentity_id (int), допустимые значения вentity_type столбец (“Employee” и“Phone”) и соответствующие типы сущностей.

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

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.

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