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 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</artifactId>
    <version>1.8.0</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 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.8.0</version>
    <configuration>
         <compilerPlugins>
             <plugin>spring</plugin>
             <plugin>jpa</plugin>
         </compilerPlugins>
    </configuration>
    <dependencies>
        <dependency>
        <groupId>org.jetbrains.kotlin</groupId>
        <artifactId>kotlin-maven-noarg</artifactId>
        <version>1.8.0</version>
        </dependency>
    </dependencies>
    <!--...-->
</plugin>

4. JPA With Kotlin Classes

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

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

@Entity
class Person(
    @Column(nullable = false)
    val name: String,
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Int?=null,
)

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

Note: Make sure to place the id attribute in last place as it is optional and auto-generated.

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

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

        assertTrue(personToSave.name == personFound.name)
    })
}

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 a 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 – name and id:

@Entity
class PhoneNumber(   
    @Column(nullable = false)
    val number: String,
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Int?=null,
)

Let’s verify this with a test:

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

        assertTrue(personToSave.name == personFound.name)
    })
}

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(          
          "John", 
          "[email protected]", 
          Arrays.asList(PhoneNumber("202-555-0171"), PhoneNumber("202-555-0102")))
        session.persist(personToSave)
        val personFound = session.find(Person::class.java, personToSave.id)
        session.refresh(personFound)

        assertTrue(personToSave.name == personFound.name)
    })
}

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. Data Classes

Kotlin Data Classes are normal classes with extra functionality that make them suitable as data holders. Among those extra functions are default implementations for equals, hashCode, and toString methods.

Naturally, we might argue that we could use Kotlin Data Classes as JPA entities. As opposed to what comes naturally here, using data classes as JPA entities is generally discouraged. This is mostly because of the complex interactions between the JPA world and those default implementations provided by the Kotlin compiler for each data class.

For the sake of demonstration, we’re going to use this entity:

@Entity
data class Address(    
    val name: String,
    @OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.LAZY)
    val phoneNumbers: List<PhoneNumber>,
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Int? = null,
)

5.1. The equals and hashCode Methods

Let’s start with equals and hashCode implementations. Most JPA entities contain at least one generated value — for example, auto-generated identifiers. This means that some properties are generated only after we persist them into the database.

So, the calculated equals and hashCode are different before and after persistence, as some properties used during equals and hashCode calculations are generated after persistence. Therefore, we should be careful when using data class JPA entities with hash-based collections:

@Test
fun givenAddressWithDefaultEquals_whenAddedToSet_thenNotFound() {
    doInHibernate({ sessionFactory() }) { session ->
        val addresses = mutableSetOf<Address>()
        val address = Address(name = "Berlin", phones = listOf(PhoneNumber("42")))
        addresses.add(address)

        assertTrue(address in addresses)
        session.persist(address)
        assertFalse { address in addresses }
    }
 }

In the above example, even though we’ve added the address into the set, we can’t find it in the set after persisting it into the database. This happens because the hashcode value changes after we persist the address.

In addition to this, the simple equals and hashCode implementations are not good enough to be used in JPA entities.

5.2. Unwanted Fetching of Lazy Associations

The Kotlin compiler generates the default method implementations based on all properties of a data class. When we’re using data classes for JPA entities, some of those properties might be a lazy association with the target entity.

Given all this, sometimes a harmless call to toString, equals, or hashCode might issue a few more queries to load the lazy associations. This may hurt the performance, especially when we don’t even need to fetch those associations:

@Test
fun givenAddress_whenLogging_thenFetchesLazyAssociations() {
    doInHibernate({ this.sessionFactory() }) { session ->
        val addressToSave = Address(name = "Berlin", phoneNumbers = listOf(PhoneNumber("42")))
        session.persist(addressToSave)
        session.clear()

        val addressFound = session.find(Address::class.java, addressToSave.id)
            
        assertFalse { Hibernate.isInitialized(addressFound.phoneNumbers) }
        logger.info("found the entity {}", addressFound) // initializes the lazy collection
        assertTrue(Hibernate.isInitialized(addressFound.phoneNumbers))
    }
}

In the above example, a seemingly innocent log statement triggers an extra query:

Hibernate: select * from Address address0_ where address0_.id=?
Hibernate: select * from Address_PhoneNumber phonenumbe0_ inner join PhoneNumber phonenumbe1_ on phonenumbe0_.phoneNumbers_id=phonenumbe1_.id where phonenumbe0_.Address_id=?

6. Conclusion

In this quick article, we saw an example of how to integrate Kotlin classes with JPA using the jpa-plugin and Hibernate. Moreover, we saw why we should be judicious when using data classes as JPA entities.

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


« 上一篇: Kotlin中的对象