Знакомьтесь: «Ад пустого конструктора» в 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-сущностей. Вот короткий