Kotlin mit JPA: Standardkonstruktor Hölle

130

Gemäß JPA @Entitysollten Klassen über einen Standardkonstruktor (ohne Argumente) verfügen, um die Objekte beim Abrufen aus der Datenbank zu instanziieren.

In Kotlin lassen sich Eigenschaften sehr bequem im primären Konstruktor deklarieren, wie im folgenden Beispiel:

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

Wenn der Nicht-Arg-Konstruktor jedoch als sekundärer deklariert wird, müssen Werte für den primären Konstruktor übergeben werden, sodass einige gültige Werte für sie erforderlich sind, wie hier:

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

Für den Fall, dass die Eigenschaften einen komplexeren Typ als nur haben Stringund Intnicht nullwertfähig sind, sieht es völlig schlecht aus, die Werte für sie anzugeben, insbesondere wenn der primäre Konstruktor und die initBlöcke viel Code enthalten und die Parameter aktiv verwendet werden. - Wenn sie durch Reflexion neu zugewiesen werden sollen, wird der größte Teil des Codes erneut ausgeführt.

Darüber hinaus können valEigenschaften nach der Ausführung des Konstruktors nicht neu zugewiesen werden, sodass auch die Unveränderlichkeit verloren geht.

Die Frage ist also: Wie kann Kotlin-Code für die Arbeit mit JPA ohne Codeduplizierung angepasst werden, indem "magische" Anfangswerte ausgewählt werden und die Unveränderlichkeit verloren geht?

PS Stimmt es, dass der Ruhezustand neben JPA Objekte ohne Standardkonstruktor erstellen kann?

Hotkey
quelle
1
INFO -- org.hibernate.tuple.PojoInstantiator: HHH000182: No default (no-argument) constructor for class: Test (class must be instantiated by Interceptor)- Ja, der Ruhezustand kann ohne den Standardkonstruktor funktionieren.
Michael Piefel
So wie es ist, ist es mit Setzern - auch bekannt als: Mutability. Es instanziiert den Standardkonstruktor und sucht dann nach Setzern. Ich möchte unveränderliche Objekte. Der einzige Weg, der gemacht werden kann, ist, wenn der Ruhezustand beginnt, den Konstruktor zu betrachten. Es gibt ein offenes Ticket auf diesem hibernate.atlassian.net/browse/HHH-9440
Christian Bongiorno

Antworten:

145

Ab Kotlin 1.0.6kotlin-noarg generiert das Compiler-Plugin synthetische Standardkonstrutoren für Klassen, die mit ausgewählten Annotationen versehen wurden.

Wenn Sie gradle verwenden, reicht das Anwenden des kotlin-jpaPlugins aus, um Standardkonstruktoren für Klassen zu generieren, die mit @Entityfolgenden Anmerkungen versehen sind :

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

apply plugin: "kotlin-jpa"

Für 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>
Ingo Kegel
quelle
4
Könnten Sie vielleicht etwas näher darauf eingehen, wie dies in Ihrem Kotlin-Code verwendet wird, selbst wenn es sich um "Ihr data class foo(bar: String)ändert sich nicht" handelt. Es wäre einfach schön, ein vollständigeres Beispiel dafür zu sehen, wie dies zusammenpasst. Danke
thecoshman
5
Dies ist der Blog-Beitrag, der eingeführt wurde kotlin-noargund kotlin-jpamit Links, die ihren Zweck beschreiben. Blog.jetbrains.com/kotlin/2016/12/kotlin-1-0-6-is-here
Dalibor Filus
1
Und was ist mit einer Primärschlüsselklasse wie CustomerEntityPK, die keine Entität ist, aber einen Standardkonstruktor benötigt?
Jannnik
3
Ich arbeite nicht für mich. Es funktioniert nur, wenn ich die Konstruktorfelder optional mache. Was bedeutet, dass das Plugin nicht funktioniert.
Ixx
3
@jannnik Sie können die Primärschlüsselklasse mit dem @EmbeddableAttribut markieren , auch wenn Sie es sonst nicht benötigen. Auf diese Weise wird es von abgeholt kotlin-jpa.
Svick
33

Geben Sie einfach Standardwerte für alle Argumente an. Kotlin erstellt einen Standardkonstruktor für Sie.

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

Siehe das NOTEFeld unter dem folgenden Abschnitt:

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

iolo
quelle
18
Sie haben seine Frage offensichtlich nicht gelesen, sonst hätten Sie den Teil gesehen, in dem er angibt, dass Standardargumente schlecht aussehen, insbesondere für komplexere Objekte. Ganz zu schweigen davon, dass das Hinzufügen von Standardwerten für etwas andere Probleme verbirgt.
Schnee
1
Warum ist es eine schlechte Idee, die Standardwerte anzugeben? Selbst wenn Sie den Java-Konstruktor no args verwenden, werden den Feldern Standardwerte zugewiesen (z. B. null für Referenztypen).
Umesh Rajbhandari
1
Es gibt Zeiten, für die Sie keine vernünftigen Standardeinstellungen angeben können. Nehmen Sie das gegebene Beispiel einer Person, Sie sollten es wirklich mit einem Geburtsdatum modellieren, da sich dies nicht ändert (natürlich gelten Ausnahmen irgendwie irgendwo), aber es gibt keinen vernünftigen Standard, der dies angibt. Aus reiner Code-Sicht müssen Sie daher ein DoB an den Personenkonstruktor übergeben, um sicherzustellen, dass Sie niemals eine Person haben können, die kein gültiges Alter hat. Das Problem ist, wie JPA gerne arbeitet, ein Objekt mit einem Konstruktor ohne Argumente erstellt und dann alles festlegt.
Thecoshman
1
Ich denke, dies ist der richtige Weg, diese Antwort funktioniert in anderen Fällen, in denen Sie weder JPA verwenden noch den Ruhezustand verwenden. Es ist auch der vorgeschlagene Weg gemäß den in der Antwort genannten Dokumenten.
Mohammad Rafigh
1
Außerdem sollten Sie keine Datenklasse mit JPA verwenden: "Verwenden Sie keine Datenklassen mit val-Eigenschaften, da JPA nicht für die Arbeit mit unveränderlichen Klassen oder den von Datenklassen automatisch generierten Methoden ausgelegt ist." spring.io/guides/tutorials/spring-boot-kotlin/…
Tafsen
11

@ D3xter hat eine gute Antwort für ein Modell, das andere ist eine neuere Funktion in Kotlin mit dem Namen lateinit:

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

    lateinit var name: String
    lateinit var birthdate: Date
}

Sie würden dies verwenden, wenn Sie sicher sind, dass etwas die Werte zum Zeitpunkt der Erstellung oder sehr bald danach (und vor der ersten Verwendung der Instanz) ausfüllt.

Sie werden feststellen, dass ich zu geändert habe age, birthdateweil Sie keine primitiven Werte mit verwenden können lateinitund dies auch im Moment sein muss var(die Einschränkung wird möglicherweise in Zukunft aufgehoben).

Also keine perfekte Antwort für Unveränderlichkeit, das gleiche Problem wie die andere Antwort in dieser Hinsicht. Die Lösung hierfür sind Plugins für Bibliotheken, die das Verständnis des Kotlin-Konstruktors und die Zuordnung von Eigenschaften zu Konstruktorparametern übernehmen können, anstatt einen Standardkonstruktor zu benötigen. Das Kotlin-Modul für Jackson macht das, also ist es eindeutig möglich.

Weitere Informationen zu ähnlichen Optionen finden Sie unter: https://stackoverflow.com/a/34624907/3679676 .

Jayson Minard
quelle
Beachten Sie, dass lateinit und Delegates.notNull () identisch sind.
Fasth
4
ähnlich, aber nicht gleich. Wenn Delegate verwendet wird, ändert sich das, was für die Serialisierung des tatsächlichen Felds durch Java angezeigt wird (es sieht die Delegate-Klasse). Es ist auch besser zu verwenden, lateinitwenn Sie einen genau definierten Lebenszyklus haben, der die Initialisierung kurz nach der Erstellung garantiert. Dies ist für diese Fälle vorgesehen. Während Delegate eher für "irgendwann vor dem ersten Gebrauch" gedacht ist. Obwohl sie technisch gesehen ein ähnliches Verhalten und einen ähnlichen Schutz aufweisen, sind sie nicht identisch.
Jayson Minard
Wenn Sie primitive Werte verwenden müssen, könnte ich mir nur falsevorstellen, beim Instanziieren eines Objekts "Standardwerte" zu verwenden. Damit meine ich die Verwendung von 0 und für Ints bzw. Booleans. Ich
bin
6
@Entity data class Person(/*@Id @GeneratedValue var id: Long? = null,*/
                          var name: String? = null,
                          var age: Int? = null)

Anfangswerte sind erforderlich, wenn Sie den Konstruktor für verschiedene Felder wiederverwenden möchten. Kotlin erlaubt keine Nullen. Wenn Sie also ein Feld weglassen möchten, verwenden Sie dieses Formular im Konstruktor:var field: Type? = defaultValue

jpa benötigt keinen Argumentkonstruktor:

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

Es gibt keine Codeduplizierung. Wenn Sie eine Konstruktionsentität und nur ein Einrichtungsalter benötigen, verwenden Sie dieses Formular:

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

Es gibt keine Magie (lesen Sie einfach die Dokumentation)

Maksim Kostromin
quelle
1
Während dieses Code-Snippet die Frage lösen kann, hilft eine Erklärung wirklich, die Qualität Ihres Beitrags zu verbessern. Denken Sie daran, dass Sie die Frage für Leser in Zukunft beantworten und diese Personen möglicherweise die Gründe für Ihren Codevorschlag nicht kennen.
DimaSan
@ DimaSan, Sie haben Recht, aber dieser Thread hat bereits Erklärungen in einigen Beiträgen ...
Maksim Kostromin
Aber Ihr Snippet ist anders und obwohl es möglicherweise eine andere Beschreibung hat, ist es jetzt viel klarer.
DimaSan
4

Es gibt keine Möglichkeit, diese Unveränderlichkeit so zu halten. Vals MUSS beim Erstellen der Instanz initialisiert werden.

Ein Weg, dies ohne Unveränderlichkeit zu tun, ist:

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()
}
D3xter
quelle
Es gibt also überhaupt keine Möglichkeit, Hibernate anzuweisen, Spalten den Konstruktorargumenten zuzuordnen? Nun, vielleicht gibt es ein ORM-Framework / eine ORM-Bibliothek, für die kein Nicht-Arg-Konstruktor erforderlich ist? :)
Hotkey
Ich bin mir nicht sicher, habe lange nicht mehr mit Hibernate gearbeitet. Aber es sollte irgendwie möglich sein, mit benannten Parametern zu implementieren.
D3xter
Ich denke, der Winterschlaf könnte dies mit ein wenig (nicht viel) Arbeit tun. In Java 8 können Sie Ihre Parameter tatsächlich im Konstruktor benennen lassen, und diese können genauso wie jetzt Feldern zugeordnet werden.
Christian Bongiorno
3

Ich arbeite seit einiger Zeit mit Kotlin + JPA und habe meine eigene Idee entwickelt, wie man Entitätsklassen schreibt.

Ich möchte Ihre ursprüngliche Idee nur geringfügig erweitern. Wie Sie sagten, können wir einen privaten argumentlosen Konstruktor erstellen und Standardwerte für Grundelemente bereitstellen. Wenn wir jedoch versuchen, andere Klassen zu verwenden, wird dies etwas chaotisch. Meine Idee ist es, ein statisches STUB-Objekt für eine Entitätsklasse zu erstellen, die Sie derzeit schreiben, z.

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

    companion object {
        val STUB = TestEntity()
    }
}

und wenn ich eine Entitätsklasse habe, die mit TestEntity zusammenhängt, kann ich einfach den gerade erstellten Stub verwenden. Beispielsweise:

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

    companion object {
        val STUB = RelatedEntity()
    }
}

Natürlich ist diese Lösung nicht perfekt. Sie müssen noch einen Code für das Boilerplate erstellen, der nicht erforderlich sein sollte. Es gibt auch einen Fall, der mit Stubbing - Eltern-Kind-Beziehung innerhalb einer Entitätsklasse - nicht gut gelöst werden kann:

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

    companion object {
        val STUB = TestEntity()
    }
}

Dieser Code wird produzieren Nullpointer aufgrund Henne-Ei - Problem - wir STUB brauchen STUB zu erstellen. Leider müssen wir dieses Feld nullbar machen (oder eine ähnliche Lösung), damit Code funktioniert.

Auch meiner Meinung nach ist es ziemlich optimal , Id als letztes Feld (und nullbar) zu haben. Wir sollten es nicht von Hand zuweisen und die Datenbank es für uns tun lassen.

Ich sage nicht, dass dies eine perfekte Lösung ist, aber ich denke, dass sie die Lesbarkeit von Entitätscode und Kotlin-Funktionen (z. B. Null-Sicherheit) nutzt. Ich hoffe nur, dass zukünftige Versionen von JPA und / oder Kotlin unseren Code noch einfacher und schöner machen.

pawelbial
quelle
2

Ich bin selbst ein Nub, aber anscheinend müssen Sie den Initialisierer explizit initialisieren und auf einen solchen Nullwert zurückgreifen

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

Ähnlich wie bei @pawelbial habe ich ein Begleitobjekt verwendet, um eine Standardinstanz zu erstellen. Anstatt jedoch einen sekundären Konstruktor zu definieren, verwenden Sie einfach Standardkonstruktorargumente wie @iolo. Dies erspart Ihnen die Definition mehrerer Konstruktoren und vereinfacht den Code (obwohl dies selbstverständlich ist, ist die Definition von "STUB" -Begleitobjekten nicht gerade einfach).

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

    companion object {
        val STUB = TestEntity()
    }
}

Und dann für Klassen, die sich beziehen TestEntity

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

Wie @pawelbial erwähnt hat, funktioniert dies nicht, wenn die TestEntityKlasse eine Klasse "hat" TestEntity, da STUB beim Ausführen des Konstruktors nicht initialisiert wurde.

dan.jones
quelle
1

Diese Gradle-Build-Linien haben mir geholfen:
https://plugins.gradle.org/plugin/org.jetbrains.kotlin.plugin.jpa/1.1.50 .
Zumindest baut es in IntelliJ. Es schlägt momentan in der Kommandozeile fehl.

Und ich habe eine

class LtreeType : UserType

und

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

var path: LtreeType hat nicht funktioniert.

Allenjom
quelle
1

Wenn Sie das Gradle-Plugin https://plugins.gradle.org/plugin/org.jetbrains.kotlin.plugin.jpa hinzugefügt haben , aber nicht funktioniert haben, ist die Version wahrscheinlich veraltet. Ich war am 1.3.30 und es hat bei mir nicht funktioniert. Nachdem ich auf 1.3.41 (spätestens zum Zeitpunkt des Schreibens) aktualisiert hatte, funktionierte es.

Hinweis: Die Kotlin-Version sollte mit diesem Plugin identisch sein, z. B.: So habe ich beide hinzugefügt:

buildscript {
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath "org.jetbrains.kotlin:kotlin-noarg:$kotlin_version"
    }
}
Liang Zhou
quelle
Ich arbeite mit Micronaut und habe es mit Version 1.3.41 zum Laufen gebracht. Gradle sagt, meine Kotlin-Version ist 1.3.21 und ich habe keine Probleme gesehen, alle anderen Plugins ('kapt / jvm / allopen') sind auf 1.3.21. Außerdem verwende ich das DSL-Format der Plugins
Gavin