Kotlin với JPA: địa ngục xây dựng mặc định


131

Như JPA yêu cầu, @Entitycác lớp nên có một hàm tạo mặc định (không đối số) để khởi tạo các đối tượng khi lấy chúng từ cơ sở dữ liệu.

Trong Kotlin, các thuộc tính rất thuận tiện để khai báo bên trong hàm tạo chính, như trong ví dụ sau:

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

Nhưng khi hàm tạo không đối số được khai báo là hàm thứ cấp, nó yêu cầu các giá trị cho hàm tạo chính được truyền vào, vì vậy một số giá trị hợp lệ là cần thiết cho chúng, như ở đây:

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

Trong trường hợp khi các thuộc tính có một số loại phức tạp hơn chỉ StringIntchúng không thể rỗng, thì việc cung cấp các giá trị cho chúng là rất tệ, đặc biệt là khi có nhiều mã trong hàm tạo và initkhối chính và khi các tham số được sử dụng tích cực - - khi chúng được gán lại thông qua sự phản chiếu, hầu hết các mã sẽ được thực thi lại.

Hơn nữa, các sản valphẩm không thể được chỉ định lại sau khi hàm tạo thực thi, do đó tính bất biến cũng bị mất.

Vì vậy, câu hỏi là: làm thế nào mã Kotlin có thể được điều chỉnh để hoạt động với JPA mà không cần sao chép mã, chọn giá trị ban đầu "ma thuật" và mất tính bất biến?

PS Có đúng là Hibernate ngoài JPA có thể xây dựng các đối tượng không có hàm tạo mặc định không?


1
INFO -- org.hibernate.tuple.PojoInstantiator: HHH000182: No default (no-argument) constructor for class: Test (class must be instantiated by Interceptor)- vì vậy, vâng, Hibernate có thể hoạt động mà không cần hàm tạo mặc định.
Michael Piefel

Cách thức thực hiện với setters - aka: Mutility. Nó khởi tạo hàm tạo mặc định và sau đó tìm kiếm setters. Tôi muốn các đối tượng bất biến. Cách duy nhất có thể được thực hiện là nếu ngủ đông bắt đầu nhìn vào hàm tạo. Có một vé mở trên hibernate.atlassian.net/browse/HHH-9440
Christian Bongiorno

Câu trả lời:


145

Kể từ Kotlin 1.0.6 , kotlin-noargplugin trình biên dịch tạo ra các cấu hình mặc định tổng hợp cho các lớp đã được chú thích với các chú thích được chọn.

Nếu bạn sử dụng gradle, áp dụng kotlin-jpaplugin là đủ để tạo các hàm tạo mặc định cho các lớp được chú thích bằng @Entity:

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

apply plugin: "kotlin-jpa"

Đối với 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
Có lẽ bạn có thể mở rộng một chút về cách sử dụng mã này trong mã kotlin của mình, ngay cả khi đó là trường hợp "bạn data class foo(bar: String)không thay đổi". Thật tuyệt khi thấy một ví dụ đầy đủ hơn về cách điều này phù hợp với vị trí. Cảm ơn
thecoshman

5
Đây là bài đăng trên blog được giới thiệu kotlin-noargkotlin-jpavới các liên kết chi tiết về mục đích của họ blog.jetbrains.com/kotlin/2016/12/kotlin-1-0-6-is-here
Dalibor Filus

1
Và những gì về một lớp khóa chính như CustomerEntityPK, không phải là một thực thể nhưng cần một hàm tạo mặc định?
jannnik

3
Không làm việc cho tôi. Nó chỉ hoạt động nếu tôi thực hiện các tùy chọn trường xây dựng. Điều đó có nghĩa là plugin không hoạt động.
Ixx

3
@jannnik Bạn có thể đánh dấu lớp khóa chính bằng @Embeddablethuộc tính ngay cả khi bạn không cần nó. Bằng cách đó, nó sẽ được chọn bởi kotlin-jpa.
Svick

33

chỉ cần cung cấp các giá trị mặc định cho tất cả các đối số, Kotlin sẽ tạo hàm tạo mặc định cho bạn.

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

xem NOTEhộp bên dưới phần sau:

https://kotlinlang.org/docs/reference/groupes.html#secondary-constructor


18
bạn rõ ràng đã không đọc câu hỏi của anh ấy, nếu không bạn sẽ thấy phần mà anh ấy nói rằng các đối số mặc định là xấu, đặc biệt là đối với các đối tượng phức tạp hơn. Chưa kể, việc thêm các giá trị mặc định cho một cái gì đó che giấu các vấn đề khác.
tuyết rơi

1
Tại sao nó là ý tưởng tồi để cung cấp các giá trị mặc định? Ngay cả khi sử dụng Java consturctor, các giá trị mặc định được gán cho các trường (ví dụ null cho các kiểu tham chiếu) của Java.
Umesh Rajbhandari

1
Có những lúc bạn không thể cung cấp một mặc định hợp lý. Lấy ví dụ về một người, bạn thực sự nên mô hình hóa nó với ngày sinh vì điều đó không thay đổi (tất nhiên, ngoại lệ áp dụng ở đâu đó bằng cách nào đó) nhưng không có mặc định hợp lý để đưa ra điều đó. Do đó hình thành quan điểm mã thuần túy, bạn phải chuyển DoB vào hàm tạo cá nhân, do đó đảm bảo bạn không bao giờ có thể có một người không có tuổi hợp lệ. Vấn đề là, cách JPA thích làm việc, nó thích tạo một đối tượng với hàm tạo không có đối số, sau đó đặt mọi thứ.
thecoshman

1
Tôi nghĩ rằng đây là cách đúng đắn để làm điều đó, câu trả lời này hoạt động trong các trường hợp khác mà bạn không sử dụng JPA hoặc ngủ đông quá. đó cũng là cách được đề xuất theo các tài liệu như được đề cập trong câu trả lời.
Mohammad Rafigh

1
Ngoài ra, bạn không nên sử dụng lớp dữ liệu với JPA: "không sử dụng các lớp dữ liệu với các thuộc tính val vì JPA không được thiết kế để hoạt động với các lớp bất biến hoặc các phương thức được tạo bởi các lớp dữ liệu." spring.io/guides/tutorials/spring-boot-kotlin/,
Tafsen

11

@ D3xter có một câu trả lời hay cho một mô hình, mô hình kia là một tính năng mới hơn trong Kotlin có tên lateinit:

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

    lateinit var name: String
    lateinit var birthdate: Date
}

Bạn sẽ sử dụng điều này khi bạn chắc chắn một cái gì đó sẽ điền vào các giá trị tại thời điểm xây dựng hoặc rất nhanh sau đó (và trước khi sử dụng ví dụ đầu tiên).

Bạn sẽ lưu ý tôi đã thay đổi agethành birthdatevì bạn không thể sử dụng các giá trị nguyên thủy lateinitvà chúng cũng phảivar (hạn chế có thể được phát hành trong tương lai).

Vì vậy, không phải là một câu trả lời hoàn hảo cho sự bất biến, vấn đề tương tự như câu trả lời khác trong vấn đề đó. Giải pháp cho điều đó là các plugin cho các thư viện có thể xử lý việc hiểu các hàm tạo của Kotlin và các thuộc tính ánh xạ tới các tham số của hàm tạo, thay vì yêu cầu một hàm tạo mặc định. Các mô-đun Kotlin cho Jackson thực hiện điều này, vì vậy nó là rõ ràng càng tốt.

Xem thêm: https://stackoverflow.com/a/34624907/3679676 để khám phá các tùy chọn tương tự.


Đáng lưu ý rằng lateinit và Delegates.notNull () là như nhau.
fasth

4
Tương tự, nhưng không giống nhau. Nếu Delegate được sử dụng, nó sẽ thay đổi những gì được thấy để tuần tự hóa trường thực tế bằng Java (nó nhìn thấy lớp đại biểu). Ngoài ra, tốt hơn là sử dụng lateinitkhi bạn có một khởi tạo bảo đảm vòng đời được xác định rõ ngay sau khi xây dựng, nó được dành cho những trường hợp đó. Trong khi đó, đại biểu được dự định nhiều hơn cho "đôi khi trước khi sử dụng lần đầu". Mặc dù về mặt kỹ thuật, chúng có hành vi và bảo vệ tương tự, nhưng chúng không giống nhau.
Jayson Minard

Nếu bạn cần sử dụng các giá trị nguyên thủy, điều duy nhất tôi có thể nghĩ đến là sử dụng "giá trị mặc định" khi khởi tạo một đối tượng và ý tôi là sử dụng 0 và falsecho Ints và Booleans tương ứng. Không chắc chắn điều đó sẽ ảnh hưởng đến mã khung như thế nào
OzzyTheGiant

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

Các giá trị ban đầu được yêu cầu nếu bạn muốn sử dụng lại hàm tạo cho các trường khác nhau, kotlin không cho phép null. Vì vậy, bất cứ khi nào bạn lập kế hoạch bỏ qua trường, hãy sử dụng biểu mẫu này trong hàm tạo:var field: Type? = defaultValue

jpa không yêu cầu hàm tạo đối số:

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

không có sự trùng lặp mã. Nếu bạn cần xây dựng thực thể và chỉ thiết lập tuổi, hãy sử dụng mẫu này:

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

không có phép thuật (chỉ cần đọc tài liệu)


1
Mặc dù đoạn mã này có thể giải quyết câu hỏi, bao gồm một lời giải thích thực sự giúp cải thiện chất lượng bài đăng của bạn. Hãy nhớ rằng bạn đang trả lời câu hỏi cho độc giả trong tương lai và những người đó có thể không biết lý do cho đề xuất mã của bạn.
DimaSan

@DimaSan, bạn đã đúng, nhưng chủ đề đó đã có lời giải thích trong một số bài viết ...
Maksim Kostromin

Nhưng đoạn trích của bạn là khác nhau và mặc dù có thể có một mô tả khác, dù sao bây giờ nó đã rõ ràng hơn nhiều.
DimaSan

4

Không có cách nào để giữ bất biến như thế này. Vals PHẢI được khởi tạo khi xây dựng thể hiện.

Một cách để làm điều đó mà không bất biến là:

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

Vì vậy, thậm chí không có cách nào để nói với Hibernate ánh xạ các cột tới các hàm tạo? Chà, có thể, có một khung / thư viện ORM không yêu cầu hàm tạo không đối số? :)
hotkey

Không chắc chắn về điều đó, đã không làm việc với Hibernate trong một thời gian dài. Nhưng nó có thể bằng cách nào đó để thực hiện với các tham số được đặt tên.
D3xter 17/08/2015

Tôi nghĩ rằng ngủ đông có thể làm điều này với một chút (không nhiều) công việc. Trong java 8, bạn thực sự có thể có các tham số được đặt tên trong hàm tạo và chúng có thể được ánh xạ giống như chúng hiện tại với các trường.
Christian Bongiorno

3

Tôi đã làm việc với Kotlin + JPA khá lâu và tôi đã tạo ra ý tưởng của riêng mình về cách viết các lớp Thực thể.

Tôi chỉ hơi mở rộng ý tưởng ban đầu của bạn. Như bạn đã nói, chúng ta có thể tạo hàm tạo không đối số riêng và cung cấp các giá trị mặc định cho các kiểu nguyên thủy , nhưng khi chúng ta cố gắng sử dụng các lớp khác, nó sẽ hơi lộn xộn. Ý tưởng của tôi là tạo đối tượng STUB tĩnh cho lớp thực thể mà bạn hiện đang viết, ví dụ:

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

    companion object {
        val STUB = TestEntity()
    }
}

và khi tôi có lớp thực thể có liên quan đến TestEntity, tôi có thể dễ dàng sử dụng sơ khai mà tôi vừa tạo. Ví dụ:

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

    companion object {
        val STUB = RelatedEntity()
    }
}

Tất nhiên giải pháp này không hoàn hảo. Bạn vẫn cần tạo một số mã soạn sẵn không cần thiết. Ngoài ra, có một trường hợp không thể giải quyết độc đáo bằng cách khai thác - mối quan hệ cha-con trong một lớp thực thể - như thế này:

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

    companion object {
        val STUB = TestEntity()
    }
}

Mã này sẽ tạo ra NullPulumException do vấn đề trứng gà - chúng ta cần STUB để tạo STUB. Thật không may, chúng ta cần làm cho trường này là null (hoặc một số giải pháp tương tự) để làm cho mã hoạt động.

Ngoài ra theo tôi có Id là trường cuối cùng (và nullable) là khá tối ưu. Chúng ta không nên gán nó bằng tay và để cơ sở dữ liệu làm điều đó cho chúng ta.

Tôi không nói rằng đây là giải pháp hoàn hảo, nhưng tôi nghĩ rằng nó thúc đẩy khả năng đọc mã thực thể và các tính năng của Kotlin (ví dụ: an toàn null). Tôi chỉ hy vọng các bản phát hành trong tương lai của JPA và / hoặc Kotlin sẽ làm cho mã của chúng tôi đơn giản hơn và đẹp hơn.



2

Bản thân tôi là một người vô dụng nhưng dường như bạn phải rõ ràng trình khởi tạo và dự phòng giá trị null như thế này

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

1

Tương tự như @pawelbial Tôi đã sử dụng đối tượng đồng hành để tạo một cá thể mặc định, tuy nhiên thay vì xác định hàm tạo thứ cấp, chỉ cần sử dụng hàm tạo mặc định lập luận như @iolo. Điều này giúp bạn tiết kiệm phải xác định nhiều hàm tạo và giữ cho mã đơn giản hơn (mặc dù được cấp, việc xác định các đối tượng đồng hành "STUB" không chính xác giữ cho nó đơn giản)

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

    companion object {
        val STUB = TestEntity()
    }
}

Và sau đó cho các lớp liên quan đến TestEntity

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

Như @pawelbial đã đề cập, điều này sẽ không hoạt động khi TestEntitylớp "có một TestEntitylớp " vì STUB sẽ không được khởi chạy khi hàm tạo được chạy.


1

Các dòng xây dựng Gradle này đã giúp tôi:
https://plugins.gradle.org/plugin/org.jetbrains.kotlin.plugin.jpa/1.1.50 .
Ít nhất, nó được xây dựng trong IntelliJ. Nó đang thất bại trên dòng lệnh vào lúc này.

Và tôi có một

class LtreeType : UserType

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

đường dẫn var: LtreeType không hoạt động.


1

Nếu bạn đã thêm plugin gradle https://plugins.gradle.org/plugin/org.jetbrains.kotlin.plugin.jpa nhưng không hoạt động, rất có thể phiên bản đã hết hạn. Tôi đã ở trên 1.3.30 và nó không làm việc cho tôi. Sau khi tôi nâng cấp lên 1.3.41 (mới nhất tại thời điểm viết bài), nó đã hoạt động.

Lưu ý: phiên bản kotlin phải giống với plugin này, ví dụ: đây là cách tôi đã thêm cả hai:

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

Tôi đang làm việc với Micronaut và tôi đã làm cho nó hoạt động với phiên bản 1.3.41. Gradle nói rằng phiên bản Kotlin của tôi là 1.3,21 và tôi không thấy bất kỳ vấn đề nào, tất cả các plugin khác ('kapt / jvm / allopen') đều có trên 1.3.21 Ngoài ra tôi đang sử dụng định dạng DSL bổ sung
Gavin
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.