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

Game-Type-of-UsersИз коробки поддерживает некий общий набор типов данных SQL и типов данных Java, а также отображений между ними. В основном в этот набор входят базовые вещи, такие как даты, строки, числа, блобы и так далее. С полным списком, а он довольно большой чтобы приводить его здесь, можно ознакомиться в документации Hibernate.

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

Для решения этой проблемы в Hibernate существует поддержка пользовательских типов данных. То есть можно написать, как тот или иной класс в Java должен сохраняться в тот или иной тип данных (читай столбец в таблице) в SQL базе.

Например, в существует встроенный тип данных inet для хранения IPv4 и IPv6 адресов. И мы хотели бы использовать этот тип данных при разработке приложения, управляющего, например,  выделением ip сетей. И у нас есть проблема: Hibernate не знает про этот тип данных ровным счётом ничего. Попробуем реализовать поддержку этого типа вручную.

Подготовка

В первую очередь создадим Maven проект и добавим в него Hibernate и JDBC драйвер PostgreSQL:

Вторым шагом настроим Hibernate на работу с Postgresql:

По сравнению с H2 базой, используемой ранее, отличия минимальные — изменился Hibernate dialect, да настройки JDBC. Так же я отключил автоматическое создание таблиц, так как нам надо создать таблицу с типом столбцов неизвестным HIbernate.

Модель данных

Описание одной сохраняемой в базе сети будет состоять из двух классов. В первом классе определяется собственно тип «ip сеть»:

Сеть состоит из адреса сети и маски сети. Во втором классе мы будем использовать объект сети как часть хранимой сущности:

Обратите внимание, что поле network встроено в сущность Network, а не связано через один-к-одному или иным типом связи. Кроме того, NetworkObject не является и сущностью. Это всего лишь обычный класс, наподобие BigDecimal или LocalDateTime. Аннотация @Type объясняет Hibernate, к кому обращаться, для преобразования этого класса в SQL столбец и обратно.

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

Чтобы реализовать поддержку собственного типа данных в Hibernate, необходимо реализовать интерфейс UserType и его методы:

Поле INSTANCE используется, например, для спецификации типов возвращаемых из SQL запросов. Методов же в интерфейсе UserType немало:

sqlTypes() собщает Hibernate, как представлять данные со стороны базы данных. В моём случае для простоты я буду использовать строковое представление.

returnedClass() наоборот, сообщает какой класс будет отдан при чтении из базы.

equals(), очевидно, сравнивает два объекта. В моей реализации я полагаюсь на корректную реализацию equals() от project lombok. Равно как и для hashCode():

Наконец, самое главное происходит в двух методах. Первый, nullSafeGet(), восстанавливает объект при чтении его из базы:

В nullSafeGet() передаётся самый обычный JDBC ResultSet, из которого мы получаем значение поля, как оно представлено в базе данных и строим каким-то образом необходимый объект из этого значения. В нашем частном случае адрес сети представляется как строка вида «адрес/маска», а адрес хоста как строка вида «адрес». Поэтому я разбиваю значение поля по символу «/», проверяю наличие маски и если она отсутствует, угадываю тип адреса (IPv4 или IPv6) и добавляю соответствующую маску хоста.

Вторая функция, nullSafeSet(), наоборот, преобразовывает значения класса в вид, пригодный для записи в базу:

В nullSafeSet() передаётся JDBC PreparedStatement, аргументы которого заполняются. В моём конкретном примере я строю строку вида «адрес/маска»

Далее следуют функции, которые нужные для поддержки кэширования и отслеживания изменений.

isMutable() сообщает Hibernate, могут ли поля объекта менять свои значения после создания или нет. Для упрощения кода я сделал NetworkObject immutable и, соответственно, возвращаю здесь false.

deepCopy() создаёт полную копию объекта. Так как мой объект immutable, я возвращаю оригинал.

assemble() и disassemble() конвертируют объект в вид, пригодный для хранения в кэше второго уровня и восстанавливают объект из вида, пригодного для хранения в кэше второго уровня. Так как NetworkObject реализует Serializable, я просто привожу его к этому интерфейсу. Как вариант реализации, я мог бы преобразовывать объект в строку и возвращать её, а впоследствии создавать из строки по новой.

Наконец replace() копирует изменения из нового объекта в старый. Так как объект immutable, достаточно вернуть оригинал.

Для тех, кому интересно, полный код пользовательского типа приведён под катом.

Тестирование

Для теста понадобится запущенный PostgreSQL, в котором мы создадим пользователя, базу данных и таблицу:

Кроме таблицы и sequence для автогенерации первичных ключей я создаю автоматическое преобразование из inet в строку и обратно. Это позволяет мне использовать строковый тип данных со стороны Hibernate.

Для сохранения в базу я использую адреса example.org:

Которые потом прочитаю из базы:

И проверю, что в базе данных они корректно представлены как ip сети:

Код примера доступен на gitbub. Код из примера использует PostgreSQL. В hibernate.cfg.xml примера следует заменить адрес сервера с ‘127.0.0.1’ на адрес вашего PostgreSQL сервера, если он установлен не на локальной машине.