Bye Bye Hibernate – Discovering alternatives to Hibernate in Kotlin

Hibernate is an Object Relational Mapping (ORM) framework for JVM languages. It provides an abstraction for mapping an object-oriented domain model to a relational database. The framework is a powerful tool to shield developers from dealing with low-level database details.

At bol.com we host a hackathon day every so many months. Developers get to pitch their hackathon idea, so that other developers can join and help them. Sander was curious about other ORM frameworks available for Kotlin. And so, the idea to look for alternatives of Hibernate in Kotlin as a hackathon project came up.

After the idea was pitched, Dimar and Aysegul decided to join the adventure of looking for an alternative, as both have several years of experience in the field of ORM frameworks. Together, we had one day to look at alternatives for Hibernate in Kotlin. This blogpost should provide you with a better understanding of the available frameworks and how they work.

Evaluation criteria

To be able to decide an appropriate alternative for Hibernate we set the following success criteria for an ORM framework:

  • Our project needs to be able to keep existing Kotlin data classes that are used in the domain model, so that changes to the rest of the application stay at a minimum.
  • Preferably boilerplate code stays at a minimum while implementing a framework.
  • A framework should be well maintained and documented, open-source and have a good community based on GitHub statistics. It would be a plus if the framework is already used within bol.com.

Considering we had one day to into alternatives, we made a shortlist of frameworks to investigate during the hackathon day. The following frameworks were selected based on recommendations by others, experiences in other projects and online searches:

Scoping

To try out the different ORM frameworks we chosean existing project, the in-house build experimentation tool.This project implements Hibernate with Spring Data JPA on top of Postgres. The database schema is created and edited using the database migration tool Flyway. The application also uses Hibernate Envers for auditing purposes (1).

Within the project there are plenty of integration tests available. These tests help to validate if a different ORM framework still allows the application to run both technically and functionally.

The project consists of several architectural layers. A boundary layer contains the API as REST services. This boundary layer exposes the API by data transfer objects (DTO’s), separating the outside interface from the internal representation. The business logic services are a separate layer and operate on the domain model. The data access layer stores and retrieves the domain model into and from the Postgres database.

architecture layers

To restrict the total time spent, we focused our effort on a specific repository: the ‘run repository’. An experiment describes the goals and scope of an experiment. A run specifies a period in which a combination of experiments ran, and the learnings from that run. As this is a many-to-many relationship, a linking table (‘experiment-run’) is used.

linking table

One last note we want to make is that we did not focus on extra features that frameworks have to offer. Hibernate for instance offers first and second level caching, eager and lazy fetching, and other features that other ORM frameworks may or may not have. Instead, we focussed on the usage part of the frameworks.

Hibernate

Before diving into the other frameworks, we show you what the current implementation looks like. We do this to get an idea of how the other frameworks compare to Hibernate. In the ORM framework world there are two terms that are important:

  • DSL, which stands for Domain Specific Language. A SQL DSL lets you write SQL queries using Kotlin code. The power in SQL DSL’s is type safety and consistent naming. SQL DSL’s can check your query code during the build of your project. The benefit of this is that you will find breaking changes in the database schema at compile time.
  • DAO, which stands for Data Access Object. As the name suggests, DAO’s provide access to the database, often through the usage of design patterns like Active Record or Repository. An example of this is the previously mentioned ‘run repository’.

This is what the domain model in the current implementation with Hibernate looks like:

@Entity 
@Audited 
data class Experiment( 
    @Id 
    @GeneratedValue(strategy = GenerationType.AUTO) 
    var id: Long? = null, 

    @Column(columnDefinition = "TEXT") 
    var hypothesis: String? = null, 

    @ManyToMany(fetch = FetchType.LAZY, targetEntity = Run::class) 
    @JoinTable( 
        name = "experiment_runs", 
        joinColumns = [JoinColumn(name = "experiment_id")], 
        inverseJoinColumns = [JoinColumn(name = "run_id")] 
    ) 
    var runs: Set<Run>, 
  ) { 
    // Generated with JPA Buddy IntelliJ Plug-in, based on https://github.com/jpa-buddy/kotlin-entities 
    override fun equals(other: Any?): Boolean { 
        if (this === other) return true 
        if (other == null || Hibernate.getClass(this) != Hibernate.getClass(other)) return false 
        other as Experiment 

        return id != null && id == other.id 
    } 
    override fun hashCode(): Int = javaClass.hashCode() 
    override fun toString(): String { 
        return this::class.simpleName + "(id = $id, hypothesis = $hypothesis )" 
    } 
} 

@Entity 
@Audited 
data class Run( 
    @Id 
    @GeneratedValue(strategy = GenerationType.AUTO) 
    var id: Long? = null, 
 
    var startDate: LocalDateTime? = null, 
 
    var endDate: LocalDateTime? = null, 
 
    @Column(columnDefinition = "TEXT") 
    var learning: String? = null, 
) { 
    // Generated with JPA Buddy IntelliJ Plug-in, based on https://github.com/jpa-buddy/kotlin-entities 
    override fun equals(other: Any?): Boolean { 
        if (this === other) return true 
        if (other == null || Hibernate.getClass(this) != Hibernate.getClass(other)) return false 
        other as Run 
 
        return id != null && id == other.id 
    } 
 
    override fun hashCode(): Int = javaClass.hashCode() 
 
    override fun toString(): String { 
        return this::class.simpleName + "(id = $id, startDate = $startDate, endDate = $endDate, learning = $learning )" 
    } 
} 

As you may notice in the above code, we use Kotlin data classes and manually override the equals, hash code and toString methods. Data classes can make it difficult to implement Hibernate correctly (2). Because of this, using Kotlin data classes with Hibernate is often advised against. This does mean you miss the nice features of data classes, like the out of the box copy method. Do you still want to make use of Kotlin data classes with Hibernate? Then we advise you to watch this video and read this article.

The DAO’s with Hibernate are Spring Data CRUD-repositories (CRUD stands for Create Read Update Delete). With these repositories, the most common CRUD queries are implemented out of the box. Custom queries can be written as interface projections, with JPQL or with a native query. With the above annotated domain model, we can write a repository like this:

interface ExperimentRepository : 
    RevisionRepository<Experiment, Long, Int>, 
    CrudRepository<Experiment, Long> { 

          fun findAllByHypothesis(hypothesis: String): List<Experiment> 
 
    @Query("SELECT e FROM Experiment e INNER JOIN e.runs r WHERE r.id = :runId") 
    fun findAllByRunId(runId: Long): List<Experiment> 
} 

The above repository can be used like below. As you can see, the code is concise because Hibernate, in combination with Spring Data, automatically handles tasks such as obtaining a database connection, managing transactions, and generating queries.

experimentRepository.save(experiment) 
-- 
experimentRepository.findAllByRunId(runId) 

Now that we know what our current implementation looks like, let’s look at the frameworks of our shortlist.

Exposed

Exposed is an ORM framework that is developed by JetBrains for Kotlin. As such, it provides a nice way to integrate with the language. This can be seen in for instance the support of Kotlin extension functions and nullability support. For a detailed example, see this blogpost.

With Exposed our data access models consist of three table model objects. Three, because one table object is needed to hold the references between Experiment and Run. Later, we will see that the Experiment DAO class holds a reference to this table object to get all the runs.

object Experiments: LongIdTable("experiment") { 
    val hypothesis = text("hypothesis").nullable() 
} 

object Runs: LongIdTable("run") { 
    val startDate = datetime("start_date").nullable() 
    val endDate = datetime("end_date").nullable() 
    val learning = text("learning").nullable() 
} 

object ExperimentRuns: Table("experiment_runs") { 
    val experiment = reference("experiment", Experiments) 
    val run = reference("run", Runs) 
} 

As you can see, we are not using any JPA annotations as we were used to with Hibernate. This is because Exposed is built directly on top of the JDBC driver. Exposed provides their framework in two flavours, a DSL flavour and a DAO flavour. To execute queries, you will implement either one of these two flavours.

With the DAO flavour you will need to create classes that extend an Exposed ‘Entity’ class. You can choose to let these classes be your domain model. Doing so will pollute the domain model with data access concerns, like the JPA annotations with Hibernate. This is because the mapping from the table objects will be done in these entity classes:

class ExperimentDao(id: EntityID<Long>): LongEntity(id) { 
    companion object : LongEntityClass<ExperimentDao>(Experiments) 
    var hypothesis by Experiments.hypothesis 
    var runs by RunDao.via(ExperimentRuns) 
} 

class RunDao(id: EntityID<Long>): LongEntity(id) 
    companion object : LongEntityClass<RunDao>(Runs) 
    var startDate by Runs.startDate 
    var endDate by Runs.endDate 
    var learning by Runs.learning 
} 

Because we must use classes for this and not data classes, this approach is not preferred in our case. This approach creates extra boilerplate for mapping between the DAO classes and our domain model.
Luckily, the DSL flavour can help us out here. The DSL flavour does not require the creation of extra classes. It allows us to use our existing domain model data classes that are cleaned of the JPA annotations:

data class Experiment( 
    var id: Long? = null, 
    var hypothesis: String? = null, 
    var runs: Set<Run> 
) 

data class Run( 
    var id: Long? = null, 
    var startDate: LocalDateTime? = null, 
    var endDate: LocalDateTime? = null, 
    var learning: String? = null 
) 

Performing queries with Exposed in our Spring Boot project is as easy as with Hibernate. This is because there is a Spring Boot starter available, which like with Hibernate, makes setting up transactions and configuring a data source available out of the box.

The only difference is that there are no out of the box repositories available with default queries, like we are used to with Hibernate and Spring Data JPA. As shown in the example blogpost we mentioned before, you can of course create your own standardised repository, based on generics. Below we show you an example of queries with both the DAO and DSL flavours:

// DAO approach inserting and deleting 
val experiment = ExperimentDao.new { 
  this.hypothesis = hypothesis 
} 
val run = RunDao.new { 
  this.startDate = startDate 
  this.endDate = endDate 
  this.learning = learning 
} 
experiment.runs = experimentDao.runs.orEmpty() + run 

ExperimentDao.findById(experimentId)?.delete() 
// DSL approach inserting and deleting 
val experimentId = Experiments.insert { 
  it[hypothesis] = experiment.hypothesis 
} get Experiments.id 
val runId = Runs.insert { 
  it[startDate] = run.startDate 
  it[endDate] = run.endDate 
  it[learning] = run.learning 
} get Runs.id 
ExperimentRuns.insert { 
    it[experiment] = EntityID(experimentId.value, Experiments) 
    it[run] = EntityID(runId.value, Runs) 
} 

Experiments.deleteWhere { Experiments.id eq experimentId } 

As you can see, in both flavours we need explicit mapping of our domain model into the table objects. The biggest difference is which object we are using to construct our queries. In the DAO flavour we use the DAO class. In the DSL flavour we create the query from the table objects directly.

Summing up, our conclusion about Exposed is that:

  • We like the DSL flavour of Exposed, as it lets us explicitly write SQL queries with type safety.
    This also lets us use the existing data classes of the domain model. However, it does require us to create separate table objects. In our case this adds unwanted boilerplate, while in your use case this separation might be preferable.
  • There is no (CRUD) repository pattern available with default queries, although you can standardise and create one yourself.
  • The documentation of Exposed could use clarification and is not as elaborate as we would have liked. Finding the difference between the DAO and DSL flavours was hard for instance.
  • The framework is quite popular, as it is maintained by JetBrains. Unfortunately, at the time of writing the last release has been over half a year ago. Besides that, the usage within bol.com is low.

Ktorm

After looking into Exposed, we set our eyes onto Ktorm (Kotlin ORM). At a first glance, Ktorm and Exposed feel much alike. Both are directly based on JDBC, use a SQL DSL to define a table model and provide Spring Boot support (see this documentation page).

To give you an idea of the similarities, this is what the Ktorm version of the Exposed table objects looks like:

object Experiments: Table<Experiment>("experiment") { 
    val id = long("id").autoIncrement().primaryKey() 
    val hypothesis = varchar("hypothesis").nullable() 
} 

object Runs: Table<Run>("run") { 
    val id = long("id").autoIncrement().primaryKey() 
    val startDate = datetime("start_date").nullable() 
    val endDate = datetime("end_date").nullable() 
    val learning = varchar("learning").nullable() 
} 

object ExperimentRuns: ManyToManyTable<Experiment, Run>("experiment_runs") { 
    val experiment = long("experiment_id").references(Experiments) { it.userId } 
    val run = long("run_id").references(Runs) { it.runId } 
} 

The biggest difference compared to Exposed is that we can directly reference the existing domain model classes in our table model objects. This makes for an explicit mapping between domain model and table objects, which you may or may not like if you want separate table objects.

An example of an insert and deletion query in Ktorm looks like:

val insertedExperimentId = database.sequenceOf(Experiments).insertAndGenerateKey { 
  it.hypothesis to experiment.hypothesis 
} 

experiment.runs.forEach { run -> 
  database.sequenceOf(ExperimentRuns).insert { 
    it.experimentId to insertedExperimentId 
    it.runId to database.sequenceOf(Runs).insertAndGenerateKey { 
      it.startDate to run.startDate 
      it.endDate to run.endDate 
      it.learning to run.learning 
    } 
  } 
} 

database.delete(ExperimentRuns) { 
  it.experimentId eq experimentId 
} 

database.delete(Experiments) { 
  it.id eq experimentId 
} 

As you can see, the way we construct a query is different from Exposed. In exposed we use either a DAO class or table object to construct a query from. In Ktorm we define our queries through the configured database class, that we can autowire through Spring.

After trying out Ktorm we concluded that:

  • The framework feels a lot like Exposed, it has the same domain model versus table model approach with a SQL DSL, based directly on JDBC. Because of this, the same setup considerations apply as Exposed (boilerplate, no out of the box repositories).
    Within the scope of our hackathon project, we did not find any major differences between the two frameworks. We can imagine that if you work with them for a longer period, you find differences in features.
  • Ktorm’s documentation feels more elaborate and better searchable than the documentation of Exposed.
  • At Github the framework is moderately popular compared to Hibernate, at least enough to consider it for usage in a production project. However, Ktorm is considerably less popular than Exposed. Within bol.com Ktorm is not used anywhere either, which makes switching to Ktorm less interesting for our use case.

JOOQ

The last framework we investigated during the hackathon was JOOQ (Java Object Oriented Querying). JOOQ has a bottom-up approach: it creates a type-safe query API based on an existing database, using a Maven or Gradle plugin.

Where JOOQ shined in our project was at the generation of what we like to call metadata constants. JOOQ looks at the existing database schema, created with Flyway migration scripts, and generates Kotlin classes filled with metadata about your tables and columns. An example of this is:

/** 
    * The column <code>public.experiment_runs.run_id</code>. 
 **/ 
val RUN_ID: TableField<ExperimentRunsRecord, Int?> =  
createField(DSL.name("run_id"),     SQLDataType.INTEGER.nullable(false), this, "") 

With JOOQ you do not have to write a separate table model, but instead can use the JOOQ generated constants to build your type-safe queries. An example:

from(RUN) 
.join(EXPERIMENT_RUNS) 
.on(EXPERIMENT_RUNS.RUN_ID.eq(RUN.RUN_ID)) 
.join(EXPERIMENT) 
.on(EXPERIMENT.EXPERIMENT_ID.eq(EXPERIMENT_RUNS.RUN_ID)) 
.where(RUN.END_DATE.eq(requestedEndDate) 

We are also able to give JOOQ our domain model and let it automatically figure out the mapping from the query into the domain model. JOOQ can do automatically match this based on existing JPA annotations in the domain model, the “best-matching constructor” or a custom mapper you provide yourself.

In our project we only used the metadata constants, while JOOQ has more to offer. JOOQ also generates DAO’s for instance. We investigated implementing the generated DAO’s in our project, but they included default methods that we considered not useful. For instance, a method was generated to look up experiments by (alphabetic) range of hypothesis. It seems this ‘select by range’ is generated for all fields and creates clutter.

Besides, the generated DAO code does not seem to take indexes into account. Most of the queries would lead to a full table scan if they were used, which can heavily impact performance. We see room for improvement here: JOOQ could use the indexes as an indicator of whether there is any use case for the code and leave a more concise DAO. This is one of the reasons we focussed our efforts on using the metadata constants.

Our conclusion about JOOQ:

  • JOOQ’s documentation is elaborate and makes the framework easy to use. Especially if you have an existing database schema or plan on using a database migration tool like Flyway.
  • The metadata constants can be a nice type-safe query implementation, based on your existing database. Because of this we were able to implement JOOQ with minimal boilerplate code regarding mappings to the data access layer.
  • JOOQ is actively maintained with monthly releases and is the most used ORM framework after Hibernate within bol.com.
  • One additional note is that JOOQ has multiple paid versions, that offer a wider range of supported database dialects and more features. Our database type, Postgres, among other common ones are supported in the open-source version. Popular databases like Oracle and SQL Server are only supported in the paid versions.

Noteworthy mention: Krush

The extra added boilerplate in mapping between the domain model and the table model with Exposed and Ktorm encouraged us to look for an alternative, onto which we encountered Krush.

Krush is based on Exposed and claims to be “a lightweight persistence layer for Kotlin based on Exposed SQL DSL.”. It removes the need for boilerplate mappings by adding back JPA annotations to the domain model, which we are used to from Hibernate.

Unfortunately, there is no usage of this framework within bol.com and on GitHub the community also seems too small to consider for usage in production. Because of this, we concluded that we would not go to the extent of testing its behaviour. Instead, we will give Krush a close look from time to time to see how it develops.

Noteworthy mention: Spring JDBC

You might not need all the complexity that Hibernate/JPA has to offer. Switching to a different ORM framework altogether can be heavy as well. What if there would just be a simpler alternative in the ecosystem you are already using? One such alternative is available in all Spring projects: Spring JDBC!

Spring JDBC will offer you a more low-level approach, based on JDBC directly. This can be a good approach for smaller projects that want to write native queries.

Conclusion

Joining forces in the bol.com hackathon to investigate Hibernate alternatives in Kotlin was fun and we learned a lot about the available alternatives out there. Our biggest learning is that there are four major ways of approaching the ORM world:

  • Database schema first, the approach that JOOQ takes.
  • SQL DSL first, the approach that Exposed and KTORM take.
  • JPA annotations first, the approach that Hibernate and Krush take.
  • Low level, the approach that Spring JDBC takes.

All these approaches come with their own set of advantages and disadvantages. For our use case JOOQ could be an alternative to Hibernate in our Kotlin projects. JOOQ would allow us to switch ORM frameworks with minimal changes and maximum type-safety, while keeping boilerplate at a minimum. The community and usage also seem good enough to adopt the framework for usage within a production environment.

It is important to note that doing a migration from one ORM framework to another is a heavy process that needs dedicated time to make it work, including performance tests. Hibernate can be a valid ORM framework choice in a project. We hope that you are now more aware of some of the other frameworks you can choose from and how they work.

1 During the hackathon the project team also reserved a small amount of time to investigate alternatives for Hibernate Envers. Using a different ORM framework then Hibernate can pose a challenge when you still want to have such out of the box auditing available, as Hibernate Envers can only be used in combination with Hibernate itself. The conclusion of the small investigation was that Javers promised to be a suitable alternative, although this framework seems only maintained by one person. Alternatively, you could use a more low-level approach by using database triggers that audit and log changes.

2 A while ago Sander spent hours trying to debug problems that were related to using data classes with Hibernate, which in the end led him to the listed article and repository. An example of such a problem is that the application tried to delete an object from the database, but through Hibernates magic under the hood the object was recreated after the deletion in the same transaction, resulting in no object being deleted. Using the best practices from the listed article led to consistent results.

Sander Boumeester

All articles by me

Dimar van Rietschoten

All articles by me

Aysegul Guldes

All articles by me