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

132

Comme JPA l'exige, les @Entityclasses doivent avoir un constructeur par défaut (non arg) pour instancier les objets lors de leur récupération à partir de la base de données.

Dans Kotlin, les propriétés sont très pratiques à déclarer 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é en tant que constructeur secondaire, il nécessite la transmission de valeurs pour le constructeur principal, donc certaines valeurs valides sont nécessaires pour eux, 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 juste Stringet Intet qu'elles ne sont pas Nullable, il semble totalement mauvais de leur fournir les valeurs, surtout quand il y a beaucoup de code dans le constructeur principal et les initblocs et lorsque les paramètres sont activement utilisés - - quand ils doivent être réaffectés par réflexion, la plupart du code sera à nouveau exécuté.

De plus, val-properties ne peut pas être réaffecté après l'exécution du constructeur, donc l'immuabilité est également perdue.

La question est donc: comment adapter le code Kotlin pour fonctionner avec JPA sans duplication de code, en choisissant des valeurs initiales "magiques" et en perte d'immuabilité?

PS Est-il vrai que Hibernate en dehors de JPA peut construire des objets sans constructeur par défaut?

touche de raccourci
la source
1
INFO -- org.hibernate.tuple.PojoInstantiator: HHH000182: No default (no-argument) constructor for class: Test (class must be instantiated by Interceptor)- donc, oui, Hibernate peut fonctionner sans le constructeur par défaut.
Michael Piefel
La façon dont il le fait est avec les setters - aka: Mutabilité. Il instancie le constructeur par défaut et recherche ensuite les setters. Je veux des objets immuables. La seule façon de procéder est que la mise en veille prolongée commence à regarder le constructeur. Il y a un ticket ouvert sur ce hibernate.atlassian.net/browse/HHH-9440
Christian Bongiorno

Réponses:

145

Depuis Kotlin 1.0.6 , le kotlin-noargplugin du compilateur génère des constructeurs synthétiques par défaut pour les classes qui ont été annotées avec des annotations sélectionnées.

Si vous utilisez gradle, l'application du kotlin-jpaplugin 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>
Ingo Kegel
la source
4
Pourriez-vous peut-être développer un peu la façon dont cela serait utilisé dans votre code kotlin, même si c'est un cas de "votre data class foo(bar: String)ne change pas". Ce serait juste bien de voir un exemple plus complet de la façon dont cela se met en place. Merci
thecoshman
5
Ceci est le billet de blog qui a présenté kotlin-noarget kotlin-jpaavec des liens détaillant leur objectif blog.jetbrains.com/kotlin/2016/12/kotlin-1-0-6-is-here
Dalibor Filus
1
Et qu'en est-il d'une classe de clé primaire comme CustomerEntityPK, qui n'est pas une entité mais a besoin d'un constructeur par défaut?
jannnik
3
Ne travaille pas pour moi. Cela ne fonctionne que si je rend les champs du constructeur optionnels. Ce qui signifie que le plugin ne fonctionne pas.
Ixx
3
@jannnik Vous pouvez marquer la classe de clé primaire avec l' @Embeddableattribut même si vous n'en avez pas besoin autrement. De cette façon, il sera récupéré par kotlin-jpa.
svick
33

fournissez simplement des valeurs par défaut pour tous les arguments, Kotlin créera un constructeur par défaut pour vous.

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

voir l' NOTEencadré ci-dessous la section suivante:

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

iolo
la source
18
vous n'avez évidemment pas lu sa question, sinon vous auriez vu la partie où il déclare que les arguments par défaut sont mauvais, en particulier pour les objets plus complexes. Sans oublier, l'ajout de valeurs par défaut pour quelque chose masque d'autres problèmes.
snowe
1
Pourquoi est-ce une mauvaise idée de fournir les valeurs par défaut? Même lors de l'utilisation du consturcteur no args de Java, des valeurs par défaut sont attribuées aux champs (par exemple, null pour les types de référence).
Umesh Rajbhandari
1
Il y a des moments pour lesquels vous ne pouvez pas fournir de valeurs par défaut raisonnables. Prenons l'exemple donné d'une personne, vous devriez vraiment le modéliser avec une date de naissance car cela ne change pas (bien sûr, des exceptions s'appliquent quelque part) mais il n'y a pas de valeur par défaut raisonnable à donner à cela. Par conséquent, d'un point de vue purement code, vous devez passer un DoB dans le constructeur de personne, vous assurant ainsi de ne jamais avoir une personne qui n'a pas d'âge valide. Le problème est que, comme JPA aime travailler, il aime créer un objet avec un constructeur sans args, puis tout définir.
thecoshman
1
Je pense que c'est la bonne façon de faire cela, cette réponse fonctionne dans d'autres cas où vous n'utilisez pas JPA ou hibernation trop. c'est aussi la manière suggérée selon les documents mentionnés dans la réponse.
Mohammad Rafigh
1
De plus, vous ne devez pas utiliser de classe de données avec JPA: "n'utilisez pas de classes de données avec des propriétés val car JPA n'est pas conçu pour fonctionner avec des classes immuables ou des méthodes générées automatiquement par des classes de données." spring.io/guides/tutorials/spring-boot-kotlin/…
Tafsen
11

@ D3xter a une bonne réponse pour un modèle, l'autre est une fonctionnalité plus récente 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 utiliseriez ceci lorsque vous êtes sûr que quelque chose remplira 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 remarquerez que j'ai changé ageen birthdateparce que vous ne pouvez pas utiliser de valeurs primitives avec lateinitet elles doivent aussi pour le moment êtrevar (une restriction pourrait être libérée à l'avenir).

Donc pas une réponse parfaite pour l'immuabilité, même problème que l'autre réponse à cet égard. La solution pour cela est des plugins pour les bibliothèques qui peuvent gérer la compréhension du constructeur Kotlin et le mappage des propriétés aux 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 également: https://stackoverflow.com/a/34624907/3679676 pour l'exploration d'options similaires.

Jayson Minard
la source
Il convient de noter que lateinit et Delegates.notNull () sont identiques.
Fasth
4
similaire mais pas le même. Si Delegate est utilisé, il change ce qui est vu pour la sérialisation du champ réel par Java (il voit la classe de délégué). De plus, il est préférable de l'utiliser lateinitlorsque vous avez un cycle de vie bien défini garantissant l'initialisation peu de temps après la construction, il est destiné à ces cas. Alors que le délégué est plus destiné à «quelque temps avant la première utilisation». Bien que techniquement, ils aient un comportement et une protection similaires, ils ne sont pas identiques.
Jayson Minard le
Si vous avez besoin d'utiliser des valeurs primitives, la seule chose à laquelle je pourrais penser serait d'utiliser des "valeurs par défaut" lors de l'instanciation d'un objet, et par là je veux dire en utilisant 0 et falsepour Ints et Booleans respectivement. Je ne sais pas comment cela affecterait le code du framework
OzzyTheGiant
6
@Entity data class Person(/*@Id @GeneratedValue var id: Long? = null,*/
                          var name: String? = null,
                          var age: Int? = null)

Les valeurs initiales sont obligatoires si vous souhaitez réutiliser le constructeur pour différents champs, kotlin n'autorise pas les valeurs nulles. Ainsi, chaque fois que vous prévoyez d'omettre le champ, utilisez ce formulaire dans le constructeur:var field: Type? = defaultValue

jpa ne nécessite aucun 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 uniquement de l'âge de configuration, 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)

Maksim Kostromin
la source
1
Bien que cet extrait de code puisse résoudre la question, inclure une explication aide vraiment à améliorer la qualité de votre message. N'oubliez pas que vous répondez à la question aux lecteurs à l'avenir et que ces personnes pourraient ne pas connaître les raisons de votre suggestion de code.
DimaSan
@DimaSan, vous avez raison, mais ce fil a déjà des explications dans certains articles ...
Maksim Kostromin
Mais votre extrait de code est différent et peut avoir une description différente, de toute façon maintenant il est beaucoup plus clair.
DimaSan
4

Il n'y a aucun moyen de garder l'immuabilité comme ça. Les valeurs Vals DOIVENT être initialisées 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()
}
D3xter
la source
Il n'y a donc même aucun moyen de dire à Hibernate de mapper les colonnes aux arguments du constructeur? Eh bien, peut-être, il existe un framework / bibliothèque ORM qui ne nécessite pas de constructeur non-arg? :)
raccourci clavier
Je ne sais pas à ce sujet, je n'ai pas travaillé avec Hibernate depuis longtemps. Mais il devrait être possible d'implémenter d'une manière ou d'une autre avec des paramètres nommés.
D3xter
Je pense que hibernate pourrait le faire avec un peu (pas beaucoup) de travail. Dans java 8, vous pouvez en fait nommer vos paramètres dans le constructeur et ceux-ci pourraient être mappés comme ils le sont aux champs maintenant.
Christian Bongiorno
3

Je travaille avec Kotlin + JPA depuis un certain temps et j'ai créé ma propre idée sur la façon d'écrire des classes Entity.

J'étend juste 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 les primitives , mais lorsque nous essayons d'utiliser une autre classe, 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 une classe d'entité 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 un code standard qui ne devrait pas être requis. Il y a aussi un cas qui ne peut pas être résolu correctement avec le stubbing - relation parent-enfant dans 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 raison d'un problème d'oeuf de poule - 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.

De plus, à 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 c'est une solution parfaite, mais je pense qu'elle exploite la lisibilité du code d'entité et les fonctionnalités de Kotlin (par exemple, la sécurité nulle). J'espère juste que les futures versions de JPA et / ou Kotlin rendront notre code encore plus simple et plus agréable.

pawelbial
la source
2

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

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

Similaire à @pawelbial, j'ai utilisé un objet compagnon pour créer une instance par défaut, mais au lieu de définir un constructeur secondaire, utilisez simplement des arguments de constructeur par défaut comme @iolo. Cela vous évite d'avoir à définir plusieurs constructeurs et simplifie le code (bien que cela soit accordé, la définition des objets compagnons "STUB" n'est pas exactement la simplicité)

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

    companion object {
        val STUB = TestEntity()
    }
}

Et puis pour les cours qui concernent 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 là où la TestEntityclasse "a une" TestEntityclasse puisque STUB n'aura pas été initialisé lorsque le constructeur est exécuté.

dan.jones
la source
1

Ces lignes de construction Gradle m'ont aidé:
https://plugins.gradle.org/plugin/org.jetbrains.kotlin.plugin.jpa/1.1.50 .
Au moins, il 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 chemin: LtreeType n'a pas fonctionné.

allenjom
la source
1

Si vous avez ajouté le plugin gradle https://plugins.gradle.org/plugin/org.jetbrains.kotlin.plugin.jpa mais n'a pas fonctionné, il y a de fortes chances que la version soit obsolète . J'étais sur 1.3.30 et cela n'a pas fonctionné pour moi. Après la mise à niveau vers la version 1.3.41 (la dernière au moment de la rédaction), cela a fonctionné.

Remarque: la version de kotlin doit être la même que ce plugin, par exemple: c'est ainsi que j'ai ajouté les deux:

buildscript {
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath "org.jetbrains.kotlin:kotlin-noarg:$kotlin_version"
    }
}
Liang Zhou
la source
Je travaille avec Micronaut et je l'ai fait fonctionner avec la version 1.3.41. Gradle dit que ma version Kotlin est 1.3.21 et que je n'ai vu aucun problème, tous les autres plugins ('kapt / jvm / allopen') sont sur 1.3.21 J'utilise également le format DSL des plugins
Gavin