DEV Community

Livio Ribeiro
Livio Ribeiro

Posted on • Edited on

Mapping JPA entities with Kotlin

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)

Collapse
 
tonyengineering profile image
tony-engineering

Hello,

Thanks for your article.

I wrote a code based on what you wrote, and have difficulties to persist an entity in the database.

javax.persistence.PersistenceException: No Persistence provider for EntityManager named PersistenceProviderMysql

    at javax.persistence.Persistence.createEntityManagerFactory(Persistence.java:85)
    at javax.persistence.Persistence.createEntityManagerFactory(Persistence.java:54)
    at KrakenConnector.saveLastPrice(KrakenConnector.kt:42)
    at KrakenConnector.getAndSaveLastPrice(KrakenConnector.kt:17)

I added a persistence.xml file at the following path: src/main/resources/META-INF

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.1"
             xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">
    <persistence-unit name="PersistenceProviderMysql" transaction-type="RESOURCE_LOCAL">
        <class>KrakenConnector</class>
        <properties>
            <!-- Configuring The Database Connection Details -->
            <property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver" />
            <property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/jpaDemoDb" />
            <property name="javax.persistence.jdbc.user" value="root" />
            <property name="javax.persistence.jdbc.password" value="" />
        </properties>
    </persistence-unit>
</persistence>

And the code to persist my entity is:

fun saveLastPrice(ohlcs: KrakenOHLCs) {
        val krakenOHLCsPersistable = KrakenOHLCsPersistable(ohlcs.ohlCs, 1)

        val emFactory: EntityManagerFactory = Persistence.createEntityManagerFactory("PersistenceProviderMysql")
        val entityManager = emFactory.createEntityManager()
        entityManager.transaction.begin()
        entityManager.persist(krakenOHLCsPersistable)
        entityManager.transaction.commit()
        entityManager.close()
        emFactory.close()
    }

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

Collapse
 
felipebelluco profile image
Felipe Belluco • Edited

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.

Collapse
 
00babe9 profile image
00babe9

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?

Collapse
 
ksambhavjain profile image
Kumar Sambhav Jain

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 ?

Collapse
 
livioribeiro profile image
Livio Ribeiro

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:

import java.time.LocalDate
import java.time.LocalDateTime

open class Base(
        val id: Int? = null,
        val dateCreated: LocalDateTime = LocalDateTime.now(),
        val lastUpdated: LocalDateTime = LocalDateTime.now(),
)

data class Task(
        var name: String,
        var description: String? = null,
        var dueDate: LocalDate? = null,
        var done: Boolean = false
) : Base()

For more complex cases, I believe it is better to use normal classes.

Collapse
 
chrisvasqm profile image
Christian Vasquez

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 :)