Working with Kotlin and JPA

1. Introduction

One of Kotlin’s characteristics is the interoperability with Java libraries, and JPA is certainly one of these.

In this tutorial, we’ll explore how to use Kotlin Data Classes as JPA entities.

2. Dependencies

To keep things simple, we’ll use Hibernate as our JPA implementation; we’ll need to add the following dependencies to our Maven project:

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>5.2.15.Final</version>
</dependency>
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-testing</artifactId>
    <version>5.2.15.Final</version>
    <scope>test</scope>
</dependency>

We’ll also use an H2 embedded database to run our tests:

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>1.4.196</version>
    <scope>test</scope>
</dependency>

For Kotlin we’ll use the following:

<dependency>
    <groupId>org.jetbrains.kotlin</groupId>
    <artifactId>kotlin-stdlib-jdk8</artifactId>
    <version>1.2.30</version>
</dependency>

Of course, the most recent versions of Hibernate, H2, and Kotlin can be found in Maven Central.

3. Compiler Plugins (jpa-plugin)

To use JPA, the entity classes need a constructor without parameters.

By default, the Kotlin data classes don’t have it, and to generate them we’ll need to use the jpa-plugin:

<plugin>
    <artifactId>kotlin-maven-plugin</artifactId>
    <groupId>org.jetbrains.kotlin</groupId>
    <version>1.2.30</version>
    <configuration>
        <compilerPlugins>
        <plugin>jpa</plugin>
        </compilerPlugins>
        <jvmTarget>1.8</jvmTarget>
    </configuration>
    <dependencies>
        <dependency>
        <groupId>org.jetbrains.kotlin</groupId>
        <artifactId>kotlin-maven-noarg</artifactId>
        <version>1.2.30</version>
        </dependency>
    </dependencies>
    <!--...-->
</plugin>

4. JPA with Kotlin Data Classes

After the previous setup is done, we’re ready to use JPA with data classes.

Let’s start creating a Person data class with two attributes – id and name, like this:

@Entity
data class Person(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Int,

    @Column(nullable = false)
    val name: String
)

As we can see, we can freely use the annotations from JPA like @Entity, @Column and @Id.

To see our entity in action, we’ll create the following test:

@Test
fun givenPerson_whenSaved_thenFound() {
    doInHibernate(({ this.sessionFactory() }), { session ->
        val personToSave = Person(0, "John")
        session.persist(personToSave)
        val personFound = session.find(Person::class.java, personToSave.id)
        session.refresh(personFound)

        assertTrue(personToSave == personFound)
    })
}

After running the test with logging enabled, we can see the following results:

Hibernate: insert into Person (id, name) values (null, ?)
Hibernate: select person0_.id as id1_0_0_, person0_.name as name2_0_0_ from Person person0_ where person0_.id=?

That is an indicator that all is going well.
It is important to note that if we don’t use the jpa-plugin in runtime, we are going to get an InstantiationException, due to the lack of default constructor:

javax.persistence.PersistenceException: org.hibernate.InstantiationException: No default constructor for entity: : com.baeldung.entity.Person

Now, we’ll test again with null values. To do this, let’s extend our Person entity with a new attribute email and a @OneToMany relationship:

    //...
    @Column(nullable = true)
    val email: String? = null,

    @Column(nullable = true)
    @OneToMany(cascade = [CascadeType.ALL])
    val phoneNumbers: List<PhoneNumber>? = null

We can also see that email and phoneNumbers fields are nullable, thus are declared with the question mark.

The PhoneNumber entity has two attributes – id and number:

@Entity
data class PhoneNumber(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Int,

    @Column(nullable = false)
    val number: String
)

Let’s verify this with a test:

@Test
fun givenPersonWithNullFields_whenSaved_thenFound() {
    doInHibernate(({ this.sessionFactory() }), { session ->
        val personToSave = Person(0, "John", null, null)
        session.persist(personToSave)
        val personFound = session.find(Person::class.java, personToSave.id)
        session.refresh(personFound)

        assertTrue(personToSave == personFound)
    })
}

This time, we’ll get one insert statement:

Hibernate: insert into Person (id, email, name) values (null, ?, ?)
Hibernate: select person0_.id as id1_0_1_, person0_.email as email2_0_1_, person0_.name as name3_0_1_, phonenumbe1_.Person_id as Person_i1_1_3_, phonenumbe2_.id as phoneNum2_1_3_, phonenumbe2_.id as id1_2_0_, phonenumbe2_.number as number2_2_0_ from Person person0_ left outer join Person_PhoneNumber phonenumbe1_ on person0_.id=phonenumbe1_.Person_id left outer join PhoneNumber phonenumbe2_ on phonenumbe1_.phoneNumbers_id=phonenumbe2_.id where person0_.id=?

Let’s test one more time but without null data to verify the output:

@Test
fun givenPersonWithFullData_whenSaved_thenFound() {
    doInHibernate(({ this.sessionFactory() }), { session ->
        val personToSave = Person(
          0,
          "John",
          "[email protected]",
          Arrays.asList(PhoneNumber(0, "202-555-0171"), PhoneNumber(0, "202-555-0102")))
        session.persist(personToSave)
        val personFound = session.find(Person::class.java, personToSave.id)
        session.refresh(personFound)

        assertTrue(personToSave == personFound)
    })
}

And, as we can see, now we get three insert statements:

Hibernate: insert into Person (id, email, name) values (null, ?, ?)
Hibernate: insert into PhoneNumber (id, number) values (null, ?)
Hibernate: insert into PhoneNumber (id, number) values (null, ?)

5. Conclusion

In this quick article, we saw an example of how to integrate Kotlin data classes with JPA using the jpa-plugin and Hibernate.

As always, the source code is available over on GitHub.

Leave a Reply

Your email address will not be published.