hibernate - jparepository - Kotlin con JPA: constructor predeterminado infierno
kotlin jparepository (10)
Como JPA requiere,
@Entity
clases
@Entity
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 el caso de que las propiedades tengan un tipo más complejo que solo
String
e
Int
y no sean anulables, parece totalmente malo proporcionar los valores para ellas, especialmente cuando hay mucho código en el constructor primario y los bloques de
init
y cuando los parámetros son utilizado activamente: cuando se reasignan mediante reflexión, la mayor parte del código se ejecutará nuevamente.
Además, las propiedades
val
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?
@ 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).
lateinit
que cambié la
age
a la
birthdate
de
birthdate
porque no puedes usar valores primitivos con
lateinit
y también por el momento deben ser
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 Kotlin y las propiedades de mapeo 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://.com/a/34624907/3679676 para explorar opciones similares.
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 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ó.
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.
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()
}
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 escribir), funcionó.
Nota: la versión de Kotlin debería 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"
}
}
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 RelatedEntity(
val testEntity: TestEntity = TestEntity:STUB,
@Id @GeneratedValue val id: Int? = null
)
Y luego para las clases que se relacionan con
TestEntity
@Entity
class Person(val name: String? = null, val age: Int? = null)
Como @pawelbial ha mencionado, esto no funcionará donde la clase
TestEntity
"tiene una" clase
TestEntity
ya que STUB no se habrá inicializado cuando se ejecuta el constructor.
Soy una protuberancia, pero parece que tienes que inicializar explícitamente y retroceder a un valor nulo como este
@Entity
data class TestEntity(
val name: String = "",
@Id @GeneratedValue val id: Int? = null
) {
companion object {
val STUB = TestEntity()
}
}
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)
vea el cuadro de
NOTE
debajo de la siguiente sección:
https://kotlinlang.org/docs/reference/classes.html#secondary-constructors
A partir de Kotlin 1.0.6
, el
kotlin-noarg
compilador
kotlin-noarg
genera
kotlin-noarg
sintéticos predeterminados para las clases que se han anotado con anotaciones seleccionadas.
Si usa gradle, aplicar el
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>
@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 campo, use este formulario en constructor:
var field: Type? = defaultValue
var field: Type? = defaultValue
jpa no requiere constructor de argumentos:
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)