Kotlin с JPA: как победить ад default constructor

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Знакомьтесь: «Ад пустого конструктора» в Kotlin + JPA

Если вы когда-нибудь пытались подружить Kotlin с JPA (Java Persistence API), то наверняка сталкивались с ситуацией, когда при запуске приложения Hibernate выбрасывает исключение вроде:

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

Знакомо? Ещё бы. Это та самая проблема, которая собрала тысячи просмотров на Stack Overflow и получила мрачное название «Kotlin with JPA: default constructor hell».

В этой статье мы спокойно, шаг за шагом, разберём:

  • Почему вообще возникает эта ошибка
  • Почему Kotlin «не хочет» генерировать пустой конструктор
  • Три проверенных способа решить проблему (от простого к профессиональному)
  • Какой способ выбрать для вашего проекта

Поехали!

Почему Hibernate требует конструктор без параметров?

Начнём с Java. В классической Java мы привыкли писать:

public class User {
    private Long id;
    private String name;

    // Обязательный пустой конструктор
    public User() {}

    // Геттеры, сеттеры...
}

Hibernate (и любая JPA-реализация) использует рефлексию для создания объектов. Когда он читает строку из базы данных, ему нужно создать экземпляр класса User, а затем через рефлексию заполнить поля. Для этого ему нужен конструктор без параметров (default constructor).

В Java компилятор автоматически создаёт пустой конструктор, если вы не определили ни одного другого. Поэтому проблема обычно не возникает.

А в чём особенность Kotlin?

Kotlin — язык с сильным акцентом на неизменяемость и безопасность. Взгляните на типичный Kotlin-класс:

data class User(
    val id: Long = 0,
    val name: String = ""
)

Здесь происходит две ключевых вещи:

  • Все свойства объявлены в первичном конструкторе (primary constructor)
  • Свойства объявлены как val (неизменяемые) и имеют значения по умолчанию

Kotlin не генерирует отдельный пустой конструктор, если у вас есть первичный конструктор с параметрами. Даже если всем параметрам заданы значения по умолчанию — это не считается «настоящим» конструктором без параметров на уровне байткода JVM.

Когда Hibernate пытается вызвать User.class.newInstance() через рефлексию, он не находит пустого конструктора — и падает с ошибкой.

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

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

data class User(
    val id: Long = 0,
    val name: String = ""
) {
    // Вторичный конструктор без параметров
    constructor() : this()
}

Здесь constructor() : this() означает: «вызови первичный конструктор со значениями по умолчанию».

Плюсы:

  • Очень просто и понятно
  • Не требует никаких плагинов или дополнительных настроек

Минусы:

  • Нужно писать этот конструктор для каждой сущности
  • Если у вас 10-15 сущностей — становится скучно
  • Легко забыть добавить

Способ 2: Использовать open class и сделать все свойства var

Многие примеры в интернете предлагают такое решение:

open class User {
    var id: Long = 0
    var name: String = ""
}

Здесь мы делаем класс open (чтобы Hibernate мог создать прокси), а свойства — var (изменяемые). И никакого первичного конструктора — значит, Kotlin сгенерирует пустой конструктор по умолчанию.

Плюсы:

  • Работает «из коробки»
  • Не нужно писать лишних конструкторов

Минусы:

  • Теряется вся магия Kotlin: data class, copy(), деструктуризация
  • Все поля — var, что нарушает принцип неизменяемости
  • Код становится более «Java-подобным» и менее идиоматичным для Kotlin

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

Способ 3: Профессиональный — плагин no-arg (рекомендуем)

Лучший способ — использовать официальный плагин от JetBrains: kotlin-noarg.

Этот плагин на этапе компиляции автоматически добавляет пустой конструктор в классы, помеченные специальной аннотацией (например, @Entity). Вы не видите его в коде, но JVM и Hibernate его находит.

Как подключить (Gradle):

plugins {
    kotlin("jvm") version "1.9.0"
    kotlin("plugin.jpa") version "1.9.0" // no-arg + jpa
}

Или вручную, если используете Maven:

<plugin>
    <groupId>org.jetbrains.kotlin</groupId>
    <artifactId>kotlin-maven-plugin</artifactId>
    <configuration>
        <compilerPlugins>
            <plugin>no-arg</plugin>
        </compilerPlugins>
        <pluginOptions>
            <option>no-arg:annotation=javax.persistence.Entity</option>
        </pluginOptions>
    </configuration>
</plugin>

После этого вы можете писать сущности как настоящий Kotlin-разработчик:

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

Плагин сам добавит пустой конструктор. Hibernate будет работать, а код останется красивым и лаконичным.

Плюсы:

  • Не нужно писать лишний код
  • Полностью идиоматичный Kotlin
  • Работает для всех сущностей автоматически
  • Поддерживается JetBrains (не исчезнет завтра)

Минусы:

  • Требует настройки build-системы (один раз)
  • Может конфликтовать с другими плагинами (редко)

Бонус: Что делать с data class и JPA?

Многие спорят, стоит ли использовать data class для JPA-сущностей. Вот короткий

Блоги

Рекомендаций по книгам