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

1. Обзор

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

Хотя отображение базовых классов Java встроено в Hibernate, отображение пользовательских типов часто бывает сложным.

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

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

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

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

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

  • Основные типы - Отображение для основных типов Java

  • Встраиваемый - отображение для составных типов Java/POJO

  • Collections - Mapping для коллекции базовых и составных java

тип

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

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

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

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

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

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

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

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

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

Итак, давайте реализуем наш __LocalDateString type, который хранит тип LocalDate __Java как 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
}

Затем нам нужно переопределить методы _wrap и unwrap для преобразования типа Java в SQL. Давайте начнем с unwrap: _

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

Далее, __wrap __method:

@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 retrieval для преобразования String в Java LocalDate_ .

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

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

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

   //other fields and methods
}

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

4.2. Реализация UserType

С разнообразием базовых типов в 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 method возвращает поля типов SQL в том же порядке, в котором они объявлены в нашем классе PhoneNumber class. Точно так же returnedClass method возвращает наш тип PhoneNumber __Java.

Осталось только реализовать методы преобразования между типом 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 __method:

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

    if (Objects.isNull(value)) {
        st.setNull(index, 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:

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

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

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

Итак, давайте посмотрим на это в действии, реализовав _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 maps имен свойств нашего Address class. Что еще более важно, the getPropertyType __method возвращает типы сопоставления для каждого свойства.

Кроме того, нам также необходимо реализовать _getPropertyValue и setPropertyValue methods для отображения PreparedStatement and ResultSet indexes для ввода свойства type. В качестве примера рассмотрим 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 ‘s обычно реализуются как альтернативный механизм отображения __Embeddable __types.

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

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

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

Итак, давайте реализуем наш параметризованный _SalaryType , который принимает currency _ как параметр:

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 ​​() method. Теперь параметр SalaryType accept a currency _ будет преобразовывать любую сумму перед ее сохранением.

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

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

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.getMetadataBuilder()
      .applyBasicType(LocalDateStringType.INSTANCE)
      .build();

    return metadata.getSessionFactoryBuilder().build()
}

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 _ .

В качестве альтернативы, мы можем пропустить регистрацию типа, определив 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. Кроме того, ** мы реализовали несколько пользовательских типов для нашего класса сущностей на основе некоторых распространенных случаев, когда новый пользовательский тип может пригодиться.

Как всегда, примеры кода доступны over на GitHub .