Рубрики
Без рубрики

Пользовательские типы в режиме гибернации и аннотация @Type

Узнайте, как использовать пользовательские классы Java для расширения базового сопоставления типов в режиме гибернации

Автор оригинала: baeldung.

1. Обзор

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

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

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

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

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

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

  • Основные типы – Сопоставление для основных типов Java
  • Встраиваемый – Отображение для составных типов java/POJO’s
  • Коллекции – Сопоставление для коллекции базового и составного типа java

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

Чтобы создать наши пользовательские типы гибернации, нам понадобится hibernate-core зависимость:


    org.hibernate
    hibernate-core
    5.3.6.Final

4. Пользовательские типы в спящем режиме

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

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

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

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

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

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

public class LocalDateStringType 
  extends AbstractSingleColumnStandardBasicType {

    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.

Теперь мы можем реализовать Локальный дескриптор строки даты Java для хранения и извлечения Локальных данных в виде VARCHAR:

public class LocalDateStringJavaDescriptor extends AbstractTypeDescriptor {

    public static final LocalDateStringJavaDescriptor INSTANCE = 
      new LocalDateStringJavaDescriptor();

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

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

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

    if (value == null)
        return null;

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

    throw unknownUnwrap(type);
}

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

@Override
public  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 в строковый тип, который сопоставляется с VARCHAR. Аналогично, wrap() вызывается во время Результирующего набора извлечения для преобразования String в Java LocalDate .

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

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

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

    // other fields and methods
}

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

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

Учитывая разнообразие базовых типов в спящем режиме, очень редко возникает необходимость в реализации пользовательского базового типа. Напротив, более типичным вариантом использования является сопоставление сложного объекта домена 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 . Аналогично, возвращаемый класс метод возвращает наш Номер телефона Тип Java.

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

Во-первых, метод 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 в нашем Office Employee классе сущностей:

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

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

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
}

В отличие от Типов пользователей , которые отображают индекс свойств типа, Составной тип отображает имена свойств нашего Адреса класса. Что еще более важно, метод getPropertyType возвращает типы сопоставления для каждого свойства.

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

@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. Это похоже на то, что мы делали ранее в нашем типе номера телефона .

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

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

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

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

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

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

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

@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. Давайте разберемся в этом, зарегистрировав тип Local Date String , который мы реализовали ранее:

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 – это ключ, к которому сопоставляется Локальный тип строки даты|/.

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

@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. Заключение

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

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