Kotlin com JPA: inferno do construtor padrão

132

Conforme a JPA exige, @Entity classes devem ter um construtor padrão (não arg) para instanciar os objetos ao recuperá-los do banco de dados.

No Kotlin, é muito conveniente declarar propriedades no construtor principal, como no exemplo a seguir:

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

Mas quando o construtor não-arg é declarado como secundário, ele exige que valores sejam passados ​​para o construtor primário, portanto, alguns valores válidos são necessários para eles, como aqui:

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

Caso as propriedades tenham um tipo mais complexo do que apenas Stringe Intnão sejam anuláveis, será totalmente ruim fornecer os valores para elas, especialmente quando houver muito código no construtor primário einit blocos principais e quando os parâmetros forem usados ​​ativamente - - quando eles serão reatribuídos por reflexão, a maior parte do código será executada novamente.

Além disso, val-properties não pode ser reatribuído após a execução do construtor, portanto a imutabilidade também é perdida.

Portanto, a pergunta é: como o código Kotlin pode ser adaptado para funcionar com o JPA sem duplicação de código, escolhendo valores iniciais "mágicos" e perda de imutabilidade?

PS É verdade que o Hibernate, além do JPA, pode construir objetos sem construtor padrão?

tecla de atalho
fonte
1
INFO -- org.hibernate.tuple.PojoInstantiator: HHH000182: No default (no-argument) constructor for class: Test (class must be instantiated by Interceptor)- então, sim, o Hibernate pode funcionar sem o construtor padrão.
Michael Piefel
A maneira como acontece é com os levantadores - também conhecidos como: Mutabilidade. Instancia o construtor padrão e, em seguida, procura por setters. Eu quero objetos imutáveis. A única maneira de fazer isso é se o hibernar começar a olhar para o construtor. Existe um ticket aberto neste site: hibernate.atlassian.net/browse/HHH-9440
Christian Bongiorno

Respostas:

145

A partir do Kotlin 1.0.6 , o kotlin-noargplug-in do compilador gera construtores sintéticos padrão para classes que foram anotadas com anotações selecionadas.

Se você usa gradle, aplicar o kotlin-jpaplug-in é suficiente para gerar construtores padrão para as classes anotadas com @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
fonte
4
Você poderia talvez expandir um pouco como isso seria usado no seu código kotlin, mesmo que seja um caso de "o seu data class foo(bar: String)não muda". Seria bom ver um exemplo mais completo de como isso se encaixa. Obrigado
thecoshman
5
Este é o post que introduziu kotlin-noarge kotlin-jpacom ligações detalhando seu propósito blog.jetbrains.com/kotlin/2016/12/kotlin-1-0-6-is-here
Dalibor Filus
1
E o que dizer de uma classe de chave primária como CustomerEntityPK, que não é uma entidade, mas precisa de um construtor padrão?
jannnik
3
Não está trabalhando para mim. Funciona apenas se eu tornar opcionais os campos do construtor. O que significa que o plug-in não está funcionando.
Ixx
3
@jannnik Você pode marcar a classe de chave primária com o @Embeddableatributo, mesmo que não seja necessário. Dessa forma, será captado por kotlin-jpa.
Sv16
33

Apenas forneça valores padrão para todos os argumentos, o Kotlin fará o construtor padrão para você.

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

Veja o NOTE caixa abaixo da seguinte seção:

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

iolo
fonte
18
você obviamente não leu a pergunta dele; caso contrário, teria visto a parte em que ele afirma que os argumentos padrão são ruins, especialmente para objetos mais complexos. Sem mencionar, adicionar valores padrão para algo oculta outros problemas.
snowe
1
Por que é uma má idéia fornecer os valores padrão? Mesmo ao usar o consturctor sem args do Java, os valores padrão são atribuídos aos campos (por exemplo, nulo aos tipos de referência).
Umesh Rajbhandari
1
Há momentos em que você não pode fornecer padrões razoáveis. Pegue o exemplo dado de uma pessoa, você realmente deve modelá-la com uma data de nascimento, pois isso não muda (é claro, as exceções se aplicam em algum lugar), mas não há um padrão sensato a ser dado a isso. Portanto, do ponto de vista do código puro, você deve passar um DoB para o construtor de pessoas, garantindo assim que você nunca poderá ter uma pessoa que não tenha uma idade válida. O problema é que, da maneira que o JPA gosta de trabalhar, ele gosta de criar um objeto com um construtor sem argumentos e definir tudo.
58617 thecoshman
1
Eu acho que essa é a maneira correta de fazer isso; essa resposta funciona em outros casos em que você não usa JPA ou hiberna também. também é a maneira sugerida de acordo com os documentos mencionados na resposta.
Mohammad Rafigh
1
Além disso, você não deve usar a classe de dados com o JPA: "não use classes de dados com propriedades val, pois o JPA não foi projetado para funcionar com classes imutáveis ​​ou com os métodos gerados automaticamente por classes de dados". spring.io/guides/tutorials/spring-boot-kotlin/…
Tafsen
11

O @ D3xter tem uma boa resposta para um modelo, o outro é um recurso mais recente do Kotlin chamado lateinit:

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

    lateinit var name: String
    lateinit var birthdate: Date
}

Você usaria isso quando tiver certeza de que algo preencherá os valores no momento da construção ou logo após (e antes do primeiro uso da instância).

Você notará que eu mudei agepara birthdateporque você não pode usar valores primitivos com lateinite eles também no momento devem servar (a restrição poderá ser liberada no futuro).

Portanto, não é uma resposta perfeita para a imutabilidade, o mesmo problema que a outra resposta a esse respeito. A solução é plugins para bibliotecas que podem lidar com a compreensão do construtor Kotlin e mapear propriedades para parâmetros do construtor, em vez de exigir um construtor padrão. O módulo Kotlin para Jackson faz isso, portanto é claramente possível.

Consulte também: https://stackoverflow.com/a/34624907/3679676 para explorar opções semelhantes.

Jayson Minard
fonte
Vale ressaltar que lateinit e Delegates.notNull () são os mesmos.
Fasth
4
semelhante, mas não o mesmo. Se Delegate for usado, ele altera o que é visto para serialização do campo real por Java (ele vê a classe delegate). Além disso, é melhor usar lateinitquando você tem um ciclo de vida bem definido, garantindo a inicialização logo após a construção, ele se destina a esses casos. Considerando que o delegado se destina mais a "algum tempo antes do primeiro uso". Embora tecnicamente tenham comportamento e proteção semelhantes, não são idênticos.
Jayson Minard
Se você precisar usar valores primitivos, a única coisa que eu poderia pensar seria usar "valores padrão" ao instanciar um objeto, e com isso quero dizer usar 0 e falseInts e Booleans, respectivamente. Não tenho certeza como isso afetaria código do framework embora
OzzyTheGiant
6
@Entity data class Person(/*@Id @GeneratedValue var id: Long? = null,*/
                          var name: String? = null,
                          var age: Int? = null)

Os valores iniciais são requeridos se você deseja reutilizar o construtor para campos diferentes, o kotlin não permite nulos. Portanto, sempre que você planeja omitir o campo, use este formulário no construtor:var field: Type? = defaultValue

O jpa não exigiu construtor de argumentos:

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

não há duplicação de código. Se você precisa construir uma entidade e configurar apenas a idade, use este formulário:

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

não há mágica (basta ler a documentação)

Maksim Kostromin
fonte
1
Embora esse trecho de código possa resolver a questão, incluir uma explicação realmente ajuda a melhorar a qualidade da sua postagem. Lembre-se de que você está respondendo à pergunta dos leitores no futuro e essas pessoas podem não saber os motivos da sua sugestão de código.
DimaSan
@DimaSan, você está certo, mas esse segmento já tem explicações em alguns lugares ...
Maksim Kostromin
Mas seu snippet é diferente e, embora possa ter uma descrição diferente, agora é muito mais claro.
DimaSan 17/04
4

Não há como manter a imutabilidade assim. Vals DEVE ser inicializado ao construir a instância.

Uma maneira de fazer isso sem imutabilidade é:

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
fonte
Portanto, não há como dizer ao Hibernate para mapear colunas para os argumentos do construtor? Bem, pode ser, existe uma estrutura / biblioteca ORM que não requer construtor não-arg? :)
hotkey
Não tenho certeza disso, não trabalha com o Hibernate há muito tempo. Mas deve ser possível de alguma forma implementar com parâmetros nomeados.
D3xter
Eu acho que o hibernate poderia fazer isso com um pouco (não muito) de trabalho. No java 8, você pode realmente ter seus parâmetros nomeados no construtor e esses podem ser mapeados exatamente como estão nos campos agora.
Christian Bongiorno
3

Trabalho com o Kotlin + JPA há um bom tempo e criei minha própria idéia de como escrever classes de entidade.

Eu apenas estendo um pouco sua ideia inicial. Como você disse, podemos criar um construtor privado sem argumentos e fornecer valores padrão para primitivas , mas quando tentamos usar outras classes, fica um pouco confuso. Minha ideia é criar um objeto STUB estático para a classe de entidade que você escreve atualmente, por exemplo:

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

    companion object {
        val STUB = TestEntity()
    }
}

e quando tenho uma classe de entidade relacionada ao TestEntity , posso usar facilmente o stub que acabei de criar. Por exemplo:

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

    companion object {
        val STUB = RelatedEntity()
    }
}

Claro que esta solução não é perfeita. Você ainda precisa criar um código padrão que não deve ser necessário. Além disso, há um caso que não pode ser resolvido bem com stubbing - relação pai-filho dentro de uma classe de entidade - assim:

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

    companion object {
        val STUB = TestEntity()
    }
}

Esse código produzirá NullPointerException devido a um problema de ovo de galinha - precisamos do STUB para criar o STUB. Infelizmente, precisamos tornar esse campo anulável (ou alguma solução semelhante) para que o código funcione.

Também na minha opinião, ter o ID como último campo (e anulável) é bastante ideal. Não devemos atribuí-lo manualmente e deixar o banco de dados fazer isso por nós.

Não estou dizendo que essa é uma solução perfeita, mas acho que ela aproveita a legibilidade do código da entidade e os recursos do Kotlin (por exemplo, segurança nula). Só espero que os lançamentos futuros do JPA e / ou Kotlin tornem nosso código ainda mais simples e agradável.

pawelbial
fonte
2

Eu sou um nub mim, mas parece que você precisa explicitar inicializador e fallback para um valor nulo como este

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

Semelhante a @pawelbial, usei o objeto complementar para criar uma instância padrão; no entanto, em vez de definir um construtor secundário, use apenas argumentos do construtor padrão como @iolo. Isso poupa a necessidade de definir vários construtores e mantém o código mais simples (embora concedido, a definição de objetos complementares "STUB" não é exatamente simples)

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

    companion object {
        val STUB = TestEntity()
    }
}

E então para as classes que se relacionam com TestEntity

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

Como o @pawelbial mencionou, isso não funcionará onde o TestEntity classe "possui uma", uma TestEntityvez que o STUB não terá sido inicializado quando o construtor for executado.

dan.jones
fonte
1

Essas linhas de construção da Gradle me ajudaram:
https://plugins.gradle.org/plugin/org.jetbrains.kotlin.plugin.jpa/1.1.50 .
Pelo menos, ele cria no IntelliJ. Está falhando na linha de comando no momento.

E eu tenho um

class LtreeType : UserType

e

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

caminho var: LtreeType não funcionou.

allenjom
fonte
1

Se você incluiu o plug-in gradle https://plugins.gradle.org/plugin/org.jetbrains.kotlin.plugin.jpa mas não funcionou, é provável que a versão esteja desatualizada. Eu estava em 1.3.30 e não funcionou para mim. Depois que eu atualizei para 1.3.41 (o mais recente no momento da redação), funcionou.

Nota: a versão do kotlin deve ser igual a este plugin, por exemplo: foi assim que adicionei os dois:

buildscript {
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath "org.jetbrains.kotlin:kotlin-noarg:$kotlin_version"
    }
}
Liang Zhou
fonte
Estou trabalhando com o Micronaut e trabalhei com a versão 1.3.41. Gradle diz minha versão Kotlin é 1.3.21 e eu não ver qualquer problema, todos os outros plugins ( 'Kapt / jvm / allopen') estão em 1.3.21 Também estou usando o formato plugins DSL
Gavin