Hello, my name is Seo Jong Hyon and I'm part of the Global EC (Global E-Commerce, GEC for short). The GEC mainly uses Spring as its server framework, and JPA as its ORM technology. We've so far used the query method provided by Spring for simple queries and a combination of JPA Specification and Criteria API for more complex queries, but both libraries have some inconveniences. We started work on developing a library that does not have these inconveniences, and this library was extended as an open-source project called Kotlin JDSL.
In this post, I'd like to go over how we came to develop Kotlin JDSL (JPA Domain Specific Language) and why it's more convenient for us compared to Criteria API.
The inconveniences of Criteria API
Criteria queries are type-safe queries that can be dynamically written using Java objects. When we use JPA, we write dynamic and complex queries as Criteria queries. Criteria queries can be written with Criteria API, but there are a few inconveniences when using it.
Metamodel
In order to use Criteria API safely, we used a library called Hibernate Jpamodelgen. Hibernate Jpamodelgen is an annotation processor that makes using Criteria API easier by creating an entity metamodel by scanning entity classes. While Hibernate Jpamodelgen excels in using Criteria API safely due to how it allows writing entity field names and relations in code instead of text strings, there are a number of issues that arise from it being based on annotation processors.
First, you must compile every time there's a change to an entity variable name, so that it can be reflected on the metamodel. There are many instances while developing locally where you might modify entities by changing the field name so that they are more representative of their domains. When you do this, you always have to recompile the metamodel for the entity change to go through. However, the code for the metamodel is not in our control and it's easy to forget the relation between a certain entity and the metamodel. This led to many errors during testing, and many modifications to the code as well.
Second, it's difficult to determine the reason behind a compile error. The metamodel is an annotation processor, thus an error occurs if a Gradle task fails to create the metamodel. As already mentioned, the code for the metamodel is not under our control and this makes it difficult to determine what may have caused the error. Annotation processors typically do not tell you what may have caused an error. Because of this, even when we knew an error was caused by an incorrect entity mapping, we had to debug our code by guessing the actual cause, modifying the code, and then hoping it compiles without errors this time.
Lastly, you cannot remove warning logs from the metamodel. Hibernate Jpamodelgen detects lists and sets that have collection fields as relations even if there is a converter. We had to use a converter on the collection field to save it in JSON format or as a text string, and this left a warning on our logs. Since we couldn't remove these warnings from the log, we had to ignore them while we analyzed the logs.
All of the reasons above were obstacles that gave us a lot of trouble when using the metamodel.
CriteriaBuilder
CriteriaBuilder is an interface that provides help when creating JPQL queries. You can use CriteriaBuilder to create queries, expressions, predicates, and projections. However, one downside to CriteriaBuilder is that your source code may become bloated due to how you can only create query elements through the builder.
Below is a sample query that looks up all the orders made by customers during the year 2021.
select distinct *
from order
where purchaser_id in (?, ?, ?)
and ordered_at >= '2021-01-01'
and ordered_at <= '2021-12-31'
If you write the above code for use with Criteria API, the result is as follows.
val criteriaBuilder = entityManager.criteriaBuilder
val criteriaQuery = criteriaBuilder.createQuery(Order::class.java)
val root = criteriaQuery.from(Order::class.java)
val inClause: CriteriaBuilder.In<String> = criteriaBuilder.`in`(root.get("purchaserId"))
for (purchaserId in purchaserIds) {
inClause.value(purchaserId)
}
val greaterThanOrEqualToClause = criteriaBuilder.greaterThanOrEqualTo(root.get("orderedAt"), "2021-01-01")
val lessThanOrEqualToClause = criteriaBuilder.lessThanOrEqualTo(root.get("orderedAt"), "2021-12-31")
criteriaQuery.select(root)
.distinct(true)
.where(inClause, lessThanOrEqualToClause, greaterThanOrEqualToClause)
val query: TypedQuery<Order> = entityManager.createQuery(criteriaQuery)
return query.getResultList()
As you can see, using CriterieaBuilder requires you to write much more code than what was required for an SQL query. The bloated code also negatively impacts readability, making it difficult to perform code reviews as it's not easy to see what the query is at a glance.
Developing Kotlin JDSL
As mentioned above, there were some downsides to using Criteria API. We looked into several libraries for a better solution, but we were unable to find one that quite satisfied all of our needs. Many people use QueryDSL as an alternative to Criteria API, but QueryDSL also requires a metamodel and thus didn't fit our requirements. It was then when we discovered that Kotlin supports Java property references alongside method references, which gave us the idea to develop a wrapper library for Criteria API using property references. While we were still designing our library's interface, we learned that not only does Kotlin make creating DSLs easy, it fully encourages DSL creation even in their official documentation. This led to our decision to create our interface as a DSL. After everything was decided, we began our development on our own library that would provide KProperty and DSL API.
KProperty
When working with Kotlin, you can acquire the property reference KProperty object by writing Order::purchaserId. The initial idea was to use the KProperty object to write safe code that circumvents compiling errors without using metamodels, as the KProperty object includes field names. One concern we had was; since KProperty is a reference object, it might use reflection. However, on a closer look we found that the code that acquires names from the KProperty object doesn't use reflection. Let's take a look at an example.
For example, if the class Book contains the fields name and author, we will be referencing KProperty in the Kotlin code with Book::name and Book::author respectively.
data class Book(
val name: String,
val author: String,
)
fun kPropertyTest() {
println(Book::name.name)
println(Book::author.name)
}
The Kotlin Compiler will generate a Java class like the one below by compiling your Kotlin code. If you look at the classes, you can see that the names of each field are used as parameters in the parent constructors.
final class ClassKt$books$1 extends PropertyReference1Impl {
public static final KProperty1 INSTANCE = new ClassKt$books$1();
books$1() {
super(Book.class, "name", "getName()Ljava/lang/String;", 0);
}
@Nullable
public Object get(@Nullable Object receiver) {
return ((Book) receiver).getName();
}
}
final class ClassKt$books$2 extends PropertyReference1Impl {
public static final KProperty1 INSTANCE = new ClassKt$books$2();
ClassKt$books$2() {
super(Book.class, "author", "getAuthor()Ljava/lang/String;", 0);
}
@Nullable
public Object get(@Nullable Object receiver) {
return ((Book) receiver).getAuthor();
}
}
As you can see, the Kotlin compiler injects the name of the KProperty object when compiling the Java file. We were able to verify that we could reference entities without any impact to performance since we were not using reflection.
Comparing Criteria API to Kotlin JDSL
Let's compare Kotlin JDSL and Criteria API to see how much of an improvement was made to readability. The code below is a Kotlin JDSL query for looking up info on all orders made by a selected group of users in the year 2021. At a glance, there's not much difference from the actual SQL query.
queryFactory.listQuery<Order> {
selectDistinct(entity(Order::class))
from(Order::class)
where(and(
col(Order::purchaserId).`in`(purchaserIds),
col(Order::purchaserId).greaterThanOrEqualTo('2021-01-01'),
col(Order::purchaserId).lessThanOrEqualTo('2021-12-31'),
))
}
select distinct *
from order
where purchaser_id in (?, ?, ?)
and ordered_at >= '2021-01-01'
and ordered_at <= '2021-12-31'
We have since used Kotlin JDSL to write all of our queries. The increased readability lead to less errors and made it easier to catch mistakes during the code review process. In addition, there is DSL API support that allows autocompletion of code like the image below. Autocompletion makes it easier for even first time users to get used to it in no time.

Using Kotlin JDSL, you can easily write Join queries in projections or entities that are not related as well.
Conclusion
At first our goal was to create a library that only we would use internally, but many of our team members suggested that we should publicly release the library as an open-source project. That is how we came to release our library to the public and write this post about it. Our team has set up an environment where we can use Kotlin JDSL to easily create queries, and find errors during code reviews. We are very satisfied with the performance in our daily work. The Kotlin JDSL library supports many more Criteria API queries not mentioned in this post, such as Update, Delete, and Subquery. If you're currently using Criteria API, but unhappy with how bloated your code has become, we highly recommend giving Kotlin JDSL a try.
In this post, we looked at how you can use Kotlin JDSL to easily write synchronous queries. Please look forward to our next post, where we will explain how you can use Kotlin JDSL with Hibernate Reactive: a library that can be used to write asynchronous JPA queries.