Котлін з JPA: конструктор за пеклами


131

Як вимагає JPA, @Entityкласи повинні мати конструктор за замовчуванням (не-аргумент) для екземпляру об'єктів під час отримання їх з бази даних.

У Kotlin властивості дуже зручно декларувати у первинному конструкторі, як у наступному прикладі:

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

Але коли конструктор non-arg оголошується як вторинний, він вимагає передачі значень первинному конструктору, тому для них потрібні деякі дійсні значення, як тут:

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

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

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

Тож виникає питання: як код Котліна можна адаптувати для роботи з JPA без дублювання коду, вибираючи "магічні" початкові значення та втрату незмінності?

PS Чи правда, що в сплячку в сторону JPA можна створювати об'єкти без конструктора за замовчуванням?


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

Як це робиться з сеттерами - aka: Зміна. Він створює конструктор за замовчуванням, а потім шукає сеттер. Я хочу незмінні предмети. Єдиний спосіб зробити це - якщо сплячки почнуть дивитися на конструктор. На цей 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"

Для Мейвена:

<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
Чи можете ви трохи розширити, як це буде використано у вашому котлі коду, навіть якщо це випадок "ваш data class foo(bar: String)не змінюється". Просто було б добре побачити більш повний приклад того, як це вписується на місце. Спасибі
thecoshman

5
Це повідомлення в блозі , який ввів kotlin-noargі kotlin-jpaз посиланнями деталізують їх призначення blog.jetbrains.com/kotlin/2016/12/kotlin-1-0-6-is-here
Dalibor Filus

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

3
Не працює для мене. Він працює лише в тому випадку, якщо я роблю поля конструктора необов'язковими. Що означає, що плагін не працює.
Ixx,

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

33

просто надайте значення за замовчуванням для всіх аргументів, Kotlin зробить конструктор за замовчуванням для вас.

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

дивіться NOTEполе нижче наступного розділу:

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


18
ви, очевидно, не читали його питання, інакше ви побачили б ту частину, де він стверджує, що аргументи за замовчуванням погано виглядають, особливо для більш складних об'єктів. Не кажучи вже про те, що додавання значень за замовчуванням для чогось приховує інші проблеми.
сноу

1
Чому погано зазначати значення за замовчуванням? Навіть при використанні конструктора без аргументів Java, полям присвоюються значення за замовчуванням (наприклад, нульові для посилальних типів).
Умеш Раджбхандарі 21

1
Бувають випадки, коли ви не можете надати розумних значень за замовчуванням. Візьміть даний приклад людини, ви дійсно повинні моделювати його з датою народження, оскільки це не змінюється (звичайно, винятки десь застосовуються десь), але немає розумного дефолту для цього. Отже, формуючи чисту точку зору коду, ви повинні передати DoB в конструктор особи, таким чином, гарантуючи, що ви ніколи не можете мати людину, яка не має дійсного віку. Проблема полягає в тому, як JPA любить працювати, він любить робити об’єкт із конструктором без аргументів, а потім встановлювати все.
thecoshman

1
Я думаю, що це правильний спосіб зробити це, ця відповідь працює в інших випадках, якщо ви також не використовуєте JPA або не впадаєте у сплячку. також запропонований спосіб згідно з документами, зазначеними у відповіді.
Мохаммед Рафіг

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

11

@ D3xter має хорошу відповідь для однієї моделі, інша - новіша функція в Котліні під назвою 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(обмеження може бути випущено в майбутньому).

Тож не ідеальна відповідь на незмінність, та сама проблема, що й інша відповідь у цьому плані. Рішення для цього - плагіни до бібліотек, які вміють розуміти конструктор Котліна та відображати властивості параметрів конструктора, замість того, щоб вимагати конструктора за замовчуванням. Модуль Котліна для Джексона робить це, тому це однозначно можливо.

Дивіться також: https://stackoverflow.com/a/34624907/3679676 щодо вивчення подібних варіантів.


Варто зауважити, що lateinit і Delegates.notNull () однакові.
фаст

4
подібні, але не однакові. Якщо використовується Delegate, він змінює те, що бачиться для серіалізації фактичного поля Java (він бачить клас делегата). Крім того, краще використовувати, lateinitколи у вас чітко визначений життєвий цикл, який гарантує ініціалізацію незабаром після побудови, він призначений для цих випадків. В той час як делегат більше призначений для "десь до першого використання". Хоча технічно вони мають схожу поведінку та захист, вони не однакові.
Джейсон Мінард

Якщо вам потрібно використовувати примітивні значення, єдине, про що я міг би придумати, - це використовувати "значення за замовчуванням" при створенні об'єктів, і під цим я маю на увазі використання 0 та falseInts та 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()
}

Тож навіть немає способу сказати сплячому зіставити стовпці на аргументи конструктора? Ну, може, є рамка / бібліотека ORM, яка не потребує конструктора без аргументів? :)
гаряча клавіша

Не впевнений у цьому, давно не працював зі сплячою. Але це можливо якось реалізувати з названими параметрами.
D3xter

Я думаю, що сплячий міг би зробити це за допомогою трохи (не багато) роботи. У java 8 ви можете фактично мати параметри, названі в конструкторі, і ті можуть бути відображені так само, як зараз, у поля.
Крістіан Бонгіорно

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. На жаль, нам потрібно зробити це поле змінним (або якесь подібне рішення), щоб зробити код кодовим.

Крім того, на мою думку, наявність Id як останнього поля (і нульового) є цілком оптимальним. Ми не повинні призначати це вручну, а дозволити базі даних робити це за нас.

Я не кажу, що це ідеальне рішення, але я думаю, що він використовує читабельність коду сутності та функції Котліна (наприклад, нульова безпека). Я просто сподіваюся, що майбутні випуски JPA та / або Kotlin зроблять наш код ще простішим та приємнішим.


3

Як зазначено вище, ви повинні використовувати no-argплагін, який надає Jetbrains.

Якщо ви використовуєте Eclispe , можливо, вам доведеться редагувати налаштування компілятора Kotlin.

Вікно> Налаштування> Kotlin> Компілятор

Активуйте no-argплагін у розділі Плагіни компілятора.

Дивіться: 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 плагінів
Gavin
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.