StackHawk

Using JPA Specifications with Kotlin

Share on LinkedIn
Share on X
Share on Facebook
Share on Reddit
Send us an email
Topher Lamey Blog Image

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 thekotlin-kapt
plugin. Then thejpamodelgen
library will usekapt
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 yourbuild/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 subclassJpaSpecificationExecutor
. 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 thespring.jpa.show-sql
flag totrue
. It is very chatty, so make sure you switch it tofalse
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.

More Hawksome Posts

Business Logic Vulnerability Testing: Why Your Scanner Can’t Find What It Doesn’t Understand

Business Logic Vulnerability Testing: Why Your Scanner Can’t Find What It Doesn’t Understand

Not all security flaws live in broken code. Some, like business logic vulnerabilities, hide in plain sight—within the workflows that make your app function. In 2019, millions of travelers’ data was exposed when a booking system treated a six-character code as full authentication. The system worked exactly as designed, and that was the problem. As APIs power more of the world’s digital experiences, protecting against these logic-based flaws requires context, creativity, and collaboration—because scanners can’t secure what they don’t understand.