Kotlin is a very interesting programming language created by JetBrains that runs on the JVM. One of its features is data classes, which are classes whose main purpose is to hold data. One might think that data classes are perfect to map database entities, and they are actually very good, but you will to think a little more in order to use them correctly with JPA.
Data classes automatically implement toString()
, equals()
and hashCode()
based on the properties defined in the primary constructor. Because of this, you need to think about what properties should be used to tell whether two objects are equal or not.
There is one caveat when using data classes with JPA: data classes require a primary constructor with at least one parameter and JPA requires a constructor without arguments. To help with this, Kotlin has the no-arg compiler plugin that generates a zero-argument constructor that can only be called using reflection. Kotlin also provides the jpa plugin that wrapps the no-arg
plugin configured for JPA.
Let's take the following schema of a task list application:
CREATE TABLE IF NOT EXISTS "task_list" (
"id" SERIAL PRIMARY KEY,
"name" VARCHAR(200) NOT NULL
);
CREATE TABLE IF NOT EXISTS "task" (
"id" SERIAL PRIMARY KEY,
"task_list_id" INTEGER NOT NULL,
"name" VARCHAR(200) NOT NULL,
"description" TEXT,
"due_date" DATE,
"done" BOOLEAN DEFAULT FALSE,
FOREIGN KEY ("task_list_id") REFERENCES "task_list" ("id")
);
For this schema, we can see that we will need two entities: TaskList
and Task
. First let's map the TaskList
:
package com.example.domain
import javax.persistence.*
@Entity
data class TaskList(
@Id @GeneratedValue
val id: Int = 0,
val name: String
) {
@OneToMany(mappedBy = "taskList")
val tasks: MutableSet<Task> = HashSet()
}
The id
and name
properties are defined in the primary constructor, but tasks
is defined in the class body. This way, only id
and name
are used for equals()
, hashCode()
and toString()
. Since tasks
maps a relationship, it is better to left it out of the primary constructor.
Now to the Task
:
package com.example.demo.domain
import java.time.LocalDate
import javax.persistence.*
@Entity
data class Task(
@Id @GeneratedValue
val id: Int = 0,
val name: String,
val description: String? = null,
val dueDate: LocalDate? = null,
val done: Boolean = false
) {
@ManyToOne
lateinit var taskList: TaskList
constructor(name: String, taskList: TaskList) : this(name = name) {
this.taskList = taskList
}
}
Same way with TaskList
, but now we have the inverse relationship with Task
. We define the attribute taskList
as lateinit
for two reasons: first, because it will be lazy loaded; and second, so we do not need to initialize it right away, but we assign it on the secondary constructor.
If we put all the attributes in the primary constructor, simply calling println(task)
would cause our application to crash with a infinite recursion because Task::toString()
would call TaskList::toString()
that would call Task::toString()
and so on.
Top comments (6)
Hello,
Thanks for your article.
I wrote a code based on what you wrote, and have difficulties to persist an entity in the database.
I added a persistence.xml file at the following path: src/main/resources/META-INF
And the code to persist my entity is:
I added "javax.persistence-api" version 2.2 as dependency in my pom.xml and also enabled the JPA support as described here kotlinlang.org/docs/reference/comp....
I am pretty sure the persistence.xml file is not detected because the error is the same when I delete it.
Could you provide the full working code of your example so I can try to reproduce it ?
Thanks in advance for your help,
Tony
Shouldn't I have to declare @JoinColumn(name = "task_list_id")? I keep getting the following error:
ERRO: column planos0_.convenio_id_convenio does not exist
In my case, Plano is task and Convenio is TaskList.
Hi, Livio! Great post. I'm interested in how to do deletions in onetomany relation. Im looking for help. I have User Role classes, and I want, when I deleting my Role, I want to kill relation link on User(role_id = null, like so on). How can I achieve this?
Short & sweet.
In most cases I find that common entity fields like id, verison, dateCreated, lastUpdated are pulled up in a super base entity class. Could you please explain that scenario too ?
Sorry for the late reply.
I did some tests and found out that inheritance with data classes can be tricky, specially if you have to set fields on the base class, since you cannot have arguments on the primary constructor that are not properties.
If the fields in the base class are auto generated (like Id, creation date), you can do something like this:
For more complex cases, I believe it is better to use normal classes.
I love it every time I get a DEV post as a Google search result in the first 5 or so links!
Thanks for sharing this, Livio :)