DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • Programming Solutions for Graph and Data Structure Problems With Implementation Examples (Word Dictionary)
  • Testcontainers With Kotlin and Spring Data R2DBC
  • Building a Kotlin Mobile App With the Salesforce SDK: Editing and Creating Data
  • The First Annual Recap From JPA Buddy

Trending

  • How to Submit a Post to DZone
  • Architecting Zero-Trust AI Agents: How to Handle Data Safely
  • Rethinking Java CRUDs With Event Sourcing and CQRS Patterns
  • A Deep Dive into Tracing Agentic Workflows (Part 1)
  1. DZone
  2. Data Engineering
  3. Data
  4. Kotlin Data Classes With JPA

Kotlin Data Classes With JPA

Want to learn more about implementing data classes with the Java Persistence API (JPA)? Check out this post to learn more about using data classes in Kotlin.

By 
Steve Gertiser user avatar
Steve Gertiser
·
Updated Nov. 02, 18 · Presentation
Likes (4)
Comment
Save
Tweet
Share
29.1K Views

Join the DZone community and get the full member experience.

Join For Free

Introduction

In Kotlin, data classes are handy and provide default implementation for equals(),  hashCode(), copy(), and  toString(). You get the implementation of these functions free of charge. For standard value classes, they are excellent, but you still need to understand what is going on particularly when using this feature within enterprise frameworks, such as Spring, sundry, and the widely-used Jakarta EE technologies, such JAX-RS or JPA. Because if you do not, you may get more or less than what you bargained for.

What Is a Data Class?

In a Kotlin data class, all properties that you declare in the primary constructor will be considered in the aforementioned functions.

Therefore, in the class:

data class Book(val isdn: String, val title: String)               


Both properties isdn and title will be considered. This is good for toString() and copy(); the information provided is clearly helpful both to programmers and operations staff. For JPA, however, we need more-considered hashCode() and equals() implementations. In the above class in a JPA setting, title would presumably be a superfluous value in those two methods. If we modify the class for JPA under the consideration of the recommended equals() and hashCode() implementations (see the article by Hibernate guru Vlad Mihalcea here), it would look like this:

package test.model

import javax.persistence.Entity
import javax.persistence.Id

/**
 *  For JPA, we only relevant property for equals and hashCode is the isdn, so that
 *  is the only thing that goes into the primary constructor. This comes at the cost
 *  of having an insufficient copy and equals method. The benefit of a Kotlin data class
 *  is compromised.
 */
@Entity
data class JpaBook(@Id val isdn: String = "undefined") {

    var title : String = ""
    // note this unpretty hack to get immutability back
    // since we cannot use this property in the primary constructor
        private set

    constructor(isdn: String, title: String) : this(isdn) {
        this.title = title
    }

}


We removed the title from the primary constructor, which is good for JPA, But, we broke toString() in the process. We are missing desired information, namely the title of the book, which is information that could have proved quite helpful in debugging.

    @Test
    fun `demonstrate that toString is not what we actually want`() {

        val isdn = "978-3-16-148410-0"
        val title = "Wuthering Heights"
        val book = JpaBook(isdn, title)
        print(book)
        // here we are asserting that toString is _not_ what we want.
        assertThat(book.toString(), Is(not("Book(isdn=$isdn, title=$title)")))
    }


Not only that, but we also broke the useful copy() magic:

    /**
     * This is a Kotlin data class API exerciser, which illustrates
     * how the interests of an entity class and a Kotlin data class conflict.
     * The copy() function, in the case of a JpaBook, does not allow you to copy the title.
     */
    @Test
    fun `copy does not handle properties that are not in the primary constructor`() {
        // Can copy both properties
        val classicalKotlinDataClassBookFunctions = Book::class.functions
        val functionNamesOfClassicalDataClassBook: MutableList<String> = mutableListOf()
        classicalKotlinDataClassBookFunctions.mapTo(functionNamesOfClassicalDataClassBook) { it.toString() }
        assertThat(functionNamesOfClassicalDataClassBook, hasItem("fun test.model.Book.copy(kotlin.String, kotlin.String): test.model.Book"))

        // Cannot copy both properties
        val jpaBookFunctions = JpaBook::class.functions
        val functionNamesOfJpaBook: MutableList<String> = mutableListOf()
        jpaBookFunctions.mapTo(functionNamesOfJpaBook) { it.toString() }
        assertThat(functionNamesOfJpaBook, not(hasItem("fun test.model.Book.copy(kotlin.String, kotlin.String): test.model.Book")))
    }


Finally, we have broken Kotlin support for immutability and have had to provide a workaround by making the setter private.

The issue is that, in a common JPA setting, equals()/hashCode() satisfy one need, toString() another, and copy() yet a third. But in Kotlin, data class implementation is all jumbled together. We need to unravel this coupling.

Business Keys

If you have the lucky situation in which say a single field satisfies what you need for hashCode() and equals(), such as a business ID, then you can declare this in the primary constructor. In Hibernate, one would annotate such a field with  @NaturalId. In order to achieve non-nullable fields, there are several approaches. First, you can implement secondary constructors with default values, or you can simply declare the values as properties. Doing so will, however, break immutability.

package test.model

import au.com.console.kassava.kotlinEquals
import org.hibernate.annotations.NaturalId
import java.util.*
import javax.persistence.*

/**
 * Hibernate has a nifty @NaturalId annotation, and as such we must
 * override equals and hashCode to use that and only that. If you are not familiar
 * with NaturalId, it is essentially a business key, or what is also known as a
 * friendly id.
 * <p>
 * The isdn field, which is marked as a NaturalId, is never null, and as such we can use it.
 * <p>
 * Note how we are not using id in equals or hashCode, and cannot have it in the constructor
 * for that reason.
 * <p>
 * @author S Gertiser, created 2018-08-31.
 */
@Entity
@EntityListeners(UuidPopulatorPersistenceListener::class)
@Access(AccessType.FIELD)
data class BookWithNaturalId(
                  /** Friendly  or business key */
                  @NaturalId
                  val isbn: String = "undefined",
                  val title: String = "undefined" ) : Identifiable<String> {

    private var _id: String = "undefined"

    @Id
    @Access(AccessType.PROPERTY)
    override fun getId(): String {
        return _id
    }

    override fun setId(id: String) {
        _id = id
    }

    // Here we optionally define properties for equals in a companion object.
    // In this way, Kotlin will generate fewer KProperty classes,
    // and we won't have array creation for every method call.
    // More on this later ...
    companion object {
        private val equalsProperties = arrayOf(BookWithNaturalId::isbn)
    }

    override fun equals(other: Any?) = kotlinEquals(other = other, properties = equalsProperties)


    /**
     * isbn is always unique and is the only thing we need in hashCode.
     */
    override fun hashCode(): Int {
        return Objects.hash(isbn)
    }


}


Solution

There is nothing stopping you from overriding any of the four methods declared to be the subject of attention for data classes. That can become onerous over time, not to mention it begins to detract from the readability and conciseness of our code, which was a primary motivator for JetBrains to invent the data class construct.

The open-source solution kassava can provide more flexibility for Kotlin data classes. The above examples used kassava to granularly define equals() and toString() where they necessarily diverged from standard data class primary constructor usage.

Conclusion

In the context of enterprise frameworks, such as JPA, Kotlin data classes are too generically defined as advisable for implicitly defining the properties used in the  equals(), hashCode(), copy(), and toString() functions in most cases. By using a handy extension, we overcome these limitations. Kotlin data classes are certainly no worse than a standard class, in which the defaults are also rarely suitable. See the reference for full source code including extensive test cases.

Data (computing) Kotlin (programming language)

Opinions expressed by DZone contributors are their own.

Related

  • Programming Solutions for Graph and Data Structure Problems With Implementation Examples (Word Dictionary)
  • Testcontainers With Kotlin and Spring Data R2DBC
  • Building a Kotlin Mobile App With the Salesforce SDK: Editing and Creating Data
  • The First Annual Recap From JPA Buddy

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook