How to implement equals() and hashCode() for a Kotlin entity

Posted by Java developer blog on November 27, 2019

Overview

In the previous post we have reviewed three ways of how to implement equals()/hashCode() for a Java entity.

In the post we are going to describe how to implement equals()/hashCode() for a Kotlin entity, discuss the usage Kotlin jpa compiler plugin and Kotlin data class for an entity.

Different types of implementation equals()/hashCode()

Let’s recall three main types of how to implement equals()/hashCode() for an entity: using a Generated Primary Key, using a Programmatically Managed Primary Key and using a Business Key.

Also, implementation of equals()/hashCode() has to follow equals and hashCode contract.

Generated Primary Key approach

Generated Primary Key approach calculates equals()/hashCode() based on sequence generated primary key:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import java.time.LocalDate
import javax.persistence.Entity
import javax.persistence.GeneratedValue
import javax.persistence.GenerationType
import javax.persistence.Id

/**
 * Using a Generated Primary Key
 */
@Entity
class MyEntity(
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    val id: Long = 0,
    val date: LocalDate,
    val message: String
) {
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as MyEntity

        if (id == 0L && id != other.id) return false

        return true
    }

    override fun hashCode(): Int {
        return 13
    }
}

Notice that in hashCode() implementation we return the same number. Otherwise, we will break the contract due to different id values before and after saving to a database.

The main difference between Kotlin and Java version is the fact that Kotlin version uses immutable and nonnull fields. Hibernate assumes that an uninitialized entity has id field set to either 0 or null. As a result, we need to set id field value to 0 for an uninitialized entity and check for 0 instead of null in the equals method compare to the Java version.

Programmatically Managed Primary Key approach

Programmatically Managed Primary Key approach calculates equals/hashCode based on a constructor parameter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import java.time.LocalDate
import javax.persistence.Entity
import javax.persistence.Id

/**
 * Using a Programmatically Managed Primary Key
 */
@Entity
class MyEntity(
    @Id
    val id: Long,
    val date: LocalDate,
    val message: String
) {
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as MyEntity

        if (id == 0L && id != other.id) return false

        return true
    }

    override fun hashCode(): Int {
        return id.hashCode()
    }
}

In this approach, a unique id is generated outside and we use a standard implementation of equals()/hashCode() for an id parameter except for the fact that we check for 0 instead of null in the equals method compare to the Java version.

Business Key approach

Business Key approach calculates equals()/hashCode() based on a constructor parameter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import java.time.LocalDate
import javax.persistence.Entity
import javax.persistence.GeneratedValue
import javax.persistence.GenerationType
import javax.persistence.Id

/**
 * Using a Business Key
 */
@Entity
class MyEntity(
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    val id: Long = 0,
    val date: LocalDate,
    val message: String,
    val businessKey: String
) {
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as MyEntity

        if (id == 0L && businessKey != other.businessKey) return false

        return true
    }

    override fun hashCode(): Int {
        return businessKey.hashCode()
    }
}

This approach is similar to the Programmatically Managed Primary Key approach except for the fact that we use a standard implementation of equals()/hashCode() for a businessKey parameter.

Kotlin jpa compiler plugin

Both Hibernate and JPA require that an entity class should have a no-argument constructor.

Kotlin does not generate default constructor by default.

However, we could use jpa compiler plugin (optionally add spring if you use spring) for that purpose:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<plugin>
  <groupId>org.jetbrains.kotlin</groupId>
  <artifactId>kotlin-maven-plugin</artifactId>
  <configuration>
    <args>
      <arg>-Xjsr305=strict</arg>
    </args>
    <compilerPlugins>
      <plugin>spring</plugin>
      <plugin>jpa</plugin>
    </compilerPlugins>
  </configuration>
  <dependencies>
    <dependency>
      <groupId>org.jetbrains.kotlin</groupId>
      <artifactId>kotlin-maven-allopen</artifactId>
      <version>${kotlin.version}</version>
    </dependency>
    <dependency>
      <groupId>org.jetbrains.kotlin</groupId>
      <artifactId>kotlin-maven-noarg</artifactId>
      <version>${kotlin.version}</version>
    </dependency>
  </dependencies>
</plugin>

jpa plugin generates no-arg constructor for a @Entity, @Embeddable and @MappedSuperclass annotations automatically.

Kotlin data class

Personally, I don’t recommend to use a data class for an entity. The reason for that is because data class automatically generates wrong implementation for equals()/hashCode() that do not follow the contract.

Conclusion

We have described three ways of how to implement equals()/hashCode() for a Kotlin entity. In particular, we described how to implement equals()/hashCode() using a Generated Primary Key, using a Business Key and using a Programmatically Managed Primary Key. We also compared Kotlin and Java versions for the same entity. The main difference between them is the fact that Kotlin version uses immutable and nonnull fields. We also discussed the usage Kotlin jpa compiler plugin and Kotlin data class for an entity.