Skip to content

QueryDSL Integration ​

The typed-value-querydsl module provides type-safe QueryDSL expressions for TypedValue fields.

Built with

  • QueryDSL 5.1.x

Installation ​

kotlin
dependencies {
    implementation("com.ekino.oss:typed-value-core:1.5.1")
    implementation("com.ekino.oss:typed-value-hibernate:1.5.1")
    implementation("com.ekino.oss:typed-value-querydsl:1.5.1")

    // QueryDSL dependencies
    implementation("com.querydsl:querydsl-jpa:5.0.0:jakarta")
    kapt("com.querydsl:querydsl-apt:5.0.0:jakarta")
}

The typed-value-querydsl module provides:

  • TypedValueExpression for type-safe QueryDSL expressions
  • Q-classes for the abstract entity base classes (QAbstractIntEntity, QAbstractLongEntity, QAbstractUuidEntity, QAbstractStringEntity), allowing QueryDSL's kapt to generate Q-classes for your entities

TypedValueExpression Class ​

Wraps a QueryDSL path to create type-safe expressions for TypedValue fields.

kotlin
class TypedValueExpression<ID : Comparable<ID>, T : Any, E : TypedValue<ID, T>> : FactoryExpression<E>

The third type parameter E allows using custom TypedValue subclasses (e.g., TypedId<User>) instead of just the base TypedValue<ID, T> type.

Methods ​

MethodSignatureDescription
eq()eq(right: TypedValue<ID, T>): BooleanExpressionEquality predicate
ne()ne(right: TypedValue<ID, T>): BooleanExpressionInequality predicate
isIn()isIn(collection: Collection<S>): BooleanExpressionIN clause
notIn()notIn(collection: Collection<S>): BooleanExpressionNOT IN clause
isNull()isNull(): BooleanExpressionNULL check
isNotNull()isNotNull(): BooleanExpressionNOT NULL check
path()path(): Expression<*>Access underlying QueryDSL path

Creating TypedValueExpression ​

Use the extension function on EntityPathBase:

kotlin
inline fun <ID : Comparable<ID>, T : Any, E : EntityPathBase<T>>
    E.typedValueExpressionOf(
        pathSelector: Function<E, Path<ID>>
    ): TypedValueExpression<ID, T>

With Hibernate Abstract Entities ​

When your entity extends one of the abstract entity classes, use the _id field:

kotlin
@Entity
class Product : AbstractLongEntity<Product>(Product::class) {
    var name: String? = null
}

// In your repository/service
val qProduct = QProduct.product
val productIdExpr = qProduct.typedValueExpressionOf { it._id }

With Custom Entities ​

For custom entities with TypedValue IDs:

kotlin
@Entity
class User {
    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private var _id: UUID? = null

    @get:Transient
    var id: TypedUuid<User>?
        get() = _id?.toTypedUuid()
        set(value) { _id = value?.value }

    var name: String? = null
}

// In your repository/service
val qUser = QUser.user
val userIdExpr = qUser.typedValueExpressionOf { it._id }

Custom TypedValue Types ​

Constructor-Based Approach

QueryDSL uses an explicit constructor-based pattern for custom types, providing maximum flexibility at the query level.

Overview ​

When you create custom TypedValue subclasses (e.g., TypedId extending TypedString), you can specify how to construct them from query results using an explicit constructor parameter.

Use Case ​

kotlin
// Custom TypedValue type
open class TypedId<T : Any>(id: String, type: KClass<T>) : TypedString<T>(id, type)

// Generic TypedValue (default):
val genericExpr = qUser.typedValueExpressionOf { it._id }
// Results: TypedValue<String, User>

// Custom TypedValue (with constructor):
val customExpr = qUser._id.typedValueExpressionOf { id -> TypedId(id, User::class) }
// Results: TypedId<User> ✓

Factory Methods ​

Method 1: Custom Types with Constructor (New!)

kotlin
inline fun <reified V : TypedValue<ID, T>, ID : Comparable<ID>, T : Any>
    Path<ID>.typedValueExpressionOf(
        noinline constructor: (value: ID) -> V
    ): TypedValueExpression<ID, T, V>

Use this when you want custom TypedValue instances in query results. The inline reified generic parameter ensures the correct runtime type is captured.

Method 1b: Custom Types (Non-inline, Java interop)

kotlin
@JvmStatic
fun <ID : Comparable<ID>, T : Any, V : TypedValue<ID, T>> typedValueExpressionOf(
    typedValueClass: Class<out V>,
    path: Path<ID>,
    constructor: Function<ID, V>
): TypedValueExpression<ID, T, V>

Use this from Java or when you need to pass the class explicitly. See Java Interop section for usage.

Method 2: Generic TypedValue (Existing)

kotlin
inline fun <ID : Comparable<ID>, T : Any, E : EntityPathBase<T>>
    E.typedValueExpressionOf(
        pathSelector: Function<E, Path<ID>>
    ): TypedValueExpression<ID, T, TypedValue<ID, T>>

Use this for automatic TypedValue construction (base type).

Basic Usage ​

Define Custom Type:

kotlin
open class TypedId<T : Any>(id: String, type: KClass<T>) : TypedString<T>(id, type)

@Entity
class User : AbstractStringEntity<User>(User::class) {
    var name: String? = null
}

Create Expression with Custom Constructor:

kotlin
val qUser = QUser.user

// Method 1: Custom type with explicit constructor
// Note: Must specify the type parameter explicitly for custom types
val customIdExpr = qUser._id.typedValueExpressionOf<TypedId<User>> { id ->
    TypedId(id, User::class)
}

// Method 2: Generic TypedValue (comparison)
val genericIdExpr = qUser.typedValueExpressionOf { it._id }

Type Parameter Required

When using custom TypedValue subclasses, you must explicitly specify the type parameter (e.g., <TypedId<User>>). This ensures QueryDSL's Projections.constructor() receives the correct runtime class.

Use in Queries:

kotlin
val userId = TypedId("user-123", User::class)

// Query with custom type
val user = queryFactory.selectFrom(qUser)
    .where(customIdExpr.eq(userId))
    .fetchOne()

// Select the ID itself
val ids: List<TypedId<User>> = queryFactory
    .select(customIdExpr)
    .from(qUser)
    .fetch()  // Returns List<TypedId<User>>, not List<TypedString<User>>

Complete Example ​

kotlin
// Custom types
open class TypedId<T : Any>(id: String, type: KClass<T>) : TypedString<T>(id, type)
open class TypedCode<T : Any>(code: String, type: KClass<T>) : TypedString<T>(code, type)

@Entity
class Product : AbstractLongEntity<Product>(Product::class) {
    var name: String? = null
    var categoryCode: String? = null
}

@Repository
class ProductQueryRepository(private val queryFactory: JPAQueryFactory) {
    private val qProduct = QProduct.product

    // Create expressions with custom constructors
    // Note: Explicit type parameters required for custom types
    private val productIdExpr = qProduct._id.typedValueExpressionOf<TypedId<Product>> { id ->
        TypedId(id.toString(), Product::class)
    }

    private val categoryCodeExpr = qProduct.categoryCode.typedValueExpressionOf<TypedCode<Product>> { code ->
        TypedCode(code, Product::class)
    }

    fun findByCustomId(id: TypedId<Product>): Product? {
        return queryFactory.selectFrom(qProduct)
            .where(productIdExpr.eq(id))
            .fetchOne()
    }

    fun findByCustomIds(ids: List<TypedId<Product>>): List<Product> {
        return queryFactory.selectFrom(qProduct)
            .where(productIdExpr.isIn(ids))
            .fetch()
    }

    fun findByCategoryCode(code: TypedCode<Product>): List<Product> {
        return queryFactory.selectFrom(qProduct)
            .where(categoryCodeExpr.eq(code))
            .fetch()
    }

    // Select IDs directly (returns custom types)
    fun getAllProductIds(): List<TypedId<Product>> {
        return queryFactory
            .select(productIdExpr)
            .from(qProduct)
            .fetch()  // Returns List<TypedId<Product>>
    }
}

Multiple Custom Types ​

Use different constructors for different purposes:

kotlin
// Different custom types for different use cases
open class TypedId<T : Any>(id: String, type: KClass<T>) : TypedString<T>(id, type)
open class ExternalId<T : Any>(id: String, type: KClass<T>) : TypedString<T>(id, type)

val qUser = QUser.user

// Different expressions for different fields
val internalIdExpr = qUser._id.typedValueExpressionOf<TypedId<User>> { id ->
    TypedId(id, User::class)
}

val externalIdExpr = qUser.externalId.typedValueExpressionOf<ExternalId<User>> { id ->
    ExternalId(id, User::class)
}

// Use in same query
val users = queryFactory.selectFrom(qUser)
    .where(
        internalIdExpr.isIn(internalIds)
            .and(externalIdExpr.isIn(externalIds))
    )
    .fetch()

Java Interop ​

When using custom TypedValue types from Java, you'll encounter unchecked cast warnings due to Java's type erasure. This is expected and safe.

Java Example:

java
import static com.ekino.oss.typedvalue.querydsl.TypedValueExpression.typedValueExpressionOf;

@Repository
public class ProductRepository {
    private final JPAQueryFactory queryFactory;
    private final QProduct qProduct = QProduct.product;

    @SuppressWarnings("unchecked")  // Safe: types are verified at compile time
    private TypedValueExpression<String, Product, TypedId<Product>> createExpression() {
        return typedValueExpressionOf(
            TypedId.class,
            qProduct._id,
            value -> new TypedId<>(value, ProductKt.getProductClass())
        );
    }

    public Product findById(TypedId<Product> id) {
        TypedValueExpression<String, Product, TypedId<Product>> expr = createExpression();
        return queryFactory.selectFrom(qProduct)
            .where(expr.eq(id))
            .fetchOne();
    }
}

Java Limitation

The @SuppressWarnings("unchecked") annotation is necessary because:

  • Java's TypedId.class is Class<TypedId> (raw type), not Class<TypedId<Product>>
  • Generic type parameters don't exist at runtime in Java (type erasure)
  • This is standard Java practice when working with generics and reflection

The suppression is safe because the compiler verifies the constructor returns the correct type.

Comparison: Constructor vs Generic ​

AspectCustom ConstructorGeneric TypedValue
Syntaxpath.typedValueExpressionOf<Custom<T>> { id -> Custom(...) }entity.typedValueExpressionOf { it._id }
Result TypeCustom type (e.g., TypedId<User>)TypedValue<ID, T>
Use CaseDomain-specific typesGeneral purpose
FlexibilityDifferent per expressionConsistent across queries
ConfigurationExplicit type parameter requiredNone needed
Type SafetyCompile-time verified with reified genericsCompile-time verified
Java InteropRequires @SuppressWarnings("unchecked")Works without warnings

When to Use Each Approach ​

Use Custom Constructor When:

  • You have domain-specific ID types (TypedId, ExternalId, etc.)
  • You want type information in query results
  • You need different behaviors for different ID types
  • You're building a rich domain model

Use Generic TypedValue When:

  • Simple use cases without custom types
  • Prototyping or exploration
  • No need for specialized ID behavior
  • Consistent handling across all queries

Design Rationale ​

QueryDSL uses a constructor-based pattern rather than registration (like Elasticsearch) because:

  1. Query Flexibility: Different queries can use different constructors
  2. No Configuration: No Spring beans or initialization required
  3. Explicit Dependencies: Constructor is visible at call site
  4. Type Safety: Full compile-time verification
  5. Zero Overhead: No lookups or reflection

This pattern fits QueryDSL's fluent API philosophy perfectly.

Projection with Custom Types ​

kotlin
// Custom type for projections
data class UserSummary(
    val id: TypedId<User>,
    val name: String
)

val customIdExpr = qUser._id.typedValueExpressionOf<TypedId<User>> { id ->
    TypedId(id, User::class)
}

val summaries = queryFactory
    .select(
        Projections.constructor(
            UserSummary::class.java,
            customIdExpr,
            qUser.name
        )
    )
    .from(qUser)
    .fetch()  // Returns List<UserSummary> with TypedId fields

Testing Custom Types ​

kotlin
@Test
fun `should work with custom TypedValue types`() {
    val customExpr = qUser._id.typedValueExpressionOf<TypedId<User>> { id ->
        TypedId(id, User::class)
    }

    val userId = TypedId("test-123", User::class)

    val user = queryFactory.selectFrom(qUser)
        .where(customExpr.eq(userId))
        .fetchOne()

    // Result has custom type
    val retrievedId: TypedId<User> = queryFactory
        .select(customExpr)
        .from(qUser)
        .where(customExpr.eq(userId))
        .fetchOne()!!

    assertThat(retrievedId).isInstanceOf<TypedId<User>>()
}

Path Type Mapping ​

The expression creates appropriate path types based on the ID type:

ID TypeQueryDSL Path Type
StringStringPath
LongNumberPath<Long>
IntNumberPath<Integer>
UUIDComparablePath<UUID>
Other ComparableComparableExpressionBase<*>

Predicates ​

Equality (eq) ​

kotlin
val productId = 42L.toTypedLong<Product>()

val product = queryFactory.selectFrom(qProduct)
    .where(productIdExpr.eq(productId))
    .fetchOne()

Generated: product._id = 42

Inequality (ne) ​

kotlin
val products = queryFactory.selectFrom(qProduct)
    .where(productIdExpr.ne(excludedId))
    .fetch()

Generated: product._id != 123

IN Clause (isIn) ​

kotlin
val productIds = listOf(
    1L.toTypedLong<Product>(),
    2L.toTypedLong<Product>(),
    3L.toTypedLong<Product>()
)

val products = queryFactory.selectFrom(qProduct)
    .where(productIdExpr.isIn(productIds))
    .fetch()

Generated: product._id IN (1, 2, 3)

NOT IN Clause (notIn) ​

kotlin
val products = queryFactory.selectFrom(qProduct)
    .where(productIdExpr.notIn(excludedIds))
    .fetch()

Generated: product._id NOT IN (1, 2)

NULL Checks ​

kotlin
// Find products without ID (new/unsaved)
val newProducts = queryFactory.selectFrom(qProduct)
    .where(productIdExpr.isNull())
    .fetch()

// Find products with ID (persisted)
val savedProducts = queryFactory.selectFrom(qProduct)
    .where(productIdExpr.isNotNull())
    .fetch()

Complex Queries ​

Combine with other predicates:

kotlin
val products = queryFactory.selectFrom(qProduct)
    .where(
        productIdExpr.isIn(productIds)
            .and(qProduct.status.eq(ProductStatus.ACTIVE))
            .and(qProduct.createdAt.after(startDate))
    )
    .orderBy(qProduct.name.asc())
    .fetch()

Complete Example ​

kotlin
@Repository
class ProductQueryRepository(
    private val queryFactory: JPAQueryFactory
) {
    private val qProduct = QProduct.product
    private val productIdExpr = qProduct.typedValueExpressionOf { it._id }

    fun findById(id: TypedLong<Product>): Product? {
        return queryFactory.selectFrom(qProduct)
            .where(productIdExpr.eq(id))
            .fetchOne()
    }

    fun findByIds(ids: List<TypedLong<Product>>): List<Product> {
        return queryFactory.selectFrom(qProduct)
            .where(productIdExpr.isIn(ids))
            .fetch()
    }

    fun findActiveProductsExcluding(excludedIds: List<TypedLong<Product>>): List<Product> {
        return queryFactory.selectFrom(qProduct)
            .where(
                productIdExpr.notIn(excludedIds)
                    .and(qProduct.status.eq(ProductStatus.ACTIVE))
            )
            .orderBy(qProduct.name.asc())
            .fetch()
    }

    fun findByCategory(categoryId: TypedInt<Category>): List<Product> {
        val categoryIdExpr = qProduct.typedValueExpressionOf { it.categoryId }
        return queryFactory.selectFrom(qProduct)
            .where(categoryIdExpr.eq(categoryId))
            .fetch()
    }
}

Working with All Entity Types ​

IntPerson (AbstractIntEntity) ​

kotlin
val qIntPerson = QIntPerson.intPerson
val intPersonIdExpr = qIntPerson.typedValueExpressionOf { it._id }

val person = queryFactory.selectFrom(qIntPerson)
    .where(intPersonIdExpr.eq(personId))
    .fetchOne()

LongPerson (AbstractLongEntity) ​

kotlin
val qLongPerson = QLongPerson.longPerson
val longPersonIdExpr = qLongPerson.typedValueExpressionOf { it._id }

val persons = queryFactory.selectFrom(qLongPerson)
    .where(longPersonIdExpr.isIn(personIds))
    .fetch()

UuidPerson (AbstractUuidEntity) ​

kotlin
val qUuidPerson = QUuidPerson.uuidPerson
val uuidPersonIdExpr = qUuidPerson.typedValueExpressionOf { it._id }

val person = queryFactory.selectFrom(qUuidPerson)
    .where(uuidPersonIdExpr.eq(personId))
    .fetchOne()

StringPerson (AbstractStringEntity) ​

kotlin
val qStringPerson = QStringPerson.stringPerson
val stringPersonIdExpr = qStringPerson.typedValueExpressionOf { it._id }

val persons = queryFactory.selectFrom(qStringPerson)
    .where(stringPersonIdExpr.isNotNull())
    .fetch()

API Summary ​

MethodDescription
typedValueExpressionOf()Create TypedValueExpression from EntityPath
eq(value)Equality predicate
ne(value)Inequality predicate
isIn(collection)IN clause
notIn(collection)NOT IN clause
isNull()NULL check
isNotNull()NOT NULL check
path()Access underlying QueryDSL path

Next Steps ​

Released under the MIT License.