Kotlin กับ JPA: นรกตัวสร้างเริ่มต้น


132

ตามที่ JPA ต้องการ@Entityคลาสควรมีคอนสตรัคเตอร์เริ่มต้น (ไม่ใช่อาร์กิวเมนต์) เพื่อสร้างอินสแตนซ์อ็อบเจ็กต์เมื่อดึงข้อมูลจากฐานข้อมูล

ใน Kotlin คุณสมบัติสะดวกมากที่จะประกาศภายในตัวสร้างหลักดังตัวอย่างต่อไปนี้:

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

แต่เมื่อคอนสตรัคเตอร์ที่ไม่ใช่อาร์กิวเมนต์ถูกประกาศเป็นตัวสร้างรองจะต้องมีการส่งผ่านค่าสำหรับตัวสร้างหลักดังนั้นจึงจำเป็นต้องมีค่าที่ถูกต้องสำหรับพวกเขาเช่นที่นี่:

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

ในกรณีที่คุณสมบัติมีบางประเภทที่ซับซ้อนมากกว่า just StringและIntและไม่เป็นโมฆะมันดูไม่ดีเลยที่จะระบุค่าสำหรับพวกเขาโดยเฉพาะอย่างยิ่งเมื่อมีโค้ดจำนวนมากในตัวสร้างและinitบล็อกหลักและเมื่อมีการใช้พารามิเตอร์ - - เมื่อพวกเขาจะถูกกำหนดใหม่โดยการสะท้อนรหัสส่วนใหญ่จะถูกเรียกใช้งานอีกครั้ง

ยิ่งไปกว่านั้นval-properties ไม่สามารถกำหนดใหม่ได้หลังจากตัวสร้างดำเนินการดังนั้นความไม่เปลี่ยนรูปจึงหายไปด้วย

คำถามคือ: โค้ด Kotlin สามารถปรับให้เข้ากับ JPA ได้อย่างไรโดยไม่ต้องทำซ้ำรหัสโดยเลือกค่าเริ่มต้น "magic" และการสูญเสียความไม่เปลี่ยนรูป

ปล. เป็นความจริงหรือไม่ที่ Hibernate นอกเหนือจาก JPA สามารถสร้างวัตถุโดยไม่มีตัวสร้างเริ่มต้นได้?


1
INFO -- org.hibernate.tuple.PojoInstantiator: HHH000182: No default (no-argument) constructor for class: Test (class must be instantiated by Interceptor)- ใช่แล้ว Hibernate สามารถทำงานได้โดยไม่มีตัวสร้างเริ่มต้น
Michael Piefel

วิธีที่ใช้กับ setters - aka: Mutability มันสร้างอินสแตนซ์ตัวสร้างเริ่มต้นจากนั้นค้นหาตัวตั้งค่า ฉันต้องการวัตถุที่ไม่เปลี่ยนรูป วิธีเดียวที่สามารถทำได้คือถ้าจำศีลเริ่มมองไปที่ตัวสร้าง มีตั๋วเปิดอยู่บนhibernate.atlassian.net/browse/HHH-9440
Christian Bongiorno

คำตอบ:


145

ในฐานะของ Kotlin 1.0.6การkotlin-noargปลั๊กอินคอมไพเลอร์สร้าง construtors เริ่มต้นสังเคราะห์สำหรับการเรียนที่ได้รับการกำกับด้วยคำอธิบายประกอบที่เลือก

หากคุณใช้ gradle การใช้kotlin-jpaปลั๊กอินก็เพียงพอที่จะสร้างตัวสร้างเริ่มต้นสำหรับคลาสที่มีคำอธิบายประกอบ@Entity:

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

apply plugin: "kotlin-jpa"

สำหรับ 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>

4
คุณช่วยขยายความเล็กน้อยเกี่ยวกับวิธีใช้สิ่งนี้ภายในรหัสคอตลินของคุณแม้ว่าจะเป็นกรณีที่ "คุณdata class foo(bar: String)ไม่เปลี่ยนแปลง" ก็ตาม เป็นเรื่องดีที่จะได้เห็นตัวอย่างที่สมบูรณ์มากขึ้นว่าสิ่งนี้เข้ากันได้อย่างไร ขอบคุณ
thecoshman

5
นี่คือบล็อกโพสต์ที่แนะนำkotlin-noargและkotlin-jpaมีลิงค์ที่ระบุวัตถุประสงค์ของพวกเขาblog.jetbrains.com/kotlin/2016/12/kotlin-1-0-6-is-here
Dalibor Filus

1
แล้วคลาสคีย์หลักเช่น CustomerEntityPK ซึ่งไม่ใช่เอนทิตี แต่ต้องการตัวสร้างเริ่มต้นล่ะ
jannnik

3
ไม่ทำงานสำหรับฉัน จะใช้ได้เฉพาะเมื่อฉันสร้างตัวเลือกฟิลด์ตัวสร้าง ซึ่งหมายความว่าปลั๊กอินไม่ทำงาน
Ixx

3
@jannnik คุณสามารถทำเครื่องหมายคลาสคีย์หลักด้วย@Embeddableแอตทริบิวต์แม้ว่าคุณจะไม่ต้องการก็ตาม ทางนั้นจะมารับkotlin-jpaเอง
svick

33

เพียงแค่ระบุค่าเริ่มต้นสำหรับอาร์กิวเมนต์ทั้งหมด Kotlin จะสร้างตัวสร้างเริ่มต้นให้คุณ

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

ดูNOTEกล่องด้านล่างส่วนต่อไปนี้:

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


18
เห็นได้ชัดว่าคุณไม่ได้อ่านคำถามของเขามิฉะนั้นคุณจะได้เห็นส่วนที่เขาระบุว่าอาร์กิวเมนต์เริ่มต้นนั้นดูไม่ดีโดยเฉพาะอย่างยิ่งสำหรับวัตถุที่ซับซ้อนมากขึ้น ไม่ต้องพูดถึงการเพิ่มค่าเริ่มต้นสำหรับบางสิ่งจะซ่อนปัญหาอื่น ๆ
snowe

1
เหตุใดจึงเป็นความคิดที่ไม่ดีที่จะระบุค่าเริ่มต้น แม้ว่าจะใช้ตัวสร้างไม่มี args ของ Java ค่าดีฟอลต์จะถูกกำหนดให้กับฟิลด์ (เช่น null เป็นชนิดการอ้างอิง)
Umesh Rajbhandari

1
มีหลายครั้งที่คุณไม่สามารถระบุค่าเริ่มต้นที่เหมาะสมได้ ยกตัวอย่างบุคคลที่คุณควรสร้างแบบจำลองด้วยวันเดือนปีเกิดเนื่องจากไม่เปลี่ยนแปลง (แน่นอนว่ามีข้อยกเว้นอยู่ที่ใดที่หนึ่ง) แต่ไม่มีค่าเริ่มต้นที่สมเหตุสมผลที่จะให้สิ่งนั้น ดังนั้นในมุมมองของรหัสที่บริสุทธิ์คุณต้องส่ง DoB ไปยังตัวสร้างบุคคลดังนั้นจึงมั่นใจได้ว่าคุณจะไม่มีบุคคลที่ไม่มีอายุที่ถูกต้อง ปัญหาคือวิธีที่ JPA ชอบทำงานมันชอบสร้างอ็อบเจกต์ที่มีตัวสร้าง no-args จากนั้นตั้งค่าทุกอย่าง
thecoshman

1
ฉันคิดว่านี่เป็นวิธีที่ถูกต้องในการทำเช่นนั้นคำตอบนี้ใช้ได้ในกรณีอื่น ๆ ที่คุณไม่ได้ใช้ JPA หรือจำศีลด้วย นอกจากนี้ยังเป็นวิธีที่แนะนำตามเอกสารที่ระบุไว้ในคำตอบ
Mohammad Rafigh

1
นอกจากนี้คุณไม่ควรใช้คลาสข้อมูลกับ JPA: "อย่าใช้คลาสข้อมูลที่มีคุณสมบัติ val เนื่องจาก JPA ไม่ได้ออกแบบมาเพื่อทำงานกับคลาสที่ไม่เปลี่ยนรูปหรือวิธีการที่สร้างขึ้นโดยอัตโนมัติโดยคลาสข้อมูล" spring.io/guides/tutorials/spring-boot-kotlin/…
Tafsen

11

@ D3xter มีคำตอบที่ดีสำหรับรุ่นหนึ่งส่วนอีกรุ่นเป็นคุณสมบัติที่ใหม่กว่าใน Kotlin เรียกว่าlateinit:

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

    lateinit var name: String
    lateinit var birthdate: Date
}

คุณจะใช้สิ่งนี้เมื่อคุณแน่ใจว่าบางสิ่งจะเติมเต็มค่าในเวลาก่อสร้างหรือหลังจากนั้นไม่นาน (และก่อนที่จะใช้อินสแตนซ์ครั้งแรก)

คุณจะสังเกตว่าฉันเปลี่ยนageเป็นbirthdateเพราะคุณไม่สามารถใช้ค่าดั้งเดิมด้วยlateinitและในขณะนี้ต้องเป็นvar(ข้อ จำกัด อาจถูกปลดในอนาคต)

ดังนั้นจึงไม่ใช่คำตอบที่สมบูรณ์แบบสำหรับการไม่เปลี่ยนรูปเป็นปัญหาเดียวกับคำตอบอื่น ๆ ในเรื่องนั้น โซลูชันสำหรับสิ่งนั้นคือปลั๊กอินไปยังไลบรารีที่สามารถจัดการกับความเข้าใจคอนสตรัคเตอร์ Kotlin และคุณสมบัติการแม็พกับพารามิเตอร์คอนสตรัคเตอร์แทนที่จะต้องใช้คอนสตรัคเตอร์เริ่มต้น โมดูล Kotlin สำหรับแจ็คสันไม่นี้จึงเป็นไปได้อย่างชัดเจน

ดูเพิ่มเติมที่ https://stackoverflow.com/a/34624907/3679676สำหรับการสำรวจตัวเลือกที่คล้ายกัน


ควรทราบว่า lateinit และ Delegates.notNull () เหมือนกัน
fasth

4
คล้ายกัน แต่ไม่เหมือนกัน ถ้าใช้ Delegate จะเปลี่ยนสิ่งที่เห็นสำหรับการทำให้เป็นอนุกรมของฟิลด์จริงโดย Java (จะเห็นคลาส delegate) นอกจากนี้ควรใช้lateinitเมื่อคุณมีวงจรอายุการใช้งานที่กำหนดไว้อย่างดีซึ่งรับประกันการเริ่มต้นใช้งานไม่นานหลังจากการก่อสร้างซึ่งมีไว้สำหรับกรณีเหล่านั้น ในขณะที่ผู้รับมอบสิทธิ์มีไว้สำหรับ "บางครั้งก่อนใช้ครั้งแรก" แม้ว่าในทางเทคนิคแล้วพวกเขาจะมีพฤติกรรมและการป้องกันที่คล้ายคลึงกัน แต่ก็ไม่เหมือนกัน
Jayson Minard

หากคุณจำเป็นต้องใช้ค่าดั้งเดิมสิ่งเดียวที่ฉันคิดได้คือใช้ "ค่าเริ่มต้น" เมื่อสร้างอินสแตนซ์วัตถุและโดยที่ฉันหมายถึงการใช้ 0 และfalseสำหรับ Ints และ Booleans ตามลำดับ ไม่แน่ใจว่าจะส่งผลกระทบต่อรหัสกรอบงานอย่างไร
OzzyTheGiant

6
@Entity data class Person(/*@Id @GeneratedValue var id: Long? = null,*/
                          var name: String? = null,
                          var age: Int? = null)

ต้องใช้ค่าเริ่มต้นหากคุณต้องการใช้ตัวสร้างซ้ำสำหรับฟิลด์ที่แตกต่างกัน kotlin ไม่อนุญาตให้ใช้ null ดังนั้นเมื่อใดก็ตามที่คุณวางแผนละเว้นฟิลด์ให้ใช้แบบฟอร์มนี้ในตัวสร้าง:var field: Type? = defaultValue

jpa ไม่จำเป็นต้องมีตัวสร้างอาร์กิวเมนต์:

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

ไม่มีการทำซ้ำรหัส หากคุณต้องการสร้างเอนทิตีและอายุการตั้งค่าเท่านั้นให้ใช้แบบฟอร์มนี้:

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

ไม่มีเวทมนตร์ (เพียงอ่านเอกสารประกอบ)


1
แม้ว่าข้อมูลโค้ดนี้จะช่วยแก้ปัญหาได้ แต่คำอธิบายจะช่วยปรับปรุงคุณภาพของโพสต์ของคุณได้มาก โปรดจำไว้ว่าคุณกำลังตอบคำถามสำหรับผู้อ่านในอนาคตและบุคคลเหล่านั้นอาจไม่ทราบสาเหตุของการแนะนำโค้ดของคุณ
DimaSan

@DimaSan คุณพูดถูก แต่กระทู้นั้นมีคำอธิบายอยู่แล้วในบางกระทู้ ...
Maksim Kostromin

แต่ตัวอย่างข้อมูลของคุณแตกต่างออกไปและแม้ว่าอาจมีคำอธิบายที่แตกต่างกัน แต่ตอนนี้ก็ชัดเจนกว่ามากแล้ว
DimaSan

4

ไม่มีวิธีใดที่จะรักษาความไม่เปลี่ยนรูปเช่นนี้ ต้องเตรียม Vals เมื่อสร้างอินสแตนซ์

วิธีหนึ่งที่จะทำได้โดยไม่เปลี่ยนรูปคือ:

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()
}

ดังนั้นจึงไม่มีวิธีใดที่จะบอกให้ Hibernate แมปคอลัมน์ไปยัง args constructor? อาจเป็นได้ว่ามีกรอบ ORM / ไลบรารีที่ไม่ต้องการตัวสร้างที่ไม่ใช่อาร์กิวเมนต์? :)
ปุ่มลัด

ไม่แน่ใจเกี่ยวกับเรื่องนี้ไม่ได้ทำงานกับ Hibernate มานานแล้ว แต่ควรเป็นไปได้ที่จะใช้พารามิเตอร์ที่มีชื่อ
D3xter

ฉันคิดว่าการจำศีลสามารถทำได้ด้วยการทำงานเล็กน้อย (ไม่มาก) ใน java 8 คุณสามารถตั้งชื่อพารามิเตอร์ในตัวสร้างได้จริงและสามารถแมปได้เช่นเดียวกับฟิลด์ในตอนนี้
Christian Bongiorno

3

ฉันทำงานกับ Kotlin + JPA มาระยะหนึ่งแล้วและฉันได้สร้างแนวคิดของตัวเองว่าจะเขียนคลาสเอนทิตีอย่างไร

ฉันแค่ขยายความคิดเริ่มต้นของคุณเล็กน้อย อย่างที่คุณบอกว่าเราสามารถสร้างตัวสร้างแบบไม่มีอาร์กิวเมนต์ส่วนตัวและระบุค่าเริ่มต้นสำหรับไพรเมทีฟ แต่เมื่อเราพยายามที่จะต้องใช้คลาสอื่นมันจะยุ่งเล็กน้อย ความคิดของฉันคือการสร้างวัตถุ STUB แบบคงที่สำหรับคลาสเอนทิตีที่คุณเขียนในปัจจุบันเช่น:

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

    companion object {
        val STUB = TestEntity()
    }
}

และเมื่อฉันมีคลาสเอนทิตีที่เกี่ยวข้องกับTestEntityฉันสามารถใช้ต้นขั้วที่เพิ่งสร้างขึ้นได้อย่างง่ายดาย ตัวอย่างเช่น:

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

    companion object {
        val STUB = RelatedEntity()
    }
}

แน่นอนว่าโซลูชันนี้ไม่สมบูรณ์แบบ คุณยังต้องสร้างรหัสต้นแบบบางอย่างที่ไม่ควรต้องใช้ นอกจากนี้ยังมีกรณีหนึ่งที่ไม่สามารถแก้ไขได้อย่างสมบูรณ์ด้วยการทำให้เกิดความสัมพันธ์ระหว่างแม่ลูกภายในคลาสเอนทิตีเดียวเช่นนี้:

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

    companion object {
        val STUB = TestEntity()
    }
}

รหัสนี้จะสร้างNullPointerExceptionเนื่องจากปัญหาไก่ - ไข่ - เราต้องการ STUB เพื่อสร้าง STUB น่าเสียดายที่เราจำเป็นต้องทำให้ฟิลด์นี้เป็นโมฆะ (หรือวิธีแก้ปัญหาที่คล้ายกัน) เพื่อให้โค้ดทำงานได้

นอกจากนี้ในความคิดของฉันการมีIdเป็นฟิลด์สุดท้าย (และเป็นโมฆะ) นั้นค่อนข้างเหมาะสม เราไม่ควรกำหนดด้วยมือและปล่อยให้ฐานข้อมูลทำแทนเรา

ฉันไม่ได้บอกว่านี่เป็นโซลูชันที่สมบูรณ์แบบ แต่ฉันคิดว่ามันใช้ประโยชน์จากความสามารถในการอ่านรหัสเอนทิตีและคุณสมบัติของ Kotlin (เช่นความปลอดภัยที่เป็นโมฆะ) ฉันหวังว่าการเปิดตัว JPA และ / หรือ Kotlin ในอนาคตจะทำให้โค้ดของเราง่ายและดียิ่งขึ้น


3

ฉันเป็นคนเก็บเอง แต่ดูเหมือนว่าคุณต้องเริ่มต้นอย่างชัดเจนและทางเลือกกลับเป็นค่าว่างเช่นนี้

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

3

ตามที่ระบุไว้ข้างต้นคุณต้องใช้no-argปลั๊กอินที่ไม่มีให้โดย Jetbrains

หากคุณใช้ Eclispeคุณอาจต้องแก้ไขการตั้งค่าคอมไพเลอร์ Kotlin

หน้าต่าง> การตั้งค่า> Kotlin> คอมไพเลอร์

เปิดใช้งานno-argปลั๊กอินในส่วนปลั๊กอินคอมไพเลอร์

ดู: https://discuss.kotlinlang.org/t/kotlin-allopen-plugin-doesnt-work-with-sts/13277/10


1

คล้ายกับ @pawelbial ฉันเคยใช้อ็อบเจ็กต์ร่วมเพื่อสร้างอินสแตนซ์เริ่มต้นอย่างไรก็ตามแทนที่จะกำหนดคอนสตรัคเตอร์รองเพียงใช้อาร์กิวเมนต์ตัวสร้างเริ่มต้นเช่น @iolo สิ่งนี้ช่วยให้คุณไม่ต้องกำหนดตัวสร้างหลายตัวและทำให้โค้ดง่ายขึ้น (แม้ว่าจะได้รับการกำหนดอ็อบเจ็กต์ที่แสดงร่วม "STUB" ก็ไม่ได้ทำให้มันง่ายอย่างแน่นอน)

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

    companion object {
        val STUB = TestEntity()
    }
}

แล้วสำหรับคลาสที่เกี่ยวข้องกับ TestEntity

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

ตามที่ @pawelbial ได้กล่าวไว้สิ่งนี้จะใช้ไม่ได้กับTestEntityคลาส "มี" TestEntityเนื่องจาก STUB จะไม่ถูกเริ่มต้นเมื่อรันคอนสตรัคเตอร์


1

เหล่านี้ Gradle สร้างเส้นช่วยให้ฉัน: https://plugins.gradle.org/plugin/org.jetbrains.kotlin.plugin.jpa/1.1.50

อย่างน้อยก็สร้างใน IntelliJ มันล้มเหลวในบรรทัดคำสั่งในขณะนี้

และฉันมี

class LtreeType : UserType

และ

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

เส้นทาง var: LtreeType ไม่ทำงาน


1

หากคุณเพิ่มปลั๊กอิน gradle https://plugins.gradle.org/plugin/org.jetbrains.kotlin.plugin.jpaแต่ไม่ได้ผลมีโอกาสที่เวอร์ชันจะล้าสมัย ฉันอยู่ที่ 1.3.30 น. และมันไม่ได้ผลสำหรับฉัน หลังจากที่ฉันอัปเกรดเป็น 1.3.41 (ล่าสุด ณ เวลาที่เขียน) มันใช้งานได้

หมายเหตุ: เวอร์ชัน kotlin ควรเหมือนกับปลั๊กอินนี้เช่น: นี่คือวิธีที่ฉันเพิ่มทั้งสอง:

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

ฉันกำลังทำงานกับ Micronaut และฉันทำให้มันใช้งานได้กับเวอร์ชัน 1.3.41 Gradle บอกว่าเวอร์ชัน Kotlin ของฉันคือ 1.3.21 และฉันไม่เห็นปัญหาใด ๆ ปลั๊กอินอื่น ๆ ทั้งหมด ('kapt / jvm / allopen') อยู่บน 1.3.21 ฉันใช้รูปแบบ DSL ของปลั๊กอินด้วย
Gavin
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.