1. Створення власного конвертера типів

Іноді виникають ситуації, коли ти хочеш зберегти до одної колонки таблиці досить складний тип даних. Якщо Hibernate знає, як перетворити його на рядок (і назад), то все добре. Якщо ж ні, то тобі доведеться написати власний конвертер даних.

Припустимо, хтось вирішив зберігати рік народження користувача у базі у вигляді ГГ.ММ.ДД, наприклад: 98.12.15. Тобі ж потрібно перетворити його на звичайну дату: 15/12/1998. Тоді доведеться написати власний конвертер.

Для цього потрібно реалізувати інтерфейс AttributeConverter<EntityType, DbType>.


@Converter(autoApply = true)
public class DateConverter implements AttributeConverter<java.time.LocalDate , String> {
 
    public String convertToDatabaseColumn(java.time.LocalDate date) {
    return date.format("YY.MM.DD");
    }
 
    public java.time.LocalDate convertToEntityAttribute(String dbData) {
    String[] data = dbData.split(".");
    return LocalDate.of(data[2], data[1], "19"+data[0]);
    }
}

І, звісно, цей конвертер можна додати до будь-якого поля (за умови збігу типів):


@Entity
@Table(name="user")
class User {
   @Id
   @Column(name="id")
   public Integer id;
 
   @Column(name="join_date")
   @Convert(converter = DateConverter.class)
   public java.time.LocalDate date;
}

Конвертери часто доводиться використовувати, якщо не ти проєктував базу даних. Дані там можуть бути у "дивних форматах". Дати можуть зберігатися як рядки, Boolean як CHAR зі значеннями Y та N тощо.

2. Створюємо власний тип даних

Пам'ятаєш таблицю зі списком типів, які відомі Hibernate? Я про типи, які вказуються разом із анотацією @Type. Ти можеш написати власний тип даних, який можна використовувати так само, як і інші вбудовані типи в Hibernate.

Наприклад, ми хочемо мати тип LocalTime, який зберігатиметься в базі не як TIME, а як VARCHAR. І, наприклад, ми маємо доступ до такої бази, і нам не дозволяють поміняти типи даних у її колонках. Тоді ми можемо написати свій Hibernate-тип. Назвемо його LocalTimeString.

Для початку нам знадобиться невеликий клас, який описуватиме наш новий тип:


public class LocalTimeStringType extends AbstractSingleColumnStandardBasicType<<LocalTime> {
 
    public static final LocalTimeStringType INSTANCE = new LocalTimeStringType ();
 
    public LocalTimeStringType () {
    super(VarcharTypeDescriptor.INSTANCE, LocalTimeStringJavaDescriptor.INSTANCE);
    }
 
    @Override
    public String getName() {
    return "LocalTimeString";
    }
}

Це щось на зразок Enum, який складається з одного значення. Набір таких одиноких енамів і є всіма типами, відомими Hibernate.

Також нам знадобиться клас — аналог конвертера, який міститиме два методи — wrap() і unwrap() для перетворення значень типу LocalTime в String.

Ось так він виглядатиме без реалізації методів:


public class LocalTimeStringJavaDescriptor extends AbstractTypeDescriptor<LocalTime > {
 
    public static final LocalTimeStringJavaDescriptor INSTANCE = new LocalTimeStringJavaDescriptor();
 
    public LocalTimeStringJavaDescriptor() {
    super(LocalTime.class, ImmutableMutabilityPlan.INSTANCE);
    }
 
    public <X> X unwrap(LocalTime value, Class<X> type, WrapperOptions options) {
 
    }
 
    public <X> LocalTime wrap(X value, WrapperOptions options) {
 
    }
 
}

А тепер напишемо реалізацію методів:


public <X> X unwrap(LocalTime value, Class<X> type, WrapperOptions options) {
 
    if (value == null)
    return null;
 
    if (String.class.isAssignableFrom(type))
    return (X) LocalTimeType.FORMATTER.format(value);
 
    throw unknownUnwrap(type);
}

І другий метод:


@Override
public <X> LocalTime wrap(X value, WrapperOptions options) {
    if (value == null)
    return null;
 
    if(String.class.isInstance(value))
    return LocalTime.from(LocalTimeType.FORMATTER.parse((CharSequence) value));
 
    throw unknownWrap(value.getClass());
}

Готово. Цей клас можна використовувати для зберігання часу у вигляді рядка:


@Entity
@Table(name="user")
class User
{
   @Id
   @Column(name="id")
   public Integer id;
 
   @Column(name="join_time")
   @Type(type = "com.javarush.hibernate.customtypes.LocalTimeStringType")
   public java.time.LocalTime time;
}

3. Реєстрація свого типу

Також можеш зареєструвати свій тип даних під час конфігурування Hibernate. Це трохи нетривіально.


ServiceRegistry serviceRegistry = StandardServiceRegistryBuilder()
    .applySettings(getProperties()).build();
                                                                                                                                                              
    MetadataSources metadataSources = new MetadataSources(serviceRegistry);
    Metadata metadata = metadataSources
  .addAnnotatedClass(User.class)
  .getMetadataBuilder()
  .applyBasicType(LocalTimeStringType.INSTANCE)
  .build();
                                                                                                                                                              
    SessionFactory factory = metadata.buildSessionFactory();

Тобі спочатку потрібно буде отримати MetadataSources, з неї отримати MetadataBuilder, а вже за допомогою нього додавати свій клас. Можна і через hibernate.cfg.xml, але теж трохи громіздко.

Зате після реєстрації ти зможеш писати так:


@Entity
@Table(name="user")
class User
{
   @Id
   @Column(name="id")
   public Integer id;
 
   @Column(name="join_time")
   @Type(type = "LocalTimeString")
   public java.time.LocalTime time;
}