Kotlin con JPA: l'inferno del costruttore predefinito


132

Come richiesto da JPA, le @Entityclassi dovrebbero avere un costruttore predefinito (non arg) per creare un'istanza degli oggetti quando vengono recuperati dal database.

In Kotlin, le proprietà sono molto convenienti da dichiarare nel costruttore principale, come nell'esempio seguente:

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

Ma quando il costruttore non arg viene dichiarato come secondario, richiede che vengano passati i valori per il costruttore primario, quindi per loro sono necessari alcuni valori validi, come qui:

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

Nel caso in cui le proprietà abbiano un tipo più complesso di un semplice Stringe Intnon siano annullabili, sembra del tutto negativo fornire i valori per loro, specialmente quando c'è molto codice nel costruttore e nei initblocchi primari e quando i parametri vengono utilizzati attivamente - - quando devono essere riassegnati attraverso la riflessione, la maggior parte del codice verrà eseguita nuovamente.

Inoltre, le valproprietà non possono essere riassegnate dopo l'esecuzione del costruttore, quindi si perde anche l'immutabilità.

Quindi la domanda è: come si può adattare il codice Kotlin per lavorare con JPA senza duplicazione del codice, scegliendo valori "magici" iniziali e perdita di immutabilità?

PS È vero che l'ibernazione a parte JPA può costruire oggetti senza costruttore predefinito?


1
INFO -- org.hibernate.tuple.PojoInstantiator: HHH000182: No default (no-argument) constructor for class: Test (class must be instantiated by Interceptor)- quindi sì, Hibernate può funzionare senza il costruttore predefinito.
Michael Piefel,

Il modo in cui lo fa è con i setter, ovvero: mutabilità. Crea un'istanza del costruttore predefinito e quindi cerca i setter. Voglio oggetti immutabili. L'unico modo che può essere fatto è se l'ibernazione inizia a guardare il costruttore. C'è un biglietto aperto su questo hibernate.atlassian.net/browse/HHH-9440
Christian Bongiorno

Risposte:


145

A partire da Kotlin 1.0.6 , il kotlin-noargplug-in del compilatore genera costruttori predefiniti sintetici per le classi che sono state annotate con annotazioni selezionate.

Se usi gradle, l'applicazione del kotlin-jpaplugin è sufficiente per generare costruttori predefiniti per le classi annotate con @Entity:

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

apply plugin: "kotlin-jpa"

Per 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
Potresti forse ampliare un po 'il modo in cui questo verrebbe usato nel tuo codice kotlin, anche se si tratta di "il tuo data class foo(bar: String)non cambia". Sarebbe bello vedere un esempio più completo di come questo si adatta al suo posto. Grazie
thecoshman l'

5
Questo è il post sul blog che ha introdotto kotlin-noarge kotlin-jpacon collegamenti che ne descrivono lo scopo blog.jetbrains.com/kotlin/2016/12/kotlin-1-0-6-is-here
Dalibor Filus

1
E che dire di una classe di chiave primaria come CustomerEntityPK, che non è un'entità ma necessita di un costruttore predefinito?
jannnik,

3
Non funziona per me. Funziona solo se rendo opzionali i campi del costruttore. Ciò significa che il plug-in non funziona.
Ixx,

3
@jannnik È possibile contrassegnare la classe chiave primaria con l' @Embeddableattributo anche se altrimenti non è necessario. In questo modo, verrà raccolto da kotlin-jpa.
svick,

33

basta fornire i valori predefiniti per tutti gli argomenti, Kotlin diventerà il costruttore predefinito per te.

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

vedere la NOTEcasella sotto la seguente sezione:

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


18
ovviamente non hai letto la sua domanda, altrimenti avresti visto la parte in cui afferma che gli argomenti di default sono cattivi, specialmente per oggetti più complessi. Per non parlare, l'aggiunta di valori predefiniti per qualcosa nasconde altri problemi.
snowe,

1
Perché è una cattiva idea fornire i valori predefiniti? Anche quando si utilizza Java no args consturctor, i valori predefiniti vengono assegnati ai campi (ad esempio null ai tipi di riferimento).
Umesh Rajbhandari,

1
Ci sono momenti per i quali non è possibile fornire impostazioni predefinite ragionevoli. Prendi l'esempio dato di una persona, dovresti davvero modellarlo con una data di nascita in quanto ciò non cambia (ovviamente, le eccezioni si applicano da qualche parte in qualche modo), ma non c'è alcun default ragionevole da dare a quello. Quindi, dal punto di vista del codice puro, devi passare un DoB nel costruttore della persona, assicurandoti così di non poter mai avere una persona che non abbia un'età valida. Il problema è che nel modo in cui a JPA piace lavorare, gli piace creare un oggetto con un costruttore no-args, quindi impostare tutto.
thecoshman,

1
Penso che questo sia il modo giusto per farlo, questa risposta funziona anche in altri casi in cui non si utilizza JPA o ibernazione. inoltre è il modo suggerito in base ai documenti come indicato nella risposta.
Mohammad Rafigh,

1
Inoltre, non dovresti usare la classe di dati con JPA: "non usare classi di dati con proprietà val perché JPA non è progettato per funzionare con classi immutabili o con i metodi generati automaticamente dalle classi di dati". spring.io/guides/tutorials/spring-boot-kotlin/…
Tafsen

11

@ D3xter ha una buona risposta per un modello, l'altro è una nuova funzionalità di Kotlin chiamata lateinit:

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

    lateinit var name: String
    lateinit var birthdate: Date
}

Lo useresti quando sei sicuro che qualcosa riempirà i valori in fase di costruzione o molto presto (e prima del primo utilizzo dell'istanza).

Noterai che sono cambiato agein birthdateperché non puoi usare i valori primitivi con lateinite anche loro per il momento devono essere var(la restrizione potrebbe essere rilasciata in futuro).

Quindi non una risposta perfetta per l'immutabilità, lo stesso problema dell'altra risposta al riguardo. La soluzione per questo sono i plugin per le librerie che possono gestire la comprensione del costruttore di Kotlin e mappare le proprietà sui parametri del costruttore, invece di richiedere un costruttore predefinito. Il modulo Kotlin per Jackson fa questo, quindi è chiaramente possibile.

Vedi anche: https://stackoverflow.com/a/34624907/3679676 per l'esplorazione di opzioni simili.


Vale la pena notare che lateinit e Delegates.notNull () sono uguali.
Fasth

4
simile, ma non lo stesso. Se viene utilizzato il delegato, cambia ciò che viene visto per la serializzazione del campo effettivo da Java (vede la classe delegata). Inoltre, è meglio usare lateinitquando si ha un ciclo di vita ben definito che garantisce l'inizializzazione subito dopo la costruzione, è destinato a quei casi. Considerando che delegato è più destinato a "prima del primo utilizzo". Sebbene tecnicamente abbiano un comportamento e una protezione simili, non sono identici.
Jayson Minard,

Se hai bisogno di usare i valori primitivi, l'unica cosa a cui potrei pensare sarebbe usare i "valori predefiniti" quando si crea un'istanza di un oggetto, e con ciò intendo usare rispettivamente 0 e falseper Ints e Booleans. Non sono sicuro di come ciò influenzerebbe il codice del framework
OzzyTheGiant,

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

I valori iniziali sono richiesti se si desidera riutilizzare il costruttore per campi diversi, kotlin non consente valori nulli. Quindi ogni volta che pianifichi ometti il ​​campo, usa questo modulo nel costruttore:var field: Type? = defaultValue

jpa non ha richiesto alcun costruttore di argomenti:

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

non c'è duplicazione del codice. Se hai bisogno di costruire un'entità e impostare solo l'età, usa questo modulo:

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

non c'è magia (basta leggere la documentazione)


1
Mentre questo frammento di codice può risolvere la domanda, inclusa una spiegazione aiuta davvero a migliorare la qualità del tuo post. Ricorda che stai rispondendo alla domanda per i lettori in futuro e quelle persone potrebbero non conoscere i motivi del tuo suggerimento sul codice.
DimaSan,

@DimaSan, hai ragione, ma quella discussione ha già delle spiegazioni in alcuni post ...
Maksim Kostromin

Ma il tuo frammento è diverso e sebbene possa avere una descrizione diversa, comunque ora è molto più chiaro.
DimaSan,

4

Non c'è modo di mantenere l'immutabilità in questo modo. Le valvole DEVONO essere inizializzate durante la costruzione dell'istanza.

Un modo per farlo senza immutabilità è:

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

Quindi non c'è nemmeno modo di dire a Hibernate di mappare le colonne sugli argomenti del costruttore? Beh, potrebbe essere, c'è un framework / libreria ORM che non richiede un costruttore non arg? :)
tasto di scelta rapida

Non ne sono sicuro, non ho lavorato con Hibernate per molto tempo. Ma dovrebbe essere possibile in qualche modo implementare con parametri denominati.
D3xter,

Penso che l'ibernazione possa farlo con un po '(non molto) di lavoro. In java 8 puoi effettivamente avere i tuoi parametri nominati nel costruttore e quelli potrebbero essere mappati proprio come sono adesso nei campi.
Christian Bongiorno,

3

Ho lavorato con Kotlin + JPA per un po 'e ho creato la mia idea su come scrivere classi Entity.

Estendo leggermente la tua idea iniziale. Come hai detto, possiamo creare un costruttore privato senza argomenti e fornire valori predefiniti per le primitive , ma quando proviamo a usare altre classi diventa un po 'confuso. La mia idea è quella di creare un oggetto STUB statico per la classe di entità che si scrive attualmente, ad esempio:

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

    companion object {
        val STUB = TestEntity()
    }
}

e quando ho una classe di entità correlata a TestEntity posso facilmente usare lo stub che ho appena creato. Per esempio:

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

    companion object {
        val STUB = RelatedEntity()
    }
}

Naturalmente questa soluzione non è perfetta. È ancora necessario creare un codice di boilerplate che non dovrebbe essere richiesto. Inoltre c'è un caso che non può essere risolto bene con lo stub - relazione genitore-figlio all'interno di una classe di entità - in questo modo:

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

    companion object {
        val STUB = TestEntity()
    }
}

Questo codice produrrà NullPointerException a causa del problema dell'uovo di gallina - abbiamo bisogno di STUB per creare STUB. Sfortunatamente dobbiamo rendere questo campo nullable (o qualche soluzione simile) per far funzionare il codice.

Anche a mio avviso avere Id come ultimo campo (e nullable) è abbastanza ottimale. Non dovremmo assegnarlo a mano e lasciare che il database lo faccia per noi.

Non sto dicendo che questa sia la soluzione perfetta, ma penso che sfrutti la leggibilità del codice entità e le funzionalità di Kotlin (ad es. Sicurezza nulla). Spero solo che le versioni future di JPA e / o Kotlin rendano il nostro codice ancora più semplice e gradevole.



2

Sono un nub me stesso ma sembra che tu debba esplicitare l'inizializzatore e il fallback a un valore nullo come questo

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

1

Simile a @pawelbial, ho usato un oggetto compagno per creare un'istanza predefinita, tuttavia invece di definire un costruttore secondario, basta usare arg del costruttore predefiniti come @iolo. Ciò consente di evitare la necessità di definire più costruttori e di semplificare il codice (anche se concesso, la definizione di oggetti companion "STUB" non lo mantiene esattamente semplice)

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

    companion object {
        val STUB = TestEntity()
    }
}

E poi per le classi che si riferiscono a TestEntity

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

Come accennato da @pawelbial, questo non funzionerà laddove la TestEntityclasse "ha una" TestEntityclasse poiché STUB non sarà stato inizializzato quando viene eseguito il costruttore.


1

Queste linee di costruzione Gradle mi hanno aiutato:
https://plugins.gradle.org/plugin/org.jetbrains.kotlin.plugin.jpa/1.1.50 .
Almeno, si basa su IntelliJ. Al momento non funziona sulla riga di comando.

E ho un

class LtreeType : UserType

e

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

var var: LtreeType non ha funzionato.


1

Se hai aggiunto il plugin gradle https://plugins.gradle.org/plugin/org.jetbrains.kotlin.plugin.jpa ma non ha funzionato, è probabile che la versione non sia aggiornata. Ero alle 1.3.30 e non ha funzionato per me. Dopo l'aggiornamento alla versione 1.3.41 (più recente al momento della stesura), ha funzionato.

Nota: la versione di kotlin dovrebbe essere la stessa di questo plugin, ad esempio: ecco come ho aggiunto entrambi:

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

Sto lavorando con Micronaut e ho capito che funziona con la versione 1.3.41. Gradle dice che la mia versione di Kotlin è la 1.3.21 e non ho riscontrato alcun problema, tutti gli altri plugin ('kapt / jvm / allopen') sono la 1.3.21 Inoltre sto usando il formato DSL dei plugin
Gavin
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.