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.