1. Обзор

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

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

2. Типы сопоставления Hibernate

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

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

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

Базовые типы — сопоставление базовых типов Java. Встраиваемые — сопоставление составных типов Java/коллекций POJO — Сопоставление набора базовых и составных типов Java

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

Для создания наших пользовательских типов Hibernate нам понадобится зависимость hibernate-core:

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

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

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

4.1. Реализация BasicType

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

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

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

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

Теперь мы можем реализовать LocalDateStringJavaDescriptor для хранения и извлечения 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";
    }
}

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

Далее, метод переноса:

public class LocalDateStringJavaDescriptor extends AbstractTypeDescriptor<LocalDate> {

    public static final LocalDateStringJavaDescriptor INSTANCE = 
      new LocalDateStringJavaDescriptor();

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

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

@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);
}

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

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

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

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

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

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

    // other fields and methods
}

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

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

«

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

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
}	

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

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

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

@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;
}

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

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

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

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

    @Columns(columns = { @Column(name = "country_code"), 
      @Column(name = "city_code"), @Column(name = "number") })
    @Type(type = "com.baeldung.hibernate.customtypes.PhoneNumberType")
    private PhoneNumber employeeNumber;
	
    // other fields and methods
}

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

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

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

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
}

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

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

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

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

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

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

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

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

Обратите внимание, что мы пропустили методы CompositeUserType из нашего примера, чтобы сосредоточиться на параметризации. Здесь мы просто реализовали DynamicParameterizedType Hibernate и переопределили метод setParameterValues(). Теперь 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
}

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

5. Реестр базовых типов

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

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

    // other fields and methods
}

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

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

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

private static SessionFactory makeSessionFactory() {
    ServiceRegistry serviceRegistry = StandardServiceRegistryBuilder()
      .applySettings(getProperties()).build();
														  
    MetadataSources metadataSources = new MetadataSources(serviceRegistry);
    Metadata metadata = metadataSources.getMetadataBuilder()
      .applyBasicType(LocalDateStringType.INSTANCE)
      .build();
														  
    return metadata.getSessionFactoryBuilder().build()
}

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

Здесь LocalDateString является ключом к которому LocalDateStringType сопоставляется.

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

    @Column
    @Type(type = "LocalDateString")
    private LocalDate dateOfJoining;
	
    // other methods
}

Кроме того, мы можем пропустить регистрацию типа, определив TypeDefs:

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

@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
}

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

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

«