null

Неоднозначные отношения Kotlin с Hibernate

Kotlin позиционируется как язык, который призван сделать жизнь Java разработчика счастливее. Код становится лаконичнее, Null-safety спасает от NPE, а стандартная библиотека - удовольствие. Но когда мы подключаем к этому котлу JPA-провайдер (например Hibernate), начинаются интересные сюрпризы.

Хотя Kotlin и Java отлично совместимы, их парадигмы иногда конфликтуют. Hibernate изначально проектировался под Java-бины, а Kotlin привносит свои концепции: immutable data classes, свойства вместо геттеров/сеттеров и строгую типизацию.

Ловушка data class и equals/hashCode

Это, пожалуй, самая популярная ошибка. Все мы любим data class, но сущности помеченные @Entity - это не просто DTO.

@Entity

data class User(
 @Id 
 @GeneratedValue
 val id: Long? = null,
 val username: String
)

Так в чем же проблема? Первое - это Lazy Loading: Hibernate создает прокси-объекты, наследующие сущность. Если в equals используется == или ===, вы можете получить некорректные сравнения. Второе это - изменение ID: Когда вы сохраняете новую сущность, Hibernate присваивает ей ID. Если equals/hashCode опираются на ID (как генерирует data class), хеш-код объекта изменяется прямо в момент сохранения. Если этот объект лежал в HashSet, он «потеряется», так как хеш изменился после добавления.

Не используйте data class для сущностей, либо отключите генерацию методов и напишите их руками, используя бизнес-ключ (например, уникальный username), а не ID.

Проблема final и отсутствие open

В Kotlin все классы и методы по умолчанию final. Hibernate же требует наследоваться от класса сущности для создания прокси (например, для Lazy Loading). Без дополнительных настроек вы получите ошибку при попытке подгрузить данные лениво.

Самый простой способ - использовать магию плагинов в build.gradle. Плагин kotlin-spring (или all-open) делает все классы, помеченные аннотациями (в т.ч. @Entity), открытыми, а плагин kotlin-jpa еще и open-ит методы, имеющие аннотации @Entity и т.д.

Конструкторы без аргументов

JPA-провайдеру нужен конструктор по умолчанию (без аргументов), чтобы создать экземпляр класса через рефлексию, а затем заполнить его поля данными из ResultSet. В Kotlin, если вы напишете primary конструктор с параметрами, конструктора по умолчанию не будет.

Здесь на помощь приходит тот же плагин kotlin-jpa. Он сгенерирует для вас невидимый конструктор без аргументов, если вы пометите класс аннотацией @Entity.

Проблема Default values

Допустим, у нас есть такое поле:

@Entity

class Task(

   val status: String = "NEW"

)

Разработчик ожидает, что если он сохранит объект, не задавая статус, то в БД будет "NEW". Но Hibernate при построении SQL-запроса (INSERT) смотрит на значение поля. Если мы создали объект через new Task(), в памяти статус "NEW". Но если мы мапим результат из базы, Hibernate использует конструктор без аргументов (о котором говорили выше) и сеттеры.

Если в БД колонка не имеет DEFAULT, а вы передаете null (или Hibrenate решит не вставлять это поле), возникнет ошибка. И наоборот, значение по умолчанию в Kotlin не синхронизируется со схемой БД.

Всегда дублируйте дефолтные значения на уровне бд и инициализируйте поле в классе. Либо, что более надежно в Enterprise-разработке, используйте @PrePersist.

 

В итоге получается, что Kotlin и Hibernate - отличная связка, но она требует понимания того, что происходит «под капотом». Плагины kotlin-jpa и kotlin-allopen решают 90% проблем с магией байт-кода, а остальные 10% решаются внимательным отношением к equals, hashCode и контрактам JPA.

Вперед

Коротко о себе:

Работаю кем-то в компании Tune-it. На работе занимаюсь какими-то проектами, связанными с чем-то.

Ничего не найдено. n is 0