Котлин — отличный язык. По сравнению с Java он лаконичнее, выразительнее и безопаснее. Также, код написанный на Котлине полностью совместим с Java-кодом. Благодаря этой совместимости, проекты можно мигрировать на Котлин постепенно, не переписывая всё за раз. Такие миграции — одна из причин, по которой JPA может использоваться в Котлине. К тому же, JPA — это зрелая технология, знакомая разработчикам. Поэтому есть смысл ее использовать и в проектах, написанных на Котлине с нуля.  Невозможно представить JPA без сущностей. Однако, их определение в Котлине таит некоторое количество подводных камней. Давайте посмотрим, как избежать распространенных ошибок и использовать возможности Котлина на максимум. Внимание спойлер! Классы данных — не лучший вариант для сущностей. В этой статье основное внимание будет уделено Hibernate, поскольку он является несомненным лидером среди всех реализаций JPA. Сущности — это не обычные DTO. Чтобы они не просто работали, а работали хорошо, надо придерживаться определенных требований. В спецификации JPA есть свои требования к сущностям, из них нам наиболее интересны эти два: 1. Класс сущности должен иметь конструктор без параметров. Класс сущности также может иметь другие конструкторы. Конструктор без параметров должен быть public или protected. 2. Класс сущности не должен быть final. Никакие методы и атрибуты сущности не могут быть final. Этих требований достаточно, чтобы сущности работали. Чтобы они работали хорошо, определим еще парочку: 3. Lazy ассоциации должны загружаться только по явному запросу. В противном случае мы можем столкнуться с проблемами с производительностью или с LazyInitializationException. 4. Реализации equals() и hashCode() должны учитывать мутабельность сущностей. Первичные (primary) конструкторы очень удобны, так как позволяют одновременно с конструктором определить поля класса. Однако, если их использовать в классе сущности, компилятор не сгенерирует конструктор по умолчанию без параметров. Без него Hibernate не сможет создавать экземпляры сущности и будет бросать следующее исключение: org.hibernate.InstantiationException: No default constructor for entity. Чтобы решить эту проблему, можно вручную определить конструктор без параметров для всех сущностей в проекте. Но существует более удобное решение: плагин для компилятора kotlin-jpa. Этот плагин гарантирует, что конструктор без параметров будет сгенерирован в байт-коде для всех JPA-классов: @Entity, @MappedSuperclass или @Embeddable. Чтобы включить kotlin-jpa, его надо добавить в зависимости kotlin-maven-plugin и указать его в compilerPlugins: В Gradle: Согласно спецификации JPA, все классы и свойства, связанные с JPA, не должны быть final. Некоторые реализации JPA не требуют исполнения этого правила. Например, Hibernate не бросает исключение, когда встречает final класс сущности. Однако, от final класса нельзя наследоваться, из-за чего отключается механизм проксирования Hibernate. Нет прокси — нет lazy загрузки. Это означает, что ассоциации ToOne всегда будут не lazy, а eager, что может заметно сказаться на производительности. Это не проблема для EclipseLink с включенным static weaving, поскольку он не использует подклассы в механизме lazy загрузки. В отличие от Джавы, в Котлине классы, свойства и методы по умолчанию final. Поэтому их нужно явно помечать ключевым словом open: Также эту задачу можно решить автоматически с помощью плагина для компилятора all-open. Он помечает нужные классы и свойства ключевым словом open прямо в байт-коде. Главное его правильно настроить, чтобы он применялся к классам, помеченным не только @Entity, но и @MappedSuperclass и @Embeddable. Так выглядит конфигурация для Maven: И для Gradle: Классы данных (data classes) — отличная конструкция Котлина, предназначенная специально для DTO. Классы данных являются final по задумке и для них автоматически генерируются реализации equals(), hashCode() и toString(). Однако эти реализации не подходят для JPA-сущностей. И вот почему. Во-первых, классы данных по замыслу final, и сделать их open в коде нельзя. Единственный возможный вариант — это использовать плагин для компилятора all-open. Для дальнейших примеров будем использовать следующую сущность Client. У нее есть генерируемый базой данных id, поле name и две lazy ассоциации OneToMany: Ассоциации ToMany по умолчанию lazy, и это не просто так: их загрузка без необходимости может серьезно сказаться на производительности. Реализации equals(), hashCode() и toString() в классах данных используют все поля из первичного конструктора, куда могут входить и lazy ассоциации. В таком случае, вызов этих методов может привести к нежелательным запросам к БД или исключению LazyInitializationException. Как с этим быть? Метод toString() можно переопределить и исключить из него все lazy поля. Главное — случайно их не выбрать во время автогенерации метода в IDE. В JPA Buddy есть свой конструктор для toString(), который просто не дает включить lazy поля в метод. С equals() и hashCode() ситуация сложнее: недостаточно просто исключить из них lazy ассоциации, нужно что-то сделать и с мутабельными полями. Сущности мутабельны по своей природе. Даже ID зачастую генерируется базой данных, то есть изменяется после первого сохранения сущности. Получается, не существует полей, на основе которых можно было бы консистентно вычислить hashCode. Давайте запустим простой тест с сущностью Client: Assert в последней строчке упадет с ошибкой, хотя сущность добавлена в Set всего несколькими строками выше. При первом сохранении сущности ID изменяется. Соответственно, меняется и hashCode. Именно поэтому HashSet и не может найти объект, который мы только что создали, так как он ищет его в другом бакете. Проблем бы не было, если бы ID был установлен во время создания объекта сущности (например, в качестве ID использовался бы UUID, генерируемый приложением), но чаще всего за генерацию идентификаторов отвечает именно база данных. Чтобы решить эту проблему, всегда переопределяйте методы equals() и hashCode() при использовании классов данных для сущностей. Как их реализовать, можно почитать в статьях Vlad Mihalcea или Thorben Janssen. Например, для сущности Client они должны выглядеть следующим образом: Методы класса данных генерируются на основе полей, указанных в первичном конструкторе. Если он включает только неизменяемые поля, то класс данных не имеет вышеупомянутых проблем. Пример такого поля — неизменяемый ID, установленный приложением: Если же вы предпочитаете генерировать ID в базе данных, можно использовать неизменяемый @NaturalId: Так делать можно. Однако, такой подход сводит пользу от классов данных практически на нет, поскольку делает декомпозицию бесполезной и использует только одно поле в методе toString(). Обычный класс подойдет для сущностей лучше. Одно из преимуществ Котлина перед Джавой — встроенная null безопасность (null safety). Она также может быть обеспечена на стороне БД с помощью ограничений NOT NULL. Эти два подхода можно и нужно использовать вместе. Самый простой способ — определить все обязательные атрибуты в первичном конструкторе, используя non-null типы Котлина и указав nullable = false: Однако, если вам нужно исключить их из конструктора (например, в классе данных), можно либо указать значение по умолчанию, либо добавить к полю модификатор lateinit: Таким образом, если свойство точно не будет NULL в БД, мы также можем опустить все проверки на null в Kotlin-коде. Подводя итог, определяя JPA-сущности в Котлине, необходимо придерживаться следующих правил: Убедитесь, что все связанные с JPA классы и их атрибуты помечены open. Это поможет избежать серьезных проблем с производительностью и включит lazy загрузку для ассоциаций «многие/один к одному». Или используйте плагин для компилятора all-open и применяйте его ко всем классам, помеченным аннотациями @Entity, @MappedSuperclass и @Embeddable. Определите конструкторы без параметров во всех классах, связанных с JPA, или используйте плагин для компилятора kotlin-jpa. Иначе вы столкнетесь с InstantiationException. Чтобы использовать классы данных: Включите плагин all-open, как описано выше, т.к. это единственный способ пометить классы данных ключевым словом open. Переопределите методы equals(), hashCode() и toString() в соответствии со статей Vlad Mihalcea или Thorben Janssen. JPA Buddy придерживается всех этих правил и всегда генерирует для вас рабочие сущности, а также  отлично справляется с генерацией методов equals(), hashCode() и toString(). Больше примеров с тестами можно найти в нашем GitHub репозитории. User