Kotlin dengan JPA: default constructor hell

132

Seperti yang dibutuhkan JPA, @Entitykelas harus memiliki konstruktor default (non-arg) untuk instantiate objek ketika mengambilnya dari database.

Di Kotlin, properti sangat mudah untuk menyatakan dalam konstruktor utama, seperti dalam contoh berikut:

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

Tetapi ketika konstruktor non-arg dideklarasikan sebagai konstruktor sekunder, ia membutuhkan nilai untuk konstruktor primer untuk dilewati, sehingga beberapa nilai valid diperlukan untuk mereka, seperti di sini:

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

Dalam kasus ketika properti memiliki beberapa tipe yang lebih kompleks daripada hanya Stringdan Intdan mereka tidak dapat dibatalkan, terlihat sangat buruk untuk memberikan nilai untuk mereka, terutama ketika ada banyak kode dalam konstruktor dan initblok primer dan ketika parameter digunakan secara aktif - - ketika mereka akan dipindahkan melalui refleksi sebagian besar kode akan dieksekusi lagi.

Selain itu, val-properti tidak dapat dipindahkan setelah konstruktor dijalankan, sehingga ketidakmampuan juga hilang.

Jadi pertanyaannya adalah: bagaimana kode Kotlin dapat diadaptasi untuk bekerja dengan JPA tanpa duplikasi kode, memilih nilai awal "ajaib" dan kehilangan keabadian?

PS Benarkah Hibernate selain dari JPA dapat membangun objek tanpa konstruktor default?

hotkey
sumber
1
INFO -- org.hibernate.tuple.PojoInstantiator: HHH000182: No default (no-argument) constructor for class: Test (class must be instantiated by Interceptor)- jadi, ya, Hibernate dapat berfungsi tanpa konstruktor default.
Michael Piefel
Cara melakukannya adalah dengan setter - alias: Mutability. Ini instantiates konstruktor default dan kemudian mencari setter. Saya ingin benda abadi. Satu-satunya cara yang bisa dilakukan adalah jika hibernate mulai melihat konstruktor. Ada tiket terbuka di hibernate.atlassian.net/browse/HHH-9440
Christian Bongiorno

Jawaban:

145

Pada Kotlin 1.0.6 , kotlin-noargplugin kompiler menghasilkan konstruktor default default untuk kelas yang telah dijelaskan dengan anotasi yang dipilih.

Jika Anda menggunakan gradle, menerapkan kotlin-jpaplugin sudah cukup untuk menghasilkan konstruktor default untuk kelas yang dijelaskan dengan @Entity:

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

apply plugin: "kotlin-jpa"

Untuk 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
sumber
4
Bisakah Anda mengembangkan sedikit tentang bagaimana ini akan digunakan dalam kode kotlin Anda, bahkan jika itu adalah kasus "Anda data class foo(bar: String)tidak berubah". Akan menyenangkan melihat contoh yang lebih lengkap tentang bagaimana ini cocok dengan tempatnya. Terima kasih
thecoshman
5
Ini adalah posting blog yang memperkenalkan kotlin-noargdan kotlin-jpadengan tautan yang merinci tujuan mereka blog.jetbrains.com/kotlin/2016/12/kotlin-1-0-6-is-here
Dalibor Filus
1
Dan bagaimana dengan kelas kunci primer seperti CustomerEntityPK, yang bukan entitas tetapi membutuhkan konstruktor default?
jannnik
3
Tidak bekerja untukku. Ini hanya berfungsi jika saya membuat bidang konstruktor opsional. Yang artinya plugin tidak berfungsi.
Ixx
3
@ jannnik Anda dapat menandai kelas kunci utama dengan @Embeddableatribut bahkan jika Anda tidak membutuhkannya. Dengan begitu, itu akan diambil oleh kotlin-jpa.
svick
33

cukup berikan nilai default untuk semua argumen, Kotlin akan membuat konstruktor default untuk Anda.

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

lihat NOTEkotak di bawah bagian berikut:

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

iolo
sumber
18
Anda jelas tidak membaca pertanyaannya, kalau tidak Anda akan melihat bagian di mana ia menyatakan bahwa argumen default jelek, terutama untuk objek yang lebih kompleks. Belum lagi, menambahkan nilai default untuk sesuatu menyembunyikan masalah lain.
bersalju
1
Mengapa ide buruk memberikan nilai default? Bahkan ketika menggunakan consturctor no args Java, nilai-nilai default ditugaskan ke bidang (misalnya nol untuk tipe referensi).
Umesh Rajbhandari
1
Ada saatnya Anda tidak dapat memberikan default yang masuk akal. Ambil contoh yang diberikan seseorang, Anda harus memodelkannya dengan tanggal lahir karena itu tidak berubah (tentu saja, pengecualian berlaku di suatu tempat entah bagaimana) tetapi tidak ada standar yang masuk akal untuk memberikannya. Oleh karena itu membentuk sudut pandang kode murni, Anda harus meneruskan DoB ke konstruktor orang, sehingga memastikan Anda tidak akan pernah memiliki orang yang tidak memiliki usia yang valid. Masalahnya adalah, cara JPA suka bekerja, suka membuat objek dengan konstruktor tanpa argumen, lalu mengatur semuanya.
thecoshman
1
Saya pikir ini adalah cara yang tepat untuk melakukan itu, jawaban ini berfungsi dalam kasus lain bahwa Anda tidak menggunakan JPA atau hibernasi juga. juga itu cara yang disarankan menurut dokumen sebagaimana disebutkan dalam jawaban.
Mohammad Rafigh
1
Juga, Anda tidak boleh menggunakan kelas data dengan JPA: "jangan gunakan kelas data dengan properti val karena JPA tidak dirancang untuk bekerja dengan kelas yang tidak dapat diubah atau metode yang dihasilkan secara otomatis oleh kelas data." spring.io/guides/tutorials/spring-boot-kotlin/…
Tafsen
11

@ D3xter memiliki jawaban yang bagus untuk satu model, yang lain adalah fitur yang lebih baru di Kotlin yang disebut lateinit:

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

    lateinit var name: String
    lateinit var birthdate: Date
}

Anda akan menggunakan ini ketika Anda yakin sesuatu akan mengisi nilai pada waktu konstruksi atau segera setelahnya (dan sebelum penggunaan pertama instance).

Anda akan catatan saya berubah ageke birthdatekarena Anda tidak dapat menggunakan nilai-nilai primitif dengan lateinitdan mereka juga untuk saat ini harus var(pembatasan mungkin akan dirilis di masa depan).

Jadi bukan jawaban sempurna untuk kekekalan, masalah yang sama dengan jawaban lain dalam hal itu. Solusi untuk itu adalah plugin ke perpustakaan yang dapat menangani pemahaman konstruktor Kotlin dan memetakan properti ke parameter konstruktor, alih-alih membutuhkan konstruktor default. The Kotlin modul untuk Jackson melakukan hal ini, sehingga sangat jelas mungkin.

Lihat juga: https://stackoverflow.com/a/34624907/3679676 untuk eksplorasi opsi serupa.

Jayson Minard
sumber
Patut dicatat bahwa lateinit dan Delegates.notNull () adalah sama.
fasth
4
mirip tapi tidak sama. Jika Delegate digunakan, ia mengubah apa yang dilihat untuk serialisasi bidang aktual oleh Java (ia melihat kelas delegasi). Juga, lebih baik digunakan lateinitketika Anda memiliki siklus hidup yang menjamin inisialisasi segera setelah konstruksi, ini dimaksudkan untuk kasus-kasus tersebut. Sedangkan delegasi lebih ditujukan untuk "kapan sebelum digunakan pertama kali". Meskipun secara teknis mereka memiliki perilaku dan perlindungan yang serupa, mereka tidak identik.
Jayson Minard
Jika Anda perlu menggunakan nilai-nilai primitif, satu-satunya hal yang dapat saya pikirkan adalah menggunakan "nilai default" ketika membuat instance objek, dan maksud saya menggunakan 0 dan falseuntuk Ints dan Booleans masing-masing. Tidak yakin bagaimana itu akan mempengaruhi kode kerangka kerja
OzzyTheGiant
6
@Entity data class Person(/*@Id @GeneratedValue var id: Long? = null,*/
                          var name: String? = null,
                          var age: Int? = null)

Nilai awal diperlukan jika Anda ingin menggunakan kembali konstruktor untuk bidang yang berbeda, kotlin tidak diizinkan nol. Jadi, setiap kali Anda berencana menghilangkan bidang, gunakan formulir ini di konstruktor:var field: Type? = defaultValue

jpa tidak memerlukan konstruktor argumen:

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

tidak ada duplikasi kode. Jika Anda memerlukan entitas konstruk dan hanya usia setup, gunakan formulir ini:

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

tidak ada keajaiban (baca dokumentasi saja)

Maksim Kostromin
sumber
1
Sementara potongan kode ini dapat menyelesaikan pertanyaan, termasuk penjelasan sangat membantu untuk meningkatkan kualitas posting Anda. Ingatlah bahwa Anda menjawab pertanyaan untuk pembaca di masa depan, dan orang-orang itu mungkin tidak tahu alasan untuk saran kode Anda.
DimaSan
@DimaSan, Anda benar, tetapi utas itu sudah memiliki penjelasan di beberapa pos ...
Maksim Kostromin
Tetapi cuplikan Anda berbeda dan meskipun mungkin memiliki uraian yang berbeda, toh sekarang lebih jelas.
DimaSan
4

Tidak ada cara untuk menjaga kekekalan seperti ini. Vals HARUS diinisialisasi ketika membangun instance.

Salah satu cara untuk melakukannya tanpa kekekalan adalah:

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
sumber
Jadi bahkan tidak ada cara untuk memberitahu Hibernate untuk memetakan kolom ke konstruktor args? Yah, mungkin, ada kerangka / perpustakaan ORM yang tidak memerlukan konstruktor non-arg? :)
hotkey
Tidak yakin tentang itu, sudah lama tidak bekerja dengan Hibernate. Tetapi harus bisa diterapkan dengan parameter bernama.
D3xter
Saya pikir hibernate bisa melakukan ini dengan sedikit (tidak banyak) pekerjaan. Dalam java 8 Anda sebenarnya dapat memiliki Anda parameter yang disebutkan dalam konstruktor dan mereka bisa dipetakan sama seperti mereka ke bidang sekarang.
Christian Bongiorno
3

Saya telah bekerja dengan Kotlin + JPA cukup lama dan saya telah membuat ide saya sendiri bagaimana menulis kelas Entity.

Saya hanya sedikit memperluas ide awal Anda. Seperti yang Anda katakan kami dapat membuat konstruktor tanpa argumen pribadi dan memberikan nilai default untuk primitif , tetapi ketika kami mencoba perlu menggunakan kelas lain itu menjadi sedikit berantakan. Ide saya adalah membuat objek STUB statis untuk kelas entitas yang saat ini Anda tulis misalnya:

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

    companion object {
        val STUB = TestEntity()
    }
}

dan ketika saya memiliki kelas entitas yang terkait dengan TestEntity saya dapat dengan mudah menggunakan rintisan yang baru saja saya buat. Sebagai contoh:

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

    companion object {
        val STUB = RelatedEntity()
    }
}

Tentu saja solusi ini tidak sempurna. Anda masih perlu membuat beberapa kode boilerplate yang seharusnya tidak diperlukan. Juga ada satu kasus yang tidak dapat diselesaikan dengan baik dengan stubbing - hubungan orangtua-anak dalam satu kelas entitas - seperti ini:

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

    companion object {
        val STUB = TestEntity()
    }
}

Kode ini akan menghasilkan NullPointerException karena masalah ayam-telur - kita perlu STUB untuk membuat STUB. Sayangnya kita perlu membuat bidang ini nullable (atau beberapa solusi serupa) untuk membuat kode berfungsi.

Juga menurut saya memiliki Id sebagai bidang terakhir (dan nullable) cukup optimal. Kita seharusnya tidak menetapkannya dengan tangan dan membiarkan database melakukannya untuk kita.

Saya tidak mengatakan bahwa ini adalah solusi yang sempurna, tapi saya pikir itu memanfaatkan pembacaan kode entitas dan fitur Kotlin (mis. Null safety). Saya hanya berharap rilis JPA dan / atau Kotlin di masa depan akan membuat kode kita lebih sederhana dan lebih baik.

pawelbial
sumber
2

Saya seorang nub sendiri tetapi tampaknya Anda harus menginisialisasi eksplisit dan mundur ke nilai nol seperti ini

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

Mirip dengan @pawelbial Saya telah menggunakan objek pengiring untuk membuat instance default, namun alih-alih mendefinisikan konstruktor sekunder, cukup gunakan arg konstruktor default seperti @iolo. Ini menghemat Anda harus mendefinisikan banyak konstruktor dan membuat kode lebih sederhana (meskipun diberikan, mendefinisikan objek pendamping "STUB" tidak persis membuatnya sederhana)

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

    companion object {
        val STUB = TestEntity()
    }
}

Dan kemudian untuk kelas yang berhubungan dengan TestEntity

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

Seperti @pawelbial telah menyebutkan, ini tidak akan bekerja di mana TestEntitykelas "memiliki" TestEntitykelas karena STUB tidak akan diinisialisasi ketika konstruktor dijalankan.

dan.jones
sumber
1

Baris build Gradle ini membantu saya:
https://plugins.gradle.org/plugin/org.jetbrains.kotlin.plugin.jpa/1.1.50 .
Setidaknya, itu dibangun di IntelliJ. Gagal pada baris perintah saat ini.

Dan saya punya

class LtreeType : UserType

dan

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

var path: LtreeType tidak berfungsi.

allenjom
sumber
1

Jika Anda menambahkan plugin gradle https://plugins.gradle.org/plugin/org.jetbrains.kotlin.plugin.jpa tetapi tidak berfungsi, kemungkinan versinya sudah keluar. Saya berada di 1.3.30 dan tidak berhasil untuk saya. Setelah saya memutakhirkan ke 1.3.41 (terbaru saat penulisan), itu berhasil.

Catatan: versi kotlin harus sama dengan plugin ini, mis: ini adalah cara saya menambahkan keduanya:

buildscript {
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath "org.jetbrains.kotlin:kotlin-noarg:$kotlin_version"
    }
}
Liang Zhou
sumber
Saya bekerja dengan Micronaut, dan saya membuatnya bekerja dengan versi 1.3.41. Gradle mengatakan versi Kotlin saya 1.3.21 dan saya tidak melihat masalah, semua plugin lainnya ('kapt / jvm / allopen') ada di 1.3.21. Saya juga menggunakan format plugins DSL
Gavin