Kotlin с JPA: ад конструктора по умолчанию


132

Как требует JPA, @Entityклассы должны иметь конструктор по умолчанию (без аргументов) для создания экземпляров объектов при их извлечении из базы данных.

В Kotlin свойства очень удобно объявлять в основном конструкторе, как в следующем примере:

class Person(val name: String, val age: Int) { /* ... */ }

Но когда конструктор без аргументов объявляется как вторичный, он требует передачи значений для первичного конструктора, поэтому для них необходимы некоторые допустимые значения, как здесь:

@Entity
class Person(val name: String, val age: Int) {
    private constructor(): this("", 0)
}

В случае, когда свойства имеют более сложный тип, чем просто Stringи, Intи они не допускают значения NULL, выглядит совершенно плохим предоставление значений для них, особенно когда в первичном конструкторе и initблоках много кода и когда параметры активно используются - - когда они будут переназначены посредством отражения, большая часть кода будет выполнена снова.

Более того, val-properties нельзя переназначить после выполнения конструктора, поэтому неизменяемость также теряется.

Возникает вопрос: как адаптировать код Kotlin для работы с JPA без дублирования кода, выбора «волшебных» начальных значений и потери неизменяемости?

PS Верно ли, что Hibernate помимо JPA может создавать объекты без конструктора по умолчанию?


1
INFO -- org.hibernate.tuple.PojoInstantiator: HHH000182: No default (no-argument) constructor for class: Test (class must be instantiated by Interceptor)- так что да, Hibernate может работать без конструктора по умолчанию.
Майкл Пифель,

То, как это происходит с сеттерами, иначе: изменчивость. Он создает конструктор по умолчанию, а затем ищет сеттеры. Мне нужны неизменяемые объекты. Единственный способ сделать это - если спящий режим начинает смотреть на конструктор. На этом hibernate.atlassian.net/browse/HHH-9440
Кристиан Бонджорно,

Ответы:


145

Начиная с Kotlin 1.0.6 , kotlin-noargплагин компилятора генерирует синтетические конструкторы по умолчанию для классов, которые были аннотированы выбранными аннотациями.

Если вы используете gradle, применения kotlin-jpaплагина достаточно, чтобы сгенерировать конструкторы по умолчанию для классов, аннотированных @Entity:

buildscript {
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-noarg:$kotlin_version"
    }
}

apply plugin: "kotlin-jpa"

Для Maven:

<plugin>
    <artifactId>kotlin-maven-plugin</artifactId>
    <groupId>org.jetbrains.kotlin</groupId>
    <version>${kotlin.version}</version>

    <configuration>
        <compilerPlugins>
            <plugin>jpa</plugin>
        </compilerPlugins>
    </configuration>

    <dependencies>
        <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-maven-noarg</artifactId>
            <version>${kotlin.version}</version>
        </dependency>
    </dependencies>
</plugin>

4
Не могли бы вы немного рассказать о том, как это будет использоваться в вашем коде kotlin, даже если это случай «ваш data class foo(bar: String)не меняется». Было бы неплохо увидеть более полный пример того, как это работает. Спасибо
thecoshman

5
Это сообщение в блоге, которое представляет kotlin-noargи содержитkotlin-jpa ссылки, подробно описывающие их цель. Blog.jetbrains.com/kotlin/2016/12/kotlin-1-0-6-is-here
Далибор Филус,

1
А как насчет класса первичного ключа, такого как CustomerEntityPK, который не является сущностью, но требует конструктора по умолчанию?
jannnik

3
У меня не работает. Это работает, только если я сделаю поля конструктора необязательными. Это означает, что плагин не работает.
Ixx 06

3
@jannnik Вы можете пометить класс первичного ключа с помощью @Embeddableатрибута, даже если он вам не нужен. Таким образом, его заберут kotlin-jpa.
svick 03

33

просто укажите значения по умолчанию для всех аргументов, Kotlin создаст для вас конструктор по умолчанию.

@Entity
data class Person(val name: String="", val age: Int=0)

см. NOTEрамку под следующим разделом:

https://kotlinlang.org/docs/reference/classes.html#secondary-constructors


18
вы, очевидно, не читали его вопрос, иначе вы бы видели ту часть, где он заявляет, что аргументы по умолчанию плохо выглядят, особенно для более сложных объектов. Не говоря уже о том, что добавление значений по умолчанию для чего-то скрывает другие проблемы.
Snowe 01

1
Почему указывать значения по умолчанию - плохая идея? Даже при использовании конструктора Java no args полям присваиваются значения по умолчанию (например, null для ссылочных типов).
Умеш Раджбхандари

1
Бывают случаи, когда вы не можете предоставить разумные значения по умолчанию. Возьмем приведенный пример человека, вам действительно следует смоделировать его с датой рождения, поскольку это не меняется (конечно, где-то применяются исключения), но для этого нет разумного значения по умолчанию. Следовательно, с точки зрения чистого кода, вы должны передать DoB в конструктор person, тем самым гарантируя, что у вас никогда не будет человека, у которого нет допустимого возраста. Проблема в том, что JPA любит работать, ему нравится создавать объект с конструктором без аргументов, а затем устанавливать все.
thecoshman

1
Я думаю, что это правильный способ сделать это, этот ответ работает в других случаях, когда вы также не используете JPA или спящий режим. также это предлагаемый способ в соответствии с документами, указанными в ответе.
Mohammad Rafigh

1
Кроме того, вы не должны использовать класс данных с JPA: «не используйте классы данных со свойствами val, потому что JPA не предназначен для работы с неизменяемыми классами или методами, автоматически генерируемыми классами данных». spring.io/guides/tutorials/spring-boot-kotlin/…
Тафсен

11

@ D3xter имеет хороший ответ для одной модели, другая - это новая функция в Kotlin, которая называется lateinit:

class Entity() {
    constructor(name: String, age: Date): this() {
        this.name = name
        this.birthdate = age
    }

    lateinit var name: String
    lateinit var birthdate: Date
}

Вы могли бы использовать это, когда уверены, что что-то заполнит значения во время создания или очень скоро после (и перед первым использованием экземпляра).

Вы заметите, что я изменил ageна, birthdateпотому что вы не можете использовать примитивные значения с, lateinitи они также должны быть на данный момент var(ограничение может быть снято в будущем).

Так что не идеальный ответ на неизменность, та же проблема, что и другой ответ в этом отношении. Решением для этого являются плагины к библиотекам, которые могут обрабатывать понимание конструктора Kotlin и сопоставление свойств с параметрами конструктора, вместо того, чтобы требовать конструктор по умолчанию. Модуль Kotlin для Джексона делает это, поэтому очевидно, что это возможно.

См. Также: https://stackoverflow.com/a/34624907/3679676 для изучения похожих вариантов.


Обратите внимание, что lateinit и Delegates.notNull () совпадают.
fasth

4
похоже, но не то же самое. Если используется делегат, он изменяет то, что Java видит для сериализации фактического поля (он видит класс делегата). Кроме того, лучше использовать, lateinitкогда у вас есть четко определенный жизненный цикл, гарантирующий инициализацию вскоре после создания, он предназначен для таких случаев. В то время как делегат больше предназначен для «когда-нибудь перед первым использованием». Хотя технически они имеют схожее поведение и защиту, они не идентичны.
Джейсон Минард

Если вам нужно использовать примитивные значения, единственное, о чем я мог бы подумать, - это использовать «значения по умолчанию» при создании экземпляра объекта, и под этим я подразумеваю использование 0 и falseдля Ints и Booleans соответственно. Не уверен, как это повлияет на код фреймворка
OzzyTheGiant

6
@Entity data class Person(/*@Id @GeneratedValue var id: Long? = null,*/
                          var name: String? = null,
                          var age: Int? = null)

Начальные значения требуются, если вы хотите повторно использовать конструктор для разных полей, kotlin не допускает нулей. Поэтому всякий раз, когда вы планируете опустить поле, используйте эту форму в конструкторе:var field: Type? = defaultValue

jpa не требует конструктора аргументов:

val entity = Person() // Person(name=null, age=null)

нет дублирования кода. Если вам нужно построить объект и установить только возраст, используйте эту форму:

val entity = Person(age = 33) // Person(name=null, age=33)

магии нет (просто прочтите документацию)


1
Хотя этот фрагмент кода может решить вопрос, включение объяснения действительно помогает улучшить качество вашего сообщения. Помните, что вы отвечаете на вопрос читателей в будущем, и эти люди могут не знать причины вашего предложения кода.
DimaSan

@DimaSan, вы правы, но в этой ветке уже есть объяснения в некоторых постах ...
Максим Костромин

Но ваш фрагмент отличается и, хотя может иметь другое описание, в любом случае теперь он стал намного понятнее.
DimaSan

4

Невозможно сохранить таким образом неизменность. Vals ДОЛЖЕН быть инициализирован при создании экземпляра.

Один из способов сделать это без неизменности:

class Entity() {
    public constructor(name: String, age: Int): this() {        
        this.name = name
        this.age = age
    }

    public var name: String by Delegates.notNull()

    public var age: Int by Delegates.notNull()
}

Значит, нет никакого способа указать Hibernate, чтобы он отображал столбцы в аргументы конструктора? Что ж, может быть, есть фреймворк / библиотека ORM, которая не требует конструктора без аргументов? :)
горячая клавиша

Не уверен в этом, давно не работал с Hibernate. Но это должно быть возможно как-то реализовать с именованными параметрами.
D3xter

Я думаю, что hibernate мог бы сделать это с небольшой (небольшой) работой. В java 8 вы можете иметь параметры, названные в конструкторе, и их можно было бы сопоставить так же, как сейчас с полями.
Christian Bongiorno

3

Я довольно давно работаю с Kotlin + JPA, и у меня возникла собственная идея, как писать классы Entity.

Я лишь немного расширил вашу первоначальную идею. Как вы сказали, мы можем создать частный конструктор без аргументов и предоставить значения по умолчанию для примитивов , но когда мы пытаемся использовать другие классы, это становится немного беспорядочным. Моя идея - создать статический объект STUB для класса сущности, который вы сейчас пишете, например:

@Entity
data class TestEntity(
    val name: String,
    @Id @GeneratedValue val id: Int? = null
) {
    private constructor() : this("")

    companion object {
        val STUB = TestEntity()
    }
}

и когда у меня есть класс сущности, связанный с TestEntity, я могу легко использовать только что созданную заглушку. Например:

@Entity
data class RelatedEntity(
        val testEntity: TestEntity,
        @Id @GeneratedValue val id: Long? = null
) {
    private constructor() : this(TestEntity.STUB)

    companion object {
        val STUB = RelatedEntity()
    }
}

Конечно, это решение не идеально. Вам все еще нужно создать шаблонный код, который не требуется. Также есть один случай, который не может быть решен с помощью заглушки - отношения родитель-потомок в одном классе сущностей - например:

@Entity
data class TestEntity(
        val testEntity: TestEntity,
        @Id @GeneratedValue val id: Long? = null
) {
    private constructor() : this(STUB)

    companion object {
        val STUB = TestEntity()
    }
}

Этот код сгенерирует исключение NullPointerException из-за проблемы с куриным яйцом - нам нужен STUB для создания STUB. К сожалению, нам нужно сделать это поле допускающим значение NULL (или какое-либо подобное решение), чтобы код работал.

Также, на мой взгляд, наличие Id в качестве последнего поля (и допускающего значение NULL) вполне оптимально. Мы не должны назначать его вручную и позволять базе данных делать это за нас.

Я не говорю, что это идеальное решение, но я думаю, что оно использует удобочитаемость кода объекта и функции Kotlin (например, нулевую безопасность). Я просто надеюсь, что будущие выпуски JPA и / или Kotlin сделают наш код еще проще и приятнее.


3

Как указано выше, вы должны использовать не no-argплагин, предоставляемый Jetbrains.

Если вы используете Eclispe, вам, возможно, придется отредактировать настройки компилятора Kotlin.

Окно> Настройки> Котлин> Компилятор

Активируйте no-argплагин в разделе Compiler Plugins.

См. Https://discuss.kotlinlang.org/t/kotlin-allopen-plugin-doesnt-work-with-sts/13277/10


2

Я сам болван, но, похоже, вам нужно явно указать инициализатор и вернуться к нулевому значению, как это

@Entity
class Person(val name: String? = null, val age: Int? = null)

1

Подобно @pawelbial, я использовал объект-компаньон для создания экземпляра по умолчанию, однако вместо определения вторичного конструктора просто используйте аргументы конструктора по умолчанию, такие как @iolo. Это избавляет вас от необходимости определять несколько конструкторов и упрощает код (хотя и разрешено, определение сопутствующих объектов "STUB" не совсем упрощает его)

@Entity
data class TestEntity(
    val name: String = "",
    @Id @GeneratedValue val id: Int? = null
) {

    companion object {
        val STUB = TestEntity()
    }
}

А затем для классов, относящихся к TestEntity

@Entity
data class RelatedEntity(
    val testEntity: TestEntity = TestEntity:STUB,
    @Id @GeneratedValue val id: Int? = null
)

Как упоминалось в @pawelbial, это не сработает, если TestEntityкласс «имеет» TestEntityкласс, поскольку STUB не будет инициализирован при запуске конструктора.


1

Эти строки сборки Gradle мне помогли:
https://plugins.gradle.org/plugin/org.jetbrains.kotlin.plugin.jpa/1.1.50 .
По крайней мере, он встроен в IntelliJ. На данный момент он не работает в командной строке.

И у меня есть

class LtreeType : UserType

и

    @Column(name = "path", nullable = false, columnDefinition = "ltree")
    @Type(type = "com.tgt.unitplanning.data.LtreeType")
    var path: String

var path: LtreeType не работает.


1

Если вы добавили плагин gradle https://plugins.gradle.org/plugin/org.jetbrains.kotlin.plugin.jpa, но не работал, скорее всего, версия устарела . Я был на 1.3.30, и у меня это не сработало. После того, как я обновился до 1.3.41 (последней на момент написания), все заработало.

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

buildscript {
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath "org.jetbrains.kotlin:kotlin-noarg:$kotlin_version"
    }
}

Я работаю с Micronaut, и он у меня работает с версией 1.3.41. Gradle говорит, что моя версия Kotlin - 1.3.21, и я не заметил никаких проблем, все остальные плагины ('kapt / jvm / allopen') находятся на 1.3.21. Также я использую формат DSL плагинов
Гэвин
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.