web-dev-qa-db-fra.com

Kotlin avec JPA: l'enfer du constructeur par défaut

Comme le demande JPA, @Entity Les classes doivent avoir un constructeur par défaut (non-arg) pour instancier les objets lors de leur extraction à partir de la base de données.

Dans Kotlin, il est très pratique de déclarer des propriétés dans le constructeur principal, comme dans l'exemple suivant:

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

Mais lorsque le constructeur non-arg est déclaré secondaire, il a besoin que le constructeur primaire transmette des valeurs. Par conséquent, certaines valeurs valides sont nécessaires, comme ici:

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

Dans le cas où les propriétés ont un type plus complexe que String et Int et qu'elles ne sont pas nullables, il est totalement absurde de fournir les valeurs correspondantes, en particulier lorsqu'il y a beaucoup de code dans constructeur primaire et init blocs et lorsque les paramètres sont utilisés activement - lorsqu’ils doivent être réaffectés par réflexion, la plupart du code va être exécuté à nouveau.

De plus, les propriétés val- ne peuvent pas être réaffectées après l'exécution du constructeur, donc l'immutabilité est également perdue.

La question qui se pose est donc la suivante: comment adapter le code Kotlin pour qu’il fonctionne avec JPA sans duplication de code, en choisissant des valeurs initiales "magiques" et une perte d’immuabilité?

P.S. Est-il vrai qu'Hibernate de côté de JPA peut construire des objets sans constructeur par défaut?

109
hotkey

à partir de Kotlin 1.0.6, le kotlin-noarg Le plugin compilateur génère des constructeurs synthétiques par défaut pour les classes annotées avec les annotations sélectionnées.

Si vous utilisez gradle, appliquez le kotlin-jpa Le plugin suffit à générer des constructeurs par défaut pour les classes annotées avec @Entity:

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

apply plugin: "kotlin-jpa"

Pour 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>
115
Ingo Kegel

fournissez simplement les valeurs par défaut pour tous les arguments, Kotlin fera le constructeur par défaut pour vous.

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

voir la boîte NOTE sous la section suivante:

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

30
iolo

@ D3xter a une bonne réponse pour un modèle, l'autre est une nouvelle fonctionnalité de Kotlin appelée lateinit:

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

    lateinit var name: String
    lateinit var birthdate: Date
}

Vous utiliserez ceci quand vous êtes sûr que quelque chose va renseigner les valeurs au moment de la construction ou très peu de temps après (et avant la première utilisation de l'instance).

Vous noterez que j'ai changé age en birthdate parce que vous ne pouvez pas utiliser de valeurs primitives avec lateinit et qu'elles doivent également être pour le moment var (la restriction peut être annulée). A l'avenir).

Donc, pas une réponse parfaite pour l'immuabilité, même problème que l'autre réponse à cet égard. La solution à cela est de créer des plugins vers des bibliothèques capables de comprendre le constructeur Kotlin et de mapper les propriétés sur les paramètres du constructeur, au lieu de nécessiter un constructeur par défaut. Le module Kotlin pour Jackson fait cela, donc c'est clairement possible.

Voir aussi: https://stackoverflow.com/a/34624907/3679676 pour l'exploration d'options similaires.

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

Si vous souhaitez réutiliser le constructeur pour différents champs, Kotlin n'autorise pas les valeurs NULL. Ainsi, chaque fois que vous planifiez omettre un champ, utilisez ce formulaire dans le constructeur: var field: Type? = defaultValue

jpa n'a pas besoin de constructeur d'argument:

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

il n'y a pas de duplication de code. Si vous avez besoin d'une entité de construction et que vous avez seulement à installer, utilisez ce formulaire:

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

il n'y a pas de magie (il suffit de lire la documentation)

6
Maksim Kostromin

Il n'y a aucun moyen de garder l'immuabilité comme ça. Vals DOIT être initialisé lors de la construction de l'instance.

Une façon de le faire sans immuabilité est:

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()
}
4
D3xter

Je travaille avec Kotlin + JPA depuis un certain temps et j'ai créé ma propre idée de l'écriture de classes d'entité.

Je ne fais que prolonger légèrement votre idée initiale. Comme vous l'avez dit, nous pouvons créer un constructeur privé sans argument et fournir des valeurs par défaut pour primitives, mais lorsque nous essayons d'utiliser d'autres classes, cela devient un peu compliqué. Mon idée est de créer un objet STUB statique pour la classe d'entité que vous écrivez actuellement, par exemple:

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

    companion object {
        val STUB = TestEntity()
    }
}

et quand j'ai la classe d'entité qui est liée à TestEntity je peux facilement utiliser le stub que je viens de créer. Par exemple:

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

    companion object {
        val STUB = RelatedEntity()
    }
}

Bien sûr cette solution n'est pas parfaite. Vous devez toujours créer du code standard qui ne devrait pas être nécessaire. De plus, il existe un cas qui ne peut pas être résolu correctement avec stubbing - relation parent-enfant au sein d'une classe d'entité - comme ceci:

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

    companion object {
        val STUB = TestEntity()
    }
}

Ce code produira NullPointerException en raison d'un problème poulet-œuf - nous avons besoin de STUB pour créer STUB. Malheureusement, nous devons rendre ce champ nullable (ou une solution similaire) pour que le code fonctionne.

Aussi à mon avis, avoir Id comme dernier champ (et nullable) est tout à fait optimal. Nous ne devrions pas l'attribuer à la main et laisser la base de données le faire pour nous.

Je ne dis pas que cette solution est parfaite, mais je pense qu’elle tire parti de la lisibilité du code de l’entité et des fonctionnalités de Kotlin (par exemple, la sécurité nulle). J'espère juste que les futures versions de JPA et/ou de Kotlin rendront notre code encore plus simple et agréable.

3
pawelbial

Semblable à @pawelbial, j'ai utilisé un objet compagnon pour créer une instance par défaut. Cependant, au lieu de définir un constructeur secondaire, utilisez simplement les arguments de constructeur par défaut tels que @iolo. Cela vous évite de définir plusieurs constructeurs et simplifie le code (bien que ce soit accordé, définir des objets compagnons "STUB" n'est pas vraiment simple)

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

    companion object {
        val STUB = TestEntity()
    }
}

Et ensuite pour les classes qui se rapportent à TestEntity

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

Comme @pawelbial l'a mentionné, cela ne fonctionnera pas si la classe TestEntity a une classe "TestEntity car STUB n'aura pas été initialisé lors de l'exécution du constructeur.

1
dan.jones

Ces lignes de construction de Gradle m'ont aidé à:
https://plugins.gradle.org/plugin/org.jetbrains.kotlin.plugin.jpa/1.1.5 .
Au moins, cela se construit dans IntelliJ. Il échoue sur la ligne de commande pour le moment.

Et j'ai un

class LtreeType : UserType

et

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

var path: LtreeType n'a pas fonctionné.

1
allenjom

Si vous avez ajouté le plugin Gradle https://plugins.gradle.org/plugin/org.jetbrains.kotlin.plugin.jpa mais n'a pas fonctionné, il est probable que la version ne soit plus à jour. J'étais sur 1.3.30 et cela n'a pas fonctionné pour moi. Après avoir mis à niveau vers la version 1.3.41 (au plus tard au moment de la rédaction), cela a fonctionné.

Note: la version de kotlin devrait être identique à celle de ce plugin, par exemple: voici comment j'ai ajouté les deux:

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

Je suis un peu moi-même, mais il semble que vous deviez initialiser explicitement et revenir à une valeur nulle comme celle-ci.

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