Announcing Krush – idiomatic persistence layer for Kotlin, based on Exposed

We’ve released a persistence library for Kotlin, you can find it on our Github. It’s a JPA-to-Exposed SQL DSL generator.

TL;DR

We’ve released a persistence library for Kotlin, you can find it on our Github. It’s a JPA-to-Exposed SQL DSL generator.

The state of persistence in Kotlin

One of the key decisions that helped Kotlin gain massive popularity was to reuse Java ecosystem instead of inventing it’s own. This means that you can safely use Kotlin as a primary language for a project developed using any popular Java stack like Spring Boot and built with Java build tool like Maven. What this also means is that natural choice for persistence layer in Kotlin is Spring Data with JPA 3 with Hibernate as an implementation.

However, JPA, which highly relies on mutable objects and dirty checking, may not look like pure Kotlin, which tries to embrace functional programming and immutability. The official Spring JPA guide for Kotlin uses mutable classes and properties which is not really idiomatic for Kotlin where you want to use immutable data classes whenever it’s possible.

There are some other options, which can be used safely with Kotlin and data classes, like Spring Data JDBC — interesting approach based on pure JDBC, embracing DDD and aggregate root concepts or Micronaut Data JDBC — if you’re not tied to Spring ecosystem. But they’re both relatively new, not mature yet and miss another idiomatic Kotlin feature — a DSL for making SQL queries.

DSL for SQL queries

Another thing that made Kotlin really powerful and popular is its ability to construct Domain Specific Languages using features like property reference, operators, infix and extension functions. For example, for Android development there is excellent anko library for constructing complex view layouts for Android apps. In Spring/JPA the default approach to SQL queries are query methods, where you use special naming convention of methods in repository interfaces. The method names are parsed at runtime to provide required SQL queries and mapping. The naming convention is supported by IntelliJ Idea and other IDEs and works well in simple cases, but may be not flexible enough when you want complex queries e.g. with some conditions based on dynamic filters. If you want to use a true, type-safe, composable and idiomatic Kotlin SQL DSL, you can try to use other libraries, like Requery or Exposed.

Requery

Requery is a lightweight persistence library for Java and Kotlin with RxJava and Java 8 streams support. It uses annotations (both custom and JPA) to process your entities and generate some infrastructure code called “model”.

So given a Book interface:

@Entity
@Table(name = "books")

interface Book : Persistable {
    @get:Key @get:Generated
    val id: Long

    val isbn: String
    val author: String
    val title: String

    val publishDate: LocalDate
}

 

You can instantiate and persist it by using generated BookEntity class:

//given
val book = BookEntity().apply {
    setIsbn("1449373321")
    setPublishDate(LocalDate.of(2017, Month.APRIL, 11))
    setTitle("Designing Data-Intensive Applications")
    setAutor("Martin Kleppmann")
}

// when
val persistedBook = dataStore.insert(book)

And the use SQL DSL to fetch data and map the results back to entities:

// then
val books = dataStore.select(Book::class).where(Book::id eq  book.id).get().toList()

assertThat(books).containsExactly(persistedBook)

This was really close to our needs! We like the idea of having annotations on the entities combined with the rich SQL DSL. Also the RxJava bindings and lazy Kotlin sequences support looks promising. On the other side, there are few minor issues related to immutable classes support:

  • immutable interface approach needs to be backed up with this generated, mutablexxxEntity class
  • there are some restrictions: e.g. you cannot use them to map relations to other entities (just foreign keys by ids)
  • @Generated also doesn’t work for ids in data classes.

You can check example project using Requery in requery branch of krush-example project on GitHub.

Exposed

Another approach which given you rich SQL DSL support is Exposed — a Kotlin-only persistence layer maintained by the JetBrains team. It comes in two flavors: active-record DAO and lightweight SQL DSL. As we are not the fans of active records, we tried the SQL DSL flavor. It works by creating additional mapping code using Kotlin objects and extension functions:

object  BookTable : Table("books") {
    val id: Column<Long> = long("id").promaryKey().autoIncrement()
    val isbn: Column<String> = varchar("isbn". 255)
    val autor: Column<String> = varchar("author". 255)
    val title: Column<String> = varchar("title". 255)
    val publishDate: Column<LocalDate> = date("publishDate")
}

Then you can refer to these Column properties to create type-safe queries and map results using Kotlin collections API:

val titles: List<String> = BookTable
        .select { BookTable.author like "Martin K%" }
        .map { it[BookTable.title] }

As you can see, Exposed is not a full-blown ORM — there is no direct mapping to/from your domain classes into these Table objects, but it’s not hard to write simple mapping functions for that. You can also benefit from Kotlin null-types support and write bindings for your own types by using Kotlin’s extension functions. We wrote some time ago this article about our approach to using Exposed in our projects.

Krush

We really like the Kotlin-first feeling combined with great flexibility of Exposed, but at some time we were tired of writing these table mappings manually. We thought that it would be nice to generate them from JPA-compatible annotations, in similar way it’s done in Requery. This ended with building a library called Krush, which we’re announcing today ;)

Krush consist of two components:

  • annotation-processor which generates Exposed mappings by reading (a subset of) standard JPA annotations found on entity classes
  • utility functions for persisting entities and mapping from/to Exposed objects

So given this entity:

@Entity
@Table(name = "books")

data class Book(
    @Id @GeneratedValue
    val id: Long? = null,

    val isbn: String,
    val author: String,
    val title: String,
    val publishDate: LocalDate
)

Krush will generate BookTable object which allows to persist it like this:

//given
val book = Book(
        isbn = "1449373321", publishDate = LocalDate.of(2017, Month.APRIL, 11),
        title = "Designing Data-Intensive Applications", author = "Martin Kleppmann"
)
val persistedBook = BookTable.insert(book)
assertThat(persistedBook.id).isNotNull()

And write queries using type-safe DSL just like you were using plain Exposed:

val bookId = book.id ?: throw IllegalargumentException( )
val fetchedBook = BookTable.select { BookTable.id eq bookId }.singleOrNull()?.toBook( )
assertThat(fetchedBook).isEqualTo(book)

val selectedBooks = BookTable
        .select { BookTable.author like "Martin Kx" }
        .toBookList()

assertThat(selectedBooks).containsOnly(book)

That’s it! You can find more details and supported features in the README of Krush repository or in some example projects.

Enjoy! Looking for feedback from the community!

You May Also Like

JBoss Envers and Spring transaction managers

I've stumbled upon a bug with my configuration for JBoss Envers today, despite having integration tests all over the application. I have to admit, it casted a dark shadow of doubt about the value of all the tests for a moment. I've been practicing TDD since 2005, and frankly speaking, I should have been smarter than that.

My fault was simple. I've started using Envers the right way, with exploratory tests and a prototype. Then I've deleted the prototype and created some integration tests using in-memory H2 that looked more or less like this example:

@Test
public void savingAndUpdatingPersonShouldCreateTwoHistoricalVersions() {
    //given
    Person person = createAndSavePerson();
    String oldFirstName = person.getFirstName();
    String newFirstName = oldFirstName + "NEW";

    //when
    updatePersonWithNewName(person, newFirstName);

    //then
    verifyTwoHistoricalVersionsWereSaved(oldFirstName, newFirstName);
}

private Person createAndSavePerson() {
    Transaction transaction = session.beginTransaction();
    Person person = PersonFactory.createPerson();
    session.save(person);
    transaction.commit();
    return person;
}    

private void updatePersonWithNewName(Person person, String newName) {
    Transaction transaction = session.beginTransaction();
    person.setFirstName(newName);
    session.update(person);
    transaction.commit();
}

private void verifyTwoHistoricalVersionsWereSaved(String oldFirstName, String newFirstName) {
    List<Object[]> personRevisions = getPersonRevisions();
    assertEquals(2, personRevisions.size());
    assertEquals(oldFirstName, ((Person)personRevisions.get(0)[0]).getFirstName());
    assertEquals(newFirstName, ((Person)personRevisions.get(1)[0]).getFirstName());
}

private List<Object[]> getPersonRevisions() {
    Transaction transaction = session.beginTransaction();
    AuditReader auditReader = AuditReaderFactory.get(session);
    List<Object[]> personRevisions = auditReader.createQuery()
            .forRevisionsOfEntity(Person.class, false, true)
            .getResultList();
    transaction.commit();
    return personRevisions;
}

Because Envers inserts audit data when the transaction is commited (in a new temporary session), I thought I have to create and commit the transaction manually. And that is true to some point.

My fault was that I didn't have an end-to-end integration/acceptance test, that would call to entry point of the application (in this case a service which is called by GWT via RPC), because then I'd notice, that the Spring @Transactional annotation, and calling transaction.commit() are two, very different things.

Spring @Transactional annotation will use a transaction manager configured for the application. Envers on the other hand is used by subscribing a listener to hibernate's SessionFactory like this:

<bean id="sessionFactory" class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean" >        
...
 <property name="eventListeners">
     <map key-type="java.lang.String" value-type="org.hibernate.event.EventListeners">
         <entry key="post-insert" value-ref="auditEventListener"/>
         <entry key="post-update" value-ref="auditEventListener"/>
         <entry key="post-delete" value-ref="auditEventListener"/>
         <entry key="pre-collection-update" value-ref="auditEventListener"/>
         <entry key="pre-collection-remove" value-ref="auditEventListener"/>
         <entry key="post-collection-recreate" value-ref="auditEventListener"/>
     </map>
 </property>
</bean>

<bean id="auditEventListener" class="org.hibernate.envers.event.AuditEventListener" />

Envers creates and collects something called AuditWorkUnits whenever you update/delete/insert audited entities, but audit tables are not populated until something calls AuditProcess.beforeCompletion, which makes sense. If you are using org.hibernate.transaction.JDBCTransaction manually, this is called on commit() when notifying all subscribed javax.transaction.Synchronization objects (and enver's AuditProcess is one of them).

The problem was, that I used a wrong transaction manager.

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager" >
    <property name="dataSource" ref="dataSource"/>
</bean>

This transaction manager doesn't know anything about hibernate and doesn't use org.hibernate.transaction.JDBCTransaction. While Synchronization is an interface from javax.transaction package, DataSourceTransactionManager doesn't use it (maybe because of simplicity, I didn't dig deep enough in org.springframework.jdbc.datasource), and thus Envers works fine except not pushing the data to the database.

Which is the whole point of using Envers.

Use right tools for the task, they say. The whole problem is solved by using a transaction manager that is well aware of hibernate underneath.

<bean id="transactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager" >
    <property name="sessionFactory" ref="sessionFactory"/>
</bean>

Lesson learned: always make sure your acceptance tests are testing the right thing. If there is a doubt about the value of your tests, you just don't have enough of them,