loading...

Mapping JPA entities with Kotlin

livioribeiro profile image Livio Ribeiro Updated on ・1 min read

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.

Posted on Nov 10 '17 by:

Discussion

markdown guide
 

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

 

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?

 

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.

 

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:

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.

 

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