Avatar ·

Kotlin с JPA/Hibernate: проблема отсутствия конструктора без параметров (default constructor hell)

Всем привет! Я пишу на Kotlin около полугода, и вот столкнулся с проблемой при использовании JPA/Hibernate. У меня есть простая сущность:

@Entity
data class User(
    @Id @GeneratedValue
    val id: Long = 0,
    val name: String,
    val email: String
)

При запуске приложения Hibernate выбрасывает исключение:

org.hibernate.InstantiationException: No default constructor for entity: com.example.User

Я понимаю, что Hibernate требует конструктор без параметров, чтобы создавать объекты через рефлексию. Но в Kotlin data class'ы не имеют такого конструктора, если у параметров нет значений по умолчанию. Я пробовал добавлять @NoArg из Jackson, но это не помогло. Также слышал про плагин kotlin-jpa, но не уверен, как его правильно настроить.

Вопрос: как правильно организовать сущности JPA в Kotlin, чтобы не было проблем с отсутствием конструктора по умолчанию? Есть ли стандартное решение, которое не ломает читаемость кода и не заставляет писать лишние костыли? Буду благодарен за примеры и объяснения.

P.S. Версия Kotlin 1.9, Hibernate 6, Spring Boot 3.

<<>>

Решение: плагин kotlin-jpa и правильная настройка сущностей

Проблема, которую вы описали, действительно является классическим «адским» моментом при переходе на Kotlin с Java. Hibernate требует конструктор без параметров, потому что он создаёт объекты через рефлексию, а потом заполняет поля через сеттеры или прямую установку полей. В Kotlin data class'ы по умолчанию не генерируют такой конструктор, если все параметры обязательны.

Вот лучшее и наиболее поддерживаемое решение — использовать официальный плагин kotlin-jpa, который автоматически добавляет конструктор без параметров для всех классов, аннотированных @Entity, @Embeddable, @MappedSuperclass и т.д.

Шаг 1: Подключите плагин в build.gradle.kts

plugins {
    kotlin("plugin.jpa") version "1.9.22" // Версия должна совпадать с версией Kotlin
    // остальные плагины...
}

Если у вас Maven, добавьте в pom.xml:

<plugin>
    <groupId>org.jetbrains.kotlin</groupId>
    <artifactId>kotlin-maven-plugin</artifactId>
    <configuration>
        <compilerPlugins>
            <plugin>jpa</plugin>
        </compilerPlugins>
    </configuration>
</plugin>

Шаг 2: Используйте значения по умолчанию

После подключения плагина ваша сущность будет работать, но рекомендую также добавить значения по умолчанию для всех полей, чтобы Kotlin мог сгенерировать конструктор без параметров:

@Entity
data class User(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0,
    val name: String = "",
    val email: String = ""
)

Важно: Значения по умолчанию не влияют на работу Hibernate — он всё равно будет устанавливать поля через рефлексию, игнорируя эти значения. Но это делает код безопасным для Kotlin (не нужно использовать lateinit или nullable-типы без необходимости).

Шаг 3: Альтернатива — open class вместо data class

Если вы не хотите использовать data class (например, вам не нужны equals/hashCode), можно объявить обычный open class:

@Entity
open class User(
    @Id @GeneratedValue
    val id: Long = 0,
    val name: String = "",
    val email: String = ""
)

Плагин kotlin-jpa также сработает для open class.

Почему это работает?

  • Плагин kotlin-jpa на этапе компиляции добавляет в класс пустой конструктор (сгенерированный байт-код содержит public User() { }).
  • Hibernate видит этот конструктор и может создавать объекты.
  • Значения по умолчанию в конструкторе — это требование Kotlin: если у параметров есть значения по умолчанию, компилятор генерирует дополнительный синтетический конструктор без параметров, который вызывает основной с подстановкой дефолтов.

Дополнительная рекомендация: избегайте var, используйте val

Hibernate может работать с val (через рефлексию напрямую устанавливая поля), но если вы используете var, убедитесь, что сеттеры не содержат дополнительной логики. Лучший подход — оставить поля val и довериться Hibernate.

Проверка

После настройки плагина вы должны увидеть в логах подтверждение, что сущности корректно загружаются. Если ошибка остаётся — проверьте, что плагин точно подключён и версия Kotlin совпадает.

<<>>

Альтернативный подход: использование @NoArg из Jackson или ручной конструктор

Если по каким-то причинам вы не можете использовать плагин kotlin-jpa (например, у вас legacy проект или конфликт плагинов), есть несколько обходных путей.

Вариант 1: Аннотация @NoArg из Jackson

Jackson также предоставляет аннотацию @NoArg, которая генерирует конструктор без параметров. Подключите зависимость:

implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.15.+")

И добавьте аннотацию:

@Entity
@NoArg
data class User(
    @Id @GeneratedValue
    val id: Long = 0,
    val name: String,
    val email: String
)

Минус: работает не всегда стабильно и может конфликтовать с Hibernate, если аннотация не распознаётся как генератор конструктора.

Вариант 2: Ручной вторичный конструктор

Самый явный, но громоздкий способ — добавить вторичный конструктор без параметров:

@Entity
data class User(
    @Id @GeneratedValue
    val id: Long,
    val name: String,
    val email: String
) {
    constructor() : this(0, "", "")
}

Минусы: нужно дублировать значения по умолчанию для каждого поля, код становится избыточным. Если полей много (например, 10+), это неудобно.

Вариант 3: Использование lateinit var

Можно объявить поля как lateinit var, но это ломает иммутабельность и требует nullable-типов:

@Entity
class User {
    @Id @GeneratedValue
    lateinit var id: Long
    lateinit var name: String
    lateinit var email: String
}

Не рекомендуется: теряются преимущества data class (equals, copy, toString), код становится менее безопасным.

Итог

Лучший вариант — плагин kotlin-jpa. Если он недоступен, используйте ручной вторичный конструктор как временное решение, но старайтесь избегать lateinit var. Помните, что Hibernate работает с рефлексией, поэтому наличие конструктора без параметров — обязательное условие.