Kotlin con JPA: constructor predeterminado infierno

132

Como JPA requiere, las @Entityclases deben tener un constructor predeterminado (no arg) para instanciar los objetos al recuperarlos de la base de datos.

En Kotlin, es muy conveniente declarar las propiedades 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 valores para que se pase 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 que las propiedades tengan un tipo más complejo que solo Stringy Intno sean anulables, se ve totalmente mal proporcionarles los valores, especialmente cuando hay mucho código en el constructor primario y los initbloques y cuando los parámetros se usan activamente: - cuando van a reasignarse mediante reflexión, la mayor parte del código se ejecutará nuevamente.

Además, las valpropiedades no se pueden reasignar después de que se ejecuta el constructor, por lo que también se pierde la inmutabilidad.

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

PD ¿Es cierto que Hibernate aparte de JPA puede construir objetos sin un constructor predeterminado?

tecla de acceso directo
fuente
1
INFO -- org.hibernate.tuple.PojoInstantiator: HHH000182: No default (no-argument) constructor for class: Test (class must be instantiated by Interceptor)- Entonces, sí, Hibernate puede funcionar sin el constructor predeterminado.
Michael Piefel
La forma en que lo hace es con setters, también conocido como: mutabilidad. Crea una instancia del constructor predeterminado y luego busca colocadores. Quiero objetos inmutables. La única forma de hacerlo es si hiberna comienza a mirar al constructor. Hay un boleto abierto en este hibernate.atlassian.net/browse/HHH-9440
Christian Bongiorno

Respuestas:

145

A partir de Kotlin 1.0.6 , el kotlin-noargcomplemento del compilador genera constructores sintéticos predeterminados para las clases que se han anotado con anotaciones seleccionadas.

Si usa gradle, aplicar el kotlin-jpacomplemento 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>
Ingo Kegel
fuente
44
¿Podría quizás ampliar un poco cómo se usaría esto dentro de su código kotlin, incluso si se trata de "su data class foo(bar: String)no cambia". Sería bueno ver un ejemplo más completo de cómo encaja esto en su lugar. Gracias
thecoshman
55
Esta es la publicación del blog que presentó kotlin-noargy kotlin-jpacon enlaces que detallan su propósito blog.jetbrains.com/kotlin/2016/12/kotlin-1-0-6-is-here
Dalibor Filus
1
¿Y qué pasa con una clase de clave primaria como CustomerEntityPK, que no es una entidad pero necesita un constructor predeterminado?
jannnik
3
No funciona para mi Solo funciona si hago que los campos del constructor sean opcionales. Lo que significa que el complemento no funciona.
Ixx
3
@jannnik Puede marcar la clase de clave primaria con el @Embeddableatributo, incluso si no lo necesita. De esa manera, será recogido por kotlin-jpa.
svick
33

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

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

ver el NOTEcuadro debajo de la siguiente sección:

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

iolo
fuente
18
obviamente no leyó su pregunta, de lo contrario, habría visto la parte en la que afirma que los argumentos predeterminados son malos, especialmente para objetos más complejos. Sin mencionar que agregar valores predeterminados para algo oculta otros problemas.
snowe
1
¿Por qué es mala idea proporcionar los valores predeterminados? Incluso cuando se utiliza el constructor sin args de Java, los valores predeterminados se asignan a los campos (por ejemplo, nulo a tipos de referencia).
Umesh Rajbhandari
1
Hay momentos en los que no puede proporcionar valores predeterminados razonables. Tome el ejemplo dado de una persona, realmente debería modelarlo con una fecha de nacimiento, ya que eso no cambia (por supuesto, las excepciones se aplican en algún lugar de alguna manera), pero no hay un valor predeterminado razonable para darle. Por lo tanto, para formar un punto de vista de código puro, debe pasar un DoB al constructor de personas, asegurando así que nunca pueda tener una persona que no tenga una edad válida. El problema es que, de la forma en que a JPA le gusta trabajar, le gusta hacer un objeto con un constructor sin argumentos y luego configurar todo.
thecoshman
1
Creo que esta es la forma correcta de hacerlo, esta respuesta funciona en otros casos en los que no usa JPA ni hiberna también. También es la forma sugerida según los documentos como se menciona en la respuesta.
Mohammad Rafigh
1
Además, no debe usar la clase de datos con JPA: "no use clases de datos con propiedades val porque JPA no está diseñado para trabajar con clases inmutables o los métodos generados automáticamente por las clases de datos". spring.io/guides/tutorials/spring-boot-kotlin/…
Tafsen
11

@ 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 momento de la construcción o muy poco después (y antes del primer uso de la instancia).

Notarás que cambié agea birthdateporque no puedes usar valores primitivos con lateinity también por el momento deben serlo var(la restricción podría liberarse en el futuro).

Entonces, no es una respuesta perfecta para la inmutabilidad, el mismo problema que la otra respuesta a ese respecto. La solución para eso son los complementos a las bibliotecas que pueden manejar la comprensión del constructor de Kotlin y las propiedades de asignación 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.

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

Jayson Minard
fuente
Vale la pena señalar que lateinit y Delegates.notNull () son lo mismo.
fasth
44
similar pero no igual. Si se utiliza Delegado, cambia lo que se ve para la serialización del campo real por parte de Java (ve la clase de delegado). Además, es mejor usarlo lateinitcuando tiene un ciclo de vida bien definido que garantiza la inicialización poco después de la construcción, está destinado para esos casos. Mientras que delegado está más destinado a "en algún momento antes del primer uso". Aunque técnicamente tienen un comportamiento y protección similares, no son idénticos.
Jayson Minard
Si necesita usar valores primitivos, lo único en lo que podría pensar sería en usar "valores predeterminados" al crear instancias de un objeto, y con eso me refiero a usar 0 y falsepara Ints y Booleans respectivamente. Sin embargo
OzzyTheGiant
6
@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 el constructor para diferentes campos, kotlin no permite valores nulos. Entonces, cuando planee omitir el campo, use este formulario en constructor:var field: Type? = defaultValue

jpa no requirió ningún argumento constructor:

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

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

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

no hay magia (solo lea la documentación)

Maksim Kostromin
fuente
1
Si bien este fragmento de código puede resolver la pregunta, incluir una explicación realmente ayuda a mejorar la calidad de su publicación. Recuerde que está respondiendo la pregunta para los lectores en el futuro, y que esas personas podrían no conocer los motivos de su sugerencia de código.
DimaSan
@DimaSan, tienes razón, pero ese hilo ya tiene explicaciones en algunas publicaciones ...
Maksim Kostromin
Pero su fragmento es diferente y, aunque puede tener una descripción diferente, de todos modos ahora es mucho más claro.
DimaSan
4

No hay forma 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()
}
D3xter
fuente
¿Entonces no hay forma de decirle a Hibernate que asigne columnas a los argumentos del constructor? Bueno, puede ser, ¿hay un marco / biblioteca ORM que no requiere un constructor no arg? :)
tecla
No estoy seguro de eso, no he trabajado con Hibernate durante mucho tiempo. Pero debería ser posible implementar de alguna manera con parámetros con nombre.
D3xter
Creo que hibernate podría hacer esto con un poco (no mucho) de trabajo. En java 8, en realidad puede tener sus parámetros nombrados en el constructor y estos podrían asignarse al igual que ahora a los campos.
Christian Bongiorno
3

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

Solo extiendo un poco tu idea inicial. Como dijiste, podemos crear un constructor privado sin argumentos y proporcionar valores predeterminados para las primitivas , pero cuando intentamos usar otras clases se vuelve un poco complicado. Mi idea es crear un objeto STUB estático para la clase de entidad que actualmente escribe, 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 una clase de entidad relacionada con TestEntity , puedo usar fácilmente el código auxiliar 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. Aún necesita crear un código repetitivo que no debería ser necesario. También hay un caso que no se puede resolver bien con el apisonamiento (relación padre-hijo dentro de una clase de entidad) como este:

@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 hacer que este campo sea anulable (o alguna solución similar) para que el código funcione.

También en mi opinión, tener Id como último campo (y anulable) es bastante óptimo. No debemos asignarlo a mano y dejar que la base de datos lo haga por nosotros.

No digo 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.

pawelbial
fuente
2

Soy una protuberancia, pero parece que tienes que inicializar explícitamente y retroceder a un valor nulo como este

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

Similar a @pawelbial, he usado un objeto complementario para crear una instancia predeterminada, sin embargo, en lugar de definir un constructor secundario, solo use argumentos de constructor predeterminados como @iolo. Esto le ahorra tener que definir múltiples constructores y hace que el código sea más simple (aunque está garantizado, definir objetos complementarios "STUB" no es exactamente 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 relacionan con TestEntity

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

Como @pawelbial ha mencionado, esto no funcionará donde la TestEntityclase "tiene una" TestEntityclase ya que STUB no se habrá inicializado cuando se ejecuta el constructor.

dan.jones
fuente
1

Estas líneas de compilació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 comando 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

ruta var: LtreeType no funcionó.

allenjom
fuente
1

Si agregó el complemento Gradle https://plugins.gradle.org/plugin/org.jetbrains.kotlin.plugin.jpa pero no funcionó, es probable que la versión esté desactualizada. Estaba en 1.3.30 y no funcionó para mí. Después de actualizar a 1.3.41 (la última en el momento de la escritura), funcionó.

Nota: la versión de kotlin debe ser la misma que este complemento, por ejemplo: así es como agregué ambos:

buildscript {
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath "org.jetbrains.kotlin:kotlin-noarg:$kotlin_version"
    }
}
Liang Zhou
fuente
Estoy trabajando con Micronaut, y lo hice funcionar con la versión 1.3.41. Gradle dice que mi versión de Kotlin es 1.3.21 y no vi ningún problema, todos los demás complementos ('kapt / jvm / allopen') están en 1.3.21 También estoy usando el formato DSL de complementos
Gavin