Using JPA
Specifications with Kotlin

topher-lamey

Topher Lamey|July 8, 2021

JPA Specifications make dynamic queries easy to work with. Check out this guide for a quick overview to working with Kotlin and JPA Specifications.

JPA Specifications make dynamic queries really easy to work with. The general idea is that you generate JPA Models for your Entity classes, and those models provide Entity metadata that make creating dynamic queries easy using Criteria and Paths. This lets you avoid building a bunch of findBy JPA methods and picking the right one based on the current data.

Note that this article isn't meant to be exhaustive on JPA Specifications, but rather a quick reference for working with Kotlin and JPA Specifications.

Bringing in The Dependencies

The JPA model generation works off Annotation processors to create Entity metadata as Java source files. In order to get that to work in Kotlin, we need to bring in the kotlin-kapt plugin. Then the jpamodelgen library will use kapt to process annotations and generate Java source files, one per Entity class, that provide metadata needed for Specifications.

plugins {
    kotlin("kapt") version "1.5.20"
}

dependencies {
    kapt("org.hibernate:hibernate-jpamodelgen:5.4.30.Final")
}

Entity

Here's a contrived example Entity class that we'll use for demonstration.

@Entity
class AuditRecord(
    @Id
    var id: UUID,
    var type: String,
    var userName: String,
    var userEmail: String,
    @CreationTimestamp
    @Temporal(TemporalType.TIMESTAMP)
    var createdDate: Date
)

Generated Model

Just to be complete, here's what the auto-generated Java metadata class looks like for that Entity. In gradle, they land in your build/generated/source/kapt/main folder and are automatically included as source for the project.

@Generated(value = "org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor")
@StaticMetamodel(AuditRecord.class)
public abstract class AuditRecord_ {

	public static volatile SingularAttribute<AuditRecord, Date> createdDate;
	public static volatile SingularAttribute<AuditRecord, String> userEmail;
	public static volatile SingularAttribute<AuditRecord, String> type;
	public static volatile SingularAttribute<AuditRecord, String> userName;
	public static volatile SingularAttribute<AuditRecord, UUID> userId;

	public static final String CREATED_DATE = "createdDate";
	public static final String USER_EMAIL = "userEmail";
	public static final String ID = "id";
	public static final String TYPE = "type";
	public static final String USER_NAME = "userName";

}

JPA Repository

To use Specifications, we need a repository to subclass JpaSpecificationExecutor. This provides a bunch of auto-generated JPA Repository methods (findOne, findAll, etc) that know how to deal with Specifications.

interface AuditRecordRepository : JpaSpecificationExecutor<AuditRecord>

The DAO

After all the setup, here's the usage of JPA Specifications in a DAO. The main idea is to wrap each column in a function that builds a Specification with a CriteriaBuilder and queries the Path for values, or returns null. Null simply indicates the column should just be dropped for the current query.

fun list(
    startDate: Date,
    endDate: Date,
    types: List<String>,
    userEmail: String,
    userName: String
): List<AuditRecord> {
    return auditRecordRepository.findAll(
        isInType(types)
            .and(
                isInDateRange(startDate, endDate)
                    .and(containsUserEmail(userEmail).or(containsUserName(userName)))
            )
    )
}

fun containsUserEmail(userEmail: String): Specification<AuditRecord> {
    return Specification<AuditRecord> { root, query, builder ->
        if (userEmail.isNotBlank()) {
            builder.like(builder.lower(root.get(AuditRecord_.userEmail)), "%${userEmail.toLowerCase()}%")
        } else {
            null
        }
    }
}

fun containsUserName(userName: String): Specification<AuditRecord> {
    return Specification<AuditRecord> { root, query, builder ->
        if (userName.isNotBlank()) {
            builder.like(builder.lower(root.get(AuditRecord_.userName)), "%${userName.toLowerCase()}%")
        } else {
            null
        }
    }
}

fun isInDateRange(startDate: Date, endDate: Date): Specification<AuditRecord> {
    return Specification<AuditRecord> { root, query, builder ->
        builder.between(root.get(AuditRecord_.createdDate), startDate, endDate)
    }
}

fun isInType(types: List<String>): Specification<AuditRecord> {
    return Specification<AuditRecord> { root, query, builder ->
        builder.and(root.get(AuditRecord_.type).`in`(types)) // NOTE: 'in' is a Kotlin keyword, so you have to escape it
    }
}

Logging

Once you have things running, you can verify the generated SQL in the logs by setting the spring.jpa.show-sql flag to true. It is very chatty, so make sure you switch it to false when you're done!

spring:
  jpa:
    show-sql: true

C'est La Fin

JPA Specifications make dynamic queries easy to build and use. They are a deep topic, but this article isn't meant to be exhaustive, just a quick reference for use with Kotlin.


Topher Lamey  |  July 8, 2021