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

Пользовательские типы в Hibernate и аннотация @Type

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

1. Обзор

Hibernate упрощает обработку данных между SQL и JDBC, сопоставляя объектно-ориентированную модель в Java с реляционной моделью в базах данных. Хотя сопоставление базовых классов Java встроено в Hibernate, сопоставление пользовательских типов часто бывает сложным.

В этом руководстве мы увидим, как Hibernate позволяет нам расширить базовое сопоставление типов на пользовательские классы Java. В дополнение к этому мы также увидим некоторые распространенные примеры пользовательских типов и реализуем их с использованием механизма сопоставления типов Hibernate.

2. Типы отображения гибернации

Hibernate использует типы отображения для преобразования объектов Java в запросы SQL для хранения данных. Точно так же он использует типы сопоставления для преобразования SQL ResultSet в объекты Java при извлечении данных.

Как правило, Hibernate классифицирует типы на Entity Types и Value Types . В частности, типы Entity используются для сопоставления сущностей Java, специфичных для предметной области, и, следовательно, существуют независимо от других типов в приложении. Напротив, типы значений используются для сопоставления объектов данных и почти всегда принадлежат сущностям.

В этом руководстве мы сосредоточимся на сопоставлении типов значений, которые далее классифицируются на:

  • Базовые типы — сопоставление основных типов Java.
  • Embeddable — сопоставление для составных типов Java/POJO
  • Коллекции — Сопоставление коллекции базового и составного типа Java.

3. Зависимости Maven

Чтобы создать наши собственные типы Hibernate, нам понадобится зависимость hibernate-core :

<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>5.6.7.Final</version>
</dependency>

4. Пользовательские типы в Hibernate

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

Hibernate упрощает реализацию пользовательских типов. Существует три подхода к реализации пользовательского типа в Hibernate. Давайте подробно обсудим каждый из них.

4.1. Реализация базового типа

Мы можем создать собственный базовый тип, внедрив Hibernate BasicType или одну из его конкретных реализаций, AbstractSingleColumnStandardBasicType.

Прежде чем мы реализуем наш первый пользовательский тип, давайте рассмотрим распространенный вариант использования для реализации базового типа. Предположим, нам нужно работать с устаревшей базой данных, в которой даты хранятся как VARCHAR. Обычно Hibernate сопоставляет это с типом String Java. Таким образом, усложняется проверка даты для разработчиков приложений.

Итак, давайте реализуем наш тип LocalDateString , который хранит тип Java LocalDate как VARCHAR:

public class LocalDateStringType 
extends AbstractSingleColumnStandardBasicType<LocalDate> {

public static final LocalDateStringType INSTANCE = new LocalDateStringType();

public LocalDateStringType() {
super(VarcharTypeDescriptor.INSTANCE, LocalDateStringJavaDescriptor.INSTANCE);
}

@Override
public String getName() {
return "LocalDateString";
}
}

Самое главное в этом коде — параметры конструктора. Во-первых, это экземпляр SqlTypeDescriptor , который является представлением типа SQL Hibernate, в нашем примере это VARCHAR. И второй аргумент — это экземпляр JavaTypeDescriptor , который представляет тип Java.

Теперь мы можем реализовать LocalDateStringJavaDescriptor для хранения и получения LocalDate как VARCHAR:

public class LocalDateStringJavaDescriptor extends AbstractTypeDescriptor<LocalDate> {

public static final LocalDateStringJavaDescriptor INSTANCE =
new LocalDateStringJavaDescriptor();

public LocalDateStringJavaDescriptor() {
super(LocalDate.class, ImmutableMutabilityPlan.INSTANCE);
}

// other methods
}

Далее нам нужно переопределить методы переноса и развертывания для преобразования типа Java в SQL. Начнем с развёртки:

@Override
public <X> X unwrap(LocalDate value, Class<X> type, WrapperOptions options) {

if (value == null)
return null;

if (String.class.isAssignableFrom(type))
return (X) LocalDateType.FORMATTER.format(value);

throw unknownUnwrap(type);
}

Далее метод обертывания :

@Override
public <X> LocalDate wrap(X value, WrapperOptions options) {
if (value == null)
return null;

if(String.class.isInstance(value))
return LocalDate.from(LocalDateType.FORMATTER.parse((CharSequence) value));

throw unknownWrap(value.getClass());
}

unwrap() вызывается во время привязки PreparedStatement для преобразования LocalDate в тип String, который сопоставляется с VARCHAR. Аналогично, метод wrap() вызывается во время извлечения ResultSet для преобразования String в Java LocalDate .

Наконец, мы можем использовать наш пользовательский тип в нашем классе Entity:

@Entity
@Table(name = "OfficeEmployee")
public class OfficeEmployee {

@Column
@Type(type = "com.foreach.hibernate.customtypes.LocalDateStringType")
private LocalDate dateOfJoining;

// other fields and methods
}

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

4.2. Реализация пользовательского типа

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

Итак, давайте реализуем сложный объект PhoneNumber , реализуя UserType:

public class PhoneNumberType implements UserType {
@Override
public int[] sqlTypes() {
return new int[]{Types.INTEGER, Types.INTEGER, Types.INTEGER};
}

@Override
public Class returnedClass() {
return PhoneNumber.class;
}

// other methods
}

Здесь переопределенный метод sqlTypes возвращает типы полей SQL в том же порядке, в котором они объявлены в нашем классе PhoneNumber . Точно так же метод returnClass возвращает наш Java-тип PhoneNumber. ``

Осталось только реализовать методы преобразования между типом Java и типом SQL, как мы сделали для нашего BasicType .

Во-первых, метод nullSafeGet :

@Override
public Object nullSafeGet(ResultSet rs, String[] names,
SharedSessionContractImplementor session, Object owner)
throws HibernateException, SQLException {
int countryCode = rs.getInt(names[0]);

if (rs.wasNull())
return null;

int cityCode = rs.getInt(names[1]);
int number = rs.getInt(names[2]);
PhoneNumber employeeNumber = new PhoneNumber(countryCode, cityCode, number);

return employeeNumber;
}

Далее метод nullSafeSet :

@Override
public void nullSafeSet(PreparedStatement st, Object value,
int index, SharedSessionContractImplementor session)
throws HibernateException, SQLException {

if (Objects.isNull(value)) {
st.setNull(index, Types.INTEGER);
st.setNull(index + 1, Types.INTEGER);
st.setNull(index + 2, Types.INTEGER);
} else {
PhoneNumber employeeNumber = (PhoneNumber) value;
st.setInt(index,employeeNumber.getCountryCode());
st.setInt(index+1,employeeNumber.getCityCode());
st.setInt(index+2,employeeNumber.getNumber());
}
}

Наконец, мы можем объявить наш пользовательский PhoneNumberType в нашем классе объектов OfficeEmployee :

@Entity
@Table(name = "OfficeEmployee")
public class OfficeEmployee {

@Columns(columns = { @Column(name = "country_code"),
@Column(name = "city_code"), @Column(name = "number") })
@Type(type = "com.foreach.hibernate.customtypes.PhoneNumberType")
private PhoneNumber employeeNumber;

// other fields and methods
}

4.3. Реализация CompositeUserType

Реализация UserType хорошо работает для простых типов. Однако сопоставление сложных типов Java (с наборами и каскадными составными типами) требует большей сложности. Hibernate позволяет нам сопоставлять такие типы, реализуя интерфейс CompositeUserType .

Итак, давайте посмотрим на это в действии, реализуя AddressType для сущности OfficeEmployee , которую мы использовали ранее:

public class AddressType implements CompositeUserType {

@Override
public String[] getPropertyNames() {
return new String[] { "addressLine1", "addressLine2",
"city", "country", "zipcode" };
}

@Override
public Type[] getPropertyTypes() {
return new Type[] { StringType.INSTANCE,
StringType.INSTANCE,
StringType.INSTANCE,
StringType.INSTANCE,
IntegerType.INSTANCE };
}

// other methods
}

В отличие от UserTypes , который сопоставляет индекс свойств типа, CompositeType сопоставляет имена свойств нашего класса Address . Что еще более важно, метод getPropertyType возвращает типы сопоставления для каждого свойства.

Кроме того, нам также необходимо реализовать методы getPropertyValue и setPropertyValue для сопоставления индексов PreparedStatement и ResultSet со свойством типа. В качестве примера рассмотрим getPropertyValue для нашего AddressType:

@Override
public Object getPropertyValue(Object component, int property) throws HibernateException {

Address empAdd = (Address) component;

switch (property) {
case 0:
return empAdd.getAddressLine1();
case 1:
return empAdd.getAddressLine2();
case 2:
return empAdd.getCity();
case 3:
return empAdd.getCountry();
case 4:
return Integer.valueOf(empAdd.getZipCode());
}

throw new IllegalArgumentException(property + " is an invalid property index for class type "
+ component.getClass().getName());
}

Наконец, нам потребуется реализовать методы nullSafeGet и nullSafeSet для преобразования между типами Java и SQL. Это похоже на то, что мы делали ранее в нашем PhoneNumberType.

Обратите внимание, что CompositeType обычно реализуется как альтернативный механизм сопоставления с типами Embeddable .

4.4. Тип Параметризация

Помимо создания пользовательских типов, Hibernate также позволяет нам изменять поведение типов на основе параметров.

Например, предположим, что нам нужно сохранить зарплату нашего OfficeEmployee. Что еще более важно, приложение должно преобразовывать сумму заработной платы `` в местную географическую валюту.

Итак, давайте реализуем наш параметризованный SalaryType , который принимает валюту в качестве параметра:

public class SalaryType implements CompositeUserType, DynamicParameterizedType {

private String localCurrency;

@Override
public void setParameterValues(Properties parameters) {
this.localCurrency = parameters.getProperty("currency");
}

// other method implementations from CompositeUserType
}

Обратите внимание, что мы пропустили методы CompositeUserType из нашего примера, чтобы сосредоточиться на параметризации. Здесь мы просто реализовали DynamicParameterizedType Hibernate и переопределили метод setParameterValues() . Теперь SalaryType принимает параметр валюты и преобразует любую сумму перед ее сохранением.

Мы передадим валюту в качестве параметра при объявлении зарплаты:

@Entity
@Table(name = "OfficeEmployee")
public class OfficeEmployee {

@Type(type = "com.foreach.hibernate.customtypes.SalaryType",
parameters = { @Parameter(name = "currency", value = "USD") })
@Columns(columns = { @Column(name = "amount"), @Column(name = "currency") })
private Salary salary;

// other fields and methods
}

5. Базовый реестр типов

Hibernate поддерживает сопоставление всех встроенных базовых типов в BasicTypeRegistry . Таким образом, устраняется необходимость аннотировать информацию об отображении для таких типов.

Кроме того, Hibernate позволяет нам регистрировать пользовательские типы, как и базовые типы, в BasicTypeRegistry . Обычно приложения регистрируют пользовательские типы при начальной загрузке SessionFactory. Давайте разберемся в этом, зарегистрировав тип LocalDateString , который мы реализовали ранее:

private static SessionFactory makeSessionFactory() {
ServiceRegistry serviceRegistry = StandardServiceRegistryBuilder()
.applySettings(getProperties()).build();

MetadataSources metadataSources = new MetadataSources(serviceRegistry);
Metadata metadata = metadataSources
.addAnnotatedClass(OfficeEmployee.class)
.getMetadataBuilder()
.applyBasicType(LocalDateStringType.INSTANCE)
.build();

return metadata.buildSessionFactory()
}

private static Properties getProperties() {
// return hibernate properties
}

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

@Entity
@Table(name = "OfficeEmployee")
public class OfficeEmployee {

@Column
@Type(type = "LocalDateString")
private LocalDate dateOfJoining;

// other methods
}

Здесь LocalDateString — это ключ, с которым сопоставляется тип LocalDateStringType .

В качестве альтернативы мы можем пропустить регистрацию Type, определив TypeDefs:

@TypeDef(name = "PhoneNumber", typeClass = PhoneNumberType.class, 
defaultForType = PhoneNumber.class)
@Entity
@Table(name = "OfficeEmployee")
public class OfficeEmployee {

@Columns(columns = {@Column(name = "country_code"),
@Column(name = "city_code"),
@Column(name = "number")})
private PhoneNumber employeeNumber;

// other methods
}

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

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

Как всегда, образцы кода доступны на GitHub .