Kotlin z JPA: domyślny konstruktor piekło

131

Zgodnie z wymaganiami JPA @Entityklasy powinny mieć domyślny konstruktor (bez argumentów) do tworzenia instancji obiektów podczas pobierania ich z bazy danych.

W Kotlinie właściwości są bardzo wygodne do zadeklarowania w głównym konstruktorze, jak w poniższym przykładzie:

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

Ale kiedy konstruktor nie-arg jest zadeklarowany jako pomocniczy, wymaga przekazania wartości dla konstruktora głównego, więc potrzebne są dla nich pewne prawidłowe wartości, jak tutaj:

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

W przypadku, gdy właściwości mają bardziej złożony typ niż tylko String i Intnie dopuszczają wartości null, podanie wartości dla nich wygląda zupełnie źle, zwłaszcza gdy jest dużo kodu w głównym konstruktorze i initblokach oraz gdy parametry są aktywnie używane - - kiedy mają zostać ponownie przypisane poprzez odbicie, większość kodu zostanie ponownie wykonana.

Ponadto, val -właściwości nie mogą być ponownie przypisane po wykonaniu konstruktora, więc niezmienność również zostaje utracona.

Pytanie brzmi więc: w jaki sposób można dostosować kod Kotlin do pracy z JPA bez duplikowania kodu, wybierając „magiczne” wartości początkowe i utratę niezmienności?

PS Czy to prawda, że ​​Hibernate poza JPA może konstruować obiekty bez domyślnego konstruktora?

Klawisz skrótu
źródło
1
INFO -- org.hibernate.tuple.PojoInstantiator: HHH000182: No default (no-argument) constructor for class: Test (class must be instantiated by Interceptor)- więc tak, Hibernate może działać bez domyślnego konstruktora.
Michael Piefel,
Sposób, w jaki to robi, jest z seterami - aka: Mutability. Tworzy wystąpienie domyślnego konstruktora, a następnie szuka metod ustawiających. Chcę niezmiennych obiektów. Jedynym sposobem, w jaki można to zrobić, jest hibernacja, która zaczyna patrzeć na konstruktora. Jest otwarty bilet na ten hibernate.atlassian.net/browse/HHH-9440
Christian Bongiorno,

Odpowiedzi:

145

Począwszy od Kotlin 1.0.6 , kotlin-noargwtyczka kompilatora generuje syntetyczne domyślne konstruktory dla klas, które zostały opatrzone adnotacjami wybranymi adnotacjami.

Jeśli używasz gradle, zastosowanie kotlin-jpawtyczki wystarczy do wygenerowania domyślnych konstruktorów dla klas z adnotacjami @Entity:

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

apply plugin: "kotlin-jpa"

Dla Mavena:

<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
źródło
4
Czy mógłbyś może trochę rozwinąć, w jaki sposób byłoby to używane w twoim kodzie kotlin, nawet jeśli jest to przypadek "twoje data class foo(bar: String)się nie zmienia". Byłoby miło zobaczyć pełniejszy przykład tego, jak to pasuje. Dzięki
thecoshman,
5
To jest post na blogu, który wprowadził kotlin-noargi kotlin-jpazawiera linki opisujące ich cel blog.jetbrains.com/kotlin/2016/12/kotlin-1-0-6-is-here
Dalibor Filus
1
A co z klasą klucza podstawowego, taką jak CustomerEntityPK, która nie jest jednostką, ale potrzebuje domyślnego konstruktora?
jannnik
3
Nie działa dla mnie. Działa tylko wtedy, gdy ustawię opcjonalne pola konstruktora. Co oznacza, że ​​wtyczka nie działa.
Ixx
3
@jannnik Możesz oznaczyć klasę klucza podstawowego @Embeddableatrybutem, nawet jeśli w innym przypadku nie jest to potrzebne. W ten sposób zostanie odebrany przez kotlin-jpa.
Svick
33

po prostu podaj domyślne wartości dla wszystkich argumentów, Kotlin zrobi dla ciebie domyślny konstruktor.

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

zobacz NOTEramkę pod następującą sekcją:

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

iolo
źródło
18
oczywiście nie przeczytałeś jego pytania, w przeciwnym razie widziałbyś część, w której stwierdza, że ​​domyślne argumenty wyglądają źle, szczególnie w przypadku bardziej złożonych obiektów. Nie wspominając o dodaniu domyślnych wartości dla czegoś, co ukrywa inne problemy.
śnieżny
1
Dlaczego podawanie wartości domyślnych jest złym pomysłem? Nawet w przypadku korzystania z konstruktora no args Javy, do pól przypisywane są wartości domyślne (np. Null do typów referencyjnych).
Umesh Rajbhandari
1
Są chwile, w których nie można podać rozsądnych wartości domyślnych. Weź podany przykład osoby, naprawdę powinieneś modelować ją z datą urodzenia, ponieważ to się nie zmienia (oczywiście, gdzieś istnieją wyjątki), ale nie ma sensownej wartości domyślnej. Stąd z czysto kodowego punktu widzenia, musisz przekazać DoB konstruktorowi osoby, dzięki czemu nigdy nie będziesz mieć osoby, która nie ma prawidłowego wieku. Problem w tym, że sposób, w jaki JPA lubi działać, lubi tworzyć obiekt z konstruktorem no-args, a potem wszystko ustawiać.
thecoshman
1
Myślę, że jest to właściwy sposób, aby to zrobić, ta odpowiedź działa w innych przypadkach, w których nie używasz JPA lub hibernacji. jest to również sugerowany sposób zgodnie z dokumentami wymienionymi w odpowiedzi.
Mohammad Rafigh
1
Nie należy również używać klas danych z JPA: „nie używaj klas danych z właściwościami val, ponieważ JPA nie jest zaprojektowany do pracy z niezmiennymi klasami lub metodami generowanymi automatycznie przez klasy danych”. spring.io/guides/tutorials/spring-boot-kotlin/…
Tafsen
11

@ D3xter ma dobrą odpowiedź dla jednego modelu, drugi to nowsza funkcja w Kotlinie o nazwie lateinit:

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

    lateinit var name: String
    lateinit var birthdate: Date
}

Użyjesz tego, gdy jesteś pewien, że coś wypełni wartości w czasie tworzenia lub wkrótce po (i przed pierwszym użyciem instancji).

Zauważysz, że zmieniłem się agena, birthdateponieważ nie możesz używać prymitywnych wartości z lateiniti na razie muszą być var(ograniczenie może zostać zwolnione w przyszłości).

Więc nie jest to doskonała odpowiedź na niezmienność, ten sam problem, co inna odpowiedź w tym względzie. Rozwiązaniem są wtyczki do bibliotek, które mogą obsługiwać rozumienie konstruktora Kotlin i mapowanie właściwości na parametry konstruktora, zamiast wymagać domyślnego konstruktora. Moduł Kotlin dla Jackson robi to, więc jest wyraźnie możliwe.

Zobacz też: https://stackoverflow.com/a/34624907/3679676, aby poznać podobne opcje.

Jayson Minard
źródło
Warto zauważyć, że lateinit i Delegates.notNull () są tym samym.
szybko
4
podobne, ale nie takie same. Jeśli jest używany Delegat, zmienia to, co jest widoczne dla serializacji rzeczywistego pola przez Javę (widzi klasę delegata). Ponadto lepiej jest używać go, lateinitgdy masz dobrze zdefiniowany cykl życia gwarantujący inicjalizację wkrótce po zbudowaniu, jest przeznaczony do takich przypadków. Natomiast delegat jest bardziej przeznaczony do „jakiegoś czasu przed pierwszym użyciem”. Chociaż technicznie mają podobne zachowanie i ochronę, nie są identyczne.
Jayson Minard
Jeśli potrzebujesz użyć wartości pierwotnych, jedyne, o czym mógłbym pomyśleć, to użycie „wartości domyślnych” podczas tworzenia instancji obiektu, a przez to mam na myśli użycie odpowiednio 0 i falsedla Ints i Booleans. Nie jestem pewien, jak to wpłynie na kod frameworka
OzzyTheGiant
6
@Entity data class Person(/*@Id @GeneratedValue var id: Long? = null,*/
                          var name: String? = null,
                          var age: Int? = null)

Wartości początkowe są wymagane, jeśli chcesz ponownie użyć konstruktora dla różnych pól, kotlin nie dopuszcza wartości null. Jeśli więc planujesz pominąć pole, użyj w konstruktorze tego formularza:var field: Type? = defaultValue

jpa nie wymagał konstruktora argumentów:

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

nie ma powielania kodu. Jeśli potrzebujesz encji konstrukcji i tylko ustawisz wiek, użyj tego formularza:

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

nie ma magii (wystarczy przeczytać dokumentację)

Maksim Kostromin
źródło
1
Chociaż ten fragment kodu może rozwiązać problem, dołączenie wyjaśnienia naprawdę pomaga poprawić jakość Twojego posta. Pamiętaj, że odpowiadasz na pytanie do czytelników w przyszłości, a osoby te mogą nie znać powodów, dla których zaproponowałeś kod.
DimaSan
@DimaSan, masz rację, ale ten wątek ma już wyjaśnienia w niektórych postach ...
Maksim Kostromin
Ale Twój fragment jest inny i chociaż może mieć inny opis, i tak jest teraz znacznie bardziej przejrzysty.
DimaSan
4

Nie ma sposobu, aby zachować taką niezmienność. Vals MUSI zostać zainicjowany podczas konstruowania instancji.

Jednym ze sposobów na zrobienie tego bez niezmienności jest:

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
źródło
Więc nie ma nawet sposobu, aby nakazać Hibernate mapowanie kolumn do argumentów konstruktora? Cóż, może istnieje struktura / biblioteka ORM, która nie wymaga konstruktora innego niż arg? :)
skrót klawiszowy
Nie jestem tego pewien, od dawna nie współpracowałem z Hibernate. Ale powinno być jakoś możliwe zaimplementowanie z nazwanymi parametrami.
D3xter
Myślę, że hibernacja mogłaby to zrobić przy niewielkiej (niewielkiej) pracy. W java 8 możesz tak naprawdę mieć parametry nazwane w konstruktorze, które można odwzorować tak samo, jak teraz na pola.
Christian Bongiorno
3

Z Kotlin + JPA pracuję już od dłuższego czasu i stworzyłem własny pomysł na pisanie klas Entity.

Tylko nieznacznie przedłużam twój początkowy pomysł. Jak powiedziałeś, możemy stworzyć prywatny konstruktor bez argumentów i podać domyślne wartości dla prymitywów , ale kiedy próbujemy użyć innych klas, robi się trochę bałagan. Moim pomysłem jest utworzenie statycznego obiektu STUB dla klasy encji, którą obecnie zapisujesz, np .:

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

    companion object {
        val STUB = TestEntity()
    }
}

a kiedy mam klasę encji, która jest powiązana z TestEntity , mogę z łatwością użyć kodu źródłowego , który właśnie utworzyłem. Na przykład:

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

    companion object {
        val STUB = RelatedEntity()
    }
}

Oczywiście to rozwiązanie nie jest idealne. Nadal musisz utworzyć standardowy kod, który nie powinien być wymagany. Jest też jeden przypadek, którego nie można ładnie rozwiązać za pomocą stubbing - relacji rodzic-dziecko w ramach jednej klasy encji - na przykład:

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

    companion object {
        val STUB = TestEntity()
    }
}

Ten kod wygeneruje wyjątek NullPointerException z powodu problemu z jajkiem kurzego - do utworzenia STUB potrzebujemy STUB. Niestety, aby kod działał, musimy uczynić to pole dopuszczającym wartość null (lub podobnym rozwiązaniem).

Moim zdaniem również posiadanie Id jako ostatniego pola (i wartości null) jest całkiem optymalne. Nie powinniśmy przypisywać tego ręcznie i pozwolić, aby zrobiła to za nas baza danych.

Nie mówię, że jest to idealne rozwiązanie, ale myślę, że wykorzystuje czytelność kodu encji i funkcje Kotlina (np. Zerowe bezpieczeństwo). Mam tylko nadzieję, że przyszłe wydania JPA i / lub Kotlin uczynią nasz kod jeszcze prostszym i ładniejszym.

pawelbial
źródło
2

Sam jestem nubem, ale wydaje się, że musisz jawnie inicjalizować i powrócić do wartości null w ten sposób

@Entity
class Person(val name: String? = null, val age: Int? = null)
souleyHype
źródło
1

Podobnie jak w przypadku @pawelbial, użyłem obiektu towarzyszącego, aby utworzyć domyślną instancję, jednak zamiast definiować konstruktor pomocniczy, wystarczy użyć domyślnych argumentów konstruktora, takich jak @iolo. Dzięki temu nie musisz definiować wielu konstruktorów i kod jest prostszy (chociaż jest to oczywiste, definiowanie obiektów towarzyszących „STUB” nie jest do końca proste)

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

    companion object {
        val STUB = TestEntity()
    }
}

A potem na zajęcia, które dotyczą TestEntity

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

Jak wspomniał @pawelbial, nie zadziała to tam, gdzie TestEntityklasa „ma” TestEntityklasę, ponieważ STUB nie zostanie zainicjowany po uruchomieniu konstruktora.

dan.jones
źródło
1

Pomogły mi te linie budowania Gradle:
https://plugins.gradle.org/plugin/org.jetbrains.kotlin.plugin.jpa/1.1.50 .
Przynajmniej jest zbudowany w IntelliJ. W tej chwili nie działa w linii poleceń.

I mam

class LtreeType : UserType

i

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

var path: LtreeType nie działa.

allenjom
źródło
1

Jeśli dodałeś wtyczkę gradle https://plugins.gradle.org/plugin/org.jetbrains.kotlin.plugin.jpa ale nie zadziałała, prawdopodobnie wersja jest przestarzała. Byłem na 1.3.30 i mi to nie wyszło. Po uaktualnieniu do 1.3.41 (najpóźniej w momencie pisania), zadziałało.

Uwaga: wersja kotlin powinna być taka sama jak ta wtyczka, np .: tak dodałem oba:

buildscript {
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath "org.jetbrains.kotlin:kotlin-noarg:$kotlin_version"
    }
}
Liang Zhou
źródło
Pracuję z Micronaut i mam go do pracy z wersją 1.3.41. Gradle mówi, że moja wersja Kotlin to 1.3.21 i nie widziałem żadnych problemów, wszystkie inne wtyczki ('kapt / jvm / allopen') są włączone 1.3.21 Również używam wtyczek w formacie DSL
Gavin