Spring Boot, Hibernate and Kotlin for beginners step by step

Hello everyone, my name is Oleg, I’m a technical expert in DomKlik. In our team, the kernel of the stack is Kotlin and Spring Boot. I want to share with you my experience on the interaction and the features of working with PostgreSQL and Hibernate in conjunction with Spring Boot and Kotlin. Also, using the example of a microservice, I will show the advantages of Kotlin and its differences from a similar Java application. I’ll talk about the not-so-obvious difficulties that newcomers may encounter when using this stack with Hibernate. This article will be useful for developers who want to switch to Kotlin and are familiar with Spring Boot, Hibernate Java.
Plugins
For the application on Kotlin, as the project collector, take Gradle Kotlin DSL. The list of connected plugins will be standard for Spring Boot, and for Kotlin with Hibernate we will have several new ones:
plugins {
id("org.springframework.boot") version "2.2.7.RELEASE"
id("io.spring.dependency-management") version "1.0.9.RELEASE"
kotlin("jvm") version "1.3.72"
kotlin("plugin.spring") version "1.3.72"
kotlin("plugin.jpa") version "1.3.72"
}
Consider the last three.
kotlin(«jvm»)
– The basic Kotlin plugin for working on the JVM. Without which not a single application on the Java stack will start.
kotlin(«plugin.spring»)
– since classes in Kotlin are final by default, this plugin will automatically make classes marked with annotations @Component
, @Async
, @Transactional
, @Cacheable
and @SpringBootTest
open to inheritance, and in the subject matter of this article, this will allow classes written in Kotlin to be proxied in Spring through a CGLib proxy.
It’s important to note that entities marked with annotations @Entity
, @MappedSuperclass
and @Embaddable
will not open
after connecting the plugin. Moreover, get accessor
’S will also be final, and then we will lose the opportunity to work with entity reference
. To avoid this and do Entity
and its fields open
, add to build.gradle.kts:
allOpen {
annotation("javax.persistence.Entity")
annotation("javax.persistence.MappedSuperclass")
annotation("javax.persistence.Embeddable")
}
kotlin(«plugin.jpa»)
– If the previous two plugins are applied to any application on Kotlin + Spring Boot, then the next one is already directly related to Hibernate. And he, as you know, for initialization Entity
uses reflection and initializes a class with a constructor with no arguments. But since we write in Kotlin, such a constructor may not be found. If we defined our own primary constructor (primary constructor), then at boot Entity
we will throw an exception:
org.hibernate.InstantiationException: No default constructor for entity
Dependencies
The set of dependencies will also not be completely identical to the set in Java:
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
implementation("org.liquibase:liquibase-core")
runtimeOnly("org.postgresql:postgresql")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.testcontainers:testcontainers:$testContainersVer")
testImplementation("org.testcontainers:postgresql:$testContainersVer")
}
Add a couple of dependencies in addition to the standard Spring Boot web starter and the main starter that interests us org.springframework.boot:spring-boot-starter-data-jpa
, which as the default JPA implementation pulls Hibernate:
org.jetbrains.kotlin:kotlin-reflect
– needed for reflection on Kotlin, which is already supported in Spring Boot and is widely used to initialize classes.
org.jetbrains.kotlin:kotlin-stdlib-jdk8
– Adds the ability to work with Java collections, stream support and much more.
This is where the differences in configuring a Kotlin project compared to Java end with us, let’s move on to the project itself, its structure of tables and entities.
Tables and Entities
Our application will consist of two department and employee tables, which are linked by a one-to-many relationship.
Table structure:

As a base we will use the DBMS PostgreSQL. Create the table structure using liquibase, and as test dependencies we will use the standard starter:
org.springframework.boot:spring-boot-starter-test
– we will test in Docker using testcontainers.
Entities
As with any application with more than one entity, create a common ancestor for everyone entity
.
BaseEntity
:
@MappedSuperclass
abstract class BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: T? = null
override fun equals(other: Any?): Boolean {
other ?: return false
if (this === other) return true
if (javaClass != ProxyUtils.getUserClass(other)) return false
other as BaseEntity<*>
return this.id != null && this.id == other.id
}
override fun hashCode() = 25
override fun toString(): String {
return "${this.javaClass.simpleName}(id=$id)"
}
}
DepartmentEntity
:
@Entity
@Table(name = "department")
class DepartmentEntity(
val name: String,
@OneToMany(
mappedBy = "department",
fetch = FetchType.LAZY,
orphanRemoval = true,
cascade = [CascadeType.ALL]
)
val employees: MutableList = mutableListOf()
) : BaseAuditEntity() {
fun addEmployee(block: DepartmentEntity.() -> EmployeeEntity) {
employees.add(block())
}
fun setEmployees(block: DepartmentEntity.() -> MutableSet) {
employees.clear()
employees.addAll(block())
}
}
EmployeeEntity
:
@Entity
@Table(name = "employee")
class EmployeeEntity(
val firstName: String,
var lastName: String? = null,
@ManyToOne
@JoinColumn(name = "department_id")
val department: DepartmentEntity
) : BaseAuditEntity()
We do not use data classes. This seems like a clear advantage to Kotlin over Java (prior to version 14), and there is an explanation for this.
Why not use?
Data classes, in addition to being final in themselves, have certain fields for all fields equals
, hashCode
and toString
. And this unacceptable in conjunction with Hibernate.
Why? And also why hashCode
always equal to constant – the answer is in Hibernate documentation. Specifically, we are interested in this section:
Although using a natural-id is best for equals and hashCode, sometimes you only have the entity identifier that provides a unique constraint.
It’s possible to use the entity identifier for equality check, but it needs a workaround:
- you need to provide a constant value for hashCode so that the hash code value does not change before and after the entity is flushed.
- you need to compare the entity identifier equality only for non-transient entities.
That is, you need to compare either natural id
, or, as in our example, by primary key id
. This will avoid a lot of problems when comparing the entity and save from its loss when using the entity as an element in Set
.
The presence of toString
, defined by all fields, completely kills all laziness, for example, when journalizing an entity, since all fields will be initialized for output to a string.
Given the features of Hibernate, this Kotlin functionality is not suitable for us.
Class constructor
Kotlin allows you to set variables through the constructor, which is a sin not to use. Consider again DepartmentEntity
:
class DepartmentEntity(
val name: String,
@OneToMany(
mappedBy = "department",
fetch = FetchType.LAZY,
orphanRemoval = true,
cascade = [CascadeType.ALL]
)
val employees: MutableList = mutableListOf()
) : BaseAuditEntity() {
We can also initialize the unit name through the constructor, for example:
departmentRepository.save(DepartmentEntity(name = "Department One"))
Through the constructor, you can initialize, among other things, the list of employees employees. Collections, of course, will be declared mutable.
Use var / val depending on the need to change the field
We marked the name of the organization as val
:
class DepartmentEntity(
val name: String,
and it cannot be null
.
The choice var
/val
is a convenient option and depends on the business logic. Choose between var
and val
it is necessary proceeding from the requirement: whether the entity field should be mutable.
Null validity in fields according to DB only
As for the validity of values null
everything is not so simple in the fields. Earlier we plunged a bit into the depths of Hibernate: speaking of plugin.jpa, I mentioned the use of a constructor with no arguments when initializing an entity.
When initializing fields, reflection is also used. And if the value in the corresponding column was stored in the database null
, then the class is quietly initialized with this field with the value null
. When accessing it, we run the risk of getting NPE, although the field is marked as not nullable
. To avoid this, it is necessary to monitor the synchronism of the structure of tables and classes.
If you look at what is described in the last two sections more comprehensively, then these rules apply not only to primitives, but also to a bunch of entities.
For instance, EmployeeEntity
always attached to DepartmentEntity
:
class EmployeeEntity(
val firstName: String,
var lastName: String? = null,
@ManyToOne
@JoinColumn(name = "department_id")
val department: DepartmentEntity
) : BaseAuditEntity()
Department
is not null
and it cannot be changed, which can save you from all kinds of errors, especially if business logic requires immutability.
Repositories
When using Kotlin, repositories out of the box now have a validation check null
. So, if we are sure that when searching department
by name the result will be unique and unique, then you can specify the return type as non nullable
:
interface DepartmentRepository : JpaRepository {
fun findOneByName(name: String) : DepartmentEntity
}
Here DepartmentEntity
specified sole and cannot be null
. If for some reason we did not find the desired department
, then we’ll catch not NPE, but something else:
org.springframework.dao.EmptyResultDataAccessException: Result must not be null!
This processing is achieved by adding specialized Kotlin support to MethodInvocationValidator
and ReflectionUtils
in spring data commons.
lateinit var
Another Kotlin feature that I would like to consider is lateinit var
.
Add a new ancestor class: BaseAuditEntity
.
@MappedSuperclass
@EntityListeners(AuditingEntityListener::class)
abstract class BaseAuditEntity : BaseEntity() {
@CreatedDate
@Column(updatable = false, nullable = false)
lateinit var created: LocalDateTime
@LastModifiedDate
@Column(nullable = false)
lateinit var modified: LocalDateTime
}
Consider application lateinit var
on the example of audit fields (created
, modified
)
lateinit var
– this not null
field with delayed initialization. Access to the field before its initialization generates an error:
kotlin.UninitializedPropertyAccessException: lateinit property has not been initialized
As a rule, we refer to the fields created
and modified
after the entity has been stored in the database. In our case, the data in these fields are supplied at the save stage and they not null
then lateinit var
more than suitable for us.
Summary
We created an application that took into account many of the benefits of Kotlin, and looked at the important differences from Java, avoiding many hidden surprises. I would be glad if this article is useful not only for beginners. Later we will continue the topic of microservice communication with the database.