Kotlin con JPA: infierno constructor predeterminado


Como JPA requiere, @Entity las clases deben tener un constructor predeterminado (no arg) para instanciar los objetos cuando se recuperan de la base de datos.

En Kotlin, las propiedades son muy convenientes para declarar dentro del constructor primario, como en el siguiente ejemplo:

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

Pero cuando el constructor no-arg se declara como secundario, requiere que se pasen valores para el constructor primario, por lo que se necesitan algunos valores válidos para ellos, como aquí:

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

En caso de cuando las propiedades tienen algún tipo más complejo que solo String y Int y no son nullables, se ve totalmente mal proporcionar los valores para ellos, especialmente cuando hay mucho código en los bloques constructor primario y init y cuando los parámetros se utilizan activamente when cuando se van a reasignar a través de reflexión la mayor parte del código se va a ejecutar de nuevo.

Además, val-las propiedades no se pueden reasignar después de que el constructor se ejecute, por lo que la inmutabilidad también es perder.

Entonces, la pregunta es: ¿cómo se puede adaptar el código Kotlin para trabajar con JPA sin duplicación de código, eligiendo valores iniciales "mágicos" y pérdida de inmutabilidad?

P.d. ¿Es cierto que Hibernate aparte de JPA puede construir objetos sin constructor predeterminado?

Author: hotkey, 2015-08-16

9 answers

A partir de Kotlin 1.0.6 , el complemento de compilador kotlin-noarg genera construtores sintéticos predeterminados para las clases que han sido anotadas con anotaciones seleccionadas.

Si usas gradle, aplicar el complemento kotlin-jpa es suficiente para generar constructores predeterminados para las clases anotadas con @Entity:

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

apply plugin: "kotlin-jpa"

Para 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>
 89
Author: Ingo Kegel,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-12-28 15:48:19

Simplemente proporcione valores predeterminados para todos los argumentos, Kotlin hará constructor predeterminado para usted.

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

Véase el recuadro NOTE debajo de la siguiente sección:

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

 28
Author: iolo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-03-07 10:24:49

@D3xter tiene una buena respuesta para un modelo, el otro es una característica más nueva en Kotlin llamada lateinit:

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

    lateinit var name: String
    lateinit var birthdate: Date
}

Usaría esto cuando esté seguro de que algo completará los valores en el tiempo de construcción o muy poco después (y antes del primer uso de la instancia).

Notará que cambié age a birthdate porque no puede usar valores primitivos con lateinit y también por el momento deben ser var (la restricción podría liberarse en el futuro).

Así que no es una respuesta perfecta para la inmutabilidad, el mismo problema que la otra respuesta en ese sentido. La solución para ello son complementos para bibliotecas que pueden entender el constructor de Kotlin y asignar propiedades a los parámetros del constructor, en lugar de requerir un constructor predeterminado. El módulo Kotlin para Jackson hace esto, por lo que es claramente posible.

Véase también: https://stackoverflow.com/a/34624907/3679676 para explorar opciones similares.

 9
Author: Jayson Minard,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:34:41
@Entity data class Person(/*@Id @GeneratedValue var id: Long? = null,*/
                          var name: String? = null,
                          var age: Int? = null)

Los valores iniciales son obligatorios si desea reutilizar constructor para diferentes campos, kotlin no permite nulls. Así que cada vez que planee omitir el campo, use este formulario en constructor: var field: Type? = defaultValue

Jpa no requiere ningún argumento constructor:

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

No hay duplicación de código. Si necesita construir entidad y solo configurar edad, utilice este formulario:

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

No hay magia (solo lea la documentación)

 6
Author: Maksim Kostromin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-04-17 00:10:38

No hay manera de mantener la inmutabilidad como esta. Los Vals DEBEN inicializarse al construir la instancia.

Una forma de hacerlo sin inmutabilidad es:

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
Author: D3xter,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-08-16 19:18:35

He estado trabajando con Kotlin + JPA durante bastante tiempo y he creado mi propia idea de cómo escribir clases de Entidad.

Solo extiendo ligeramente tu idea inicial. Como has dicho, podemos crear un constructor privado sin argumentos y proporcionar valores predeterminados para primitivas , pero cuando intentamos usar otras clases se vuelve un poco desordenado. Mi idea es crear un objeto STUB estático para la clase de entidad que escribe actualmente, por ejemplo:

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

    companion object {
        val STUB = TestEntity()
    }
}

Y cuando tengo clase de entidad que es relacionado con TestEntity Puedo usar fácilmente el stub que acabo de crear. Por ejemplo:

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

    companion object {
        val STUB = RelatedEntity()
    }
}

Por supuesto, esta solución no es perfecta. Todavía necesita crear algún código repetitivo que no debería ser necesario. También hay un caso que no se puede resolver bien con la relación stubbing - parent-child dentro de una clase de entidad-como esto:

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

    companion object {
        val STUB = TestEntity()
    }
}

Este código producirá NullPointerException debido a un problema de huevo de gallina-necesitamos STUB para crear STUB. Desafortunadamente necesitamos para hacer este campo nullable (o alguna solución similar) para hacer que el código funcione.

También en mi opinión tener Id como último campo (y nullable) es bastante óptimo. No deberíamos asignarlo a mano y dejar que Database lo haga por nosotros.

No estoy diciendo que esta sea la solución perfecta, pero creo que aprovecha la legibilidad del código de entidad y las características de Kotlin (por ejemplo, seguridad nula). Solo espero que las futuras versiones de JPA y / o Kotlin hagan que nuestro código sea aún más simple y agradable.

 3
Author: pawelbial,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-05-03 20:14:58

Similar a @pawelbial, he usado el objeto compañero para crear una instancia predeterminada, sin embargo, en lugar de definir un constructor secundario, simplemente use args de constructores predeterminados como @iolo. Esto le ahorra tener que definir varios constructores y mantiene el código más simple (aunque concedido, definir objetos complementarios "STUB" no es exactamente mantenerlo simple)

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

    companion object {
        val STUB = TestEntity()
    }
}

Y luego para las clases que se refieren a TestEntity

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

Como @ pawelbial ha mencionado, esto no funcionará donde el TestEntity clase "tiene una" TestEntity clase desde el TALÓN no se han inicializado cuando se ejecuta el constructor.

 1
Author: dan.jones,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-06-06 20:46:41

Yo mismo soy un nudo, pero parece que tienes que inicializar explícitamente y recurrir a un valor nulo como este

@Entity
class Person(val name: String? = null, val age: Int? = null)
 1
Author: souleyHype,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-19 04:43:18

Estas líneas de construcción de Gradle me ayudaron:
https://plugins.gradle.org/plugin/org.jetbrains.kotlin.plugin.jpa/1.1.50 .
Al menos, se construye en IntelliJ. Está fallando en la línea de comandos en este momento.

Y tengo un

class LtreeType : UserType

Y

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

Var path: LtreeType no funcionó.

 1
Author: allenjom,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-09-29 19:16:15