JPA integration

Dynamic JPA queries for Spring Boot microservices

Spring Middleware provides a small framework for building parameterized JPQL queries from typed search DTOs. It keeps repository code declarative by translating annotated search objects into joins, conditions, ordering, and pagination at runtime.

Typed search DTOs Parameterized JPQL Join handling Sub-searches Pagination and ordering Extensible condition builders

What the JPA module provides

The JPA module builds safe, parameterized JPQL from POJO search objects. Instead of manually composing query strings in each repository, services define focused search DTOs and let the framework generate the query structure at runtime.

  • Declarative search with annotated DTOs
  • Automatic join handling
  • Pagination, ordering, and optional post-filtering
  • Small, testable query builder components

Key capabilities

  • @SearchProperty: equality, LIKE, comparison, inclusion, concat, null checks, and pre-conditions.
  • @SearchProperties: multiple search properties on one field, combined into OR fragments.
  • @SubSearch: nested search composition.
  • @SearchPropertyExists: empty / not-empty collection checks.
  • @Join: explicit join metadata with LEFT, RIGHT, and FETCH options.

How it works

The module uses a set of small buffers and builders to generate a JPQL query string, parameterize it safely, and execute it through JPA.

01

Define a search DTO

Annotate a search class with @SearchForClass and annotate fields with search metadata.

02

Build query structure

QueryBuffer, WhereBuffer, and JoinBuffer generate SELECT, FROM, JOIN, and WHERE fragments.

03

Parameterize and execute

QueryParameterizer binds values safely and SearchRepositoryImpl executes the query with EntityManager.

Repository entry point

SearchRepositoryImpl creates QueryBufferParameters, delegates query construction to QueryBuffer, then parameterizes and executes the result.

Small builder model

Condition builders transform annotation semantics into query fragments. This keeps JPQL generation testable and extensible instead of hiding logic in one large query utility.

Important annotations

Search DTOs stay declarative because most of the query semantics live in annotations rather than in handwritten repository code.

@SearchForClass

Defines the entity class used in the SELECT and FROM clauses, and whether DISTINCT should be applied.

@SearchProperty

The main field-level annotation for LIKE, comparisons, inclusion operators, joins, concatenation, null checks, and pre-conditions.

@SubSearch and related annotations

Nested search objects, collection existence checks, and grouped property definitions allow richer WHERE fragments without manual JPQL composition.

Defining a search DTO

Search DTOs describe query intent. The framework reads the annotations and turns them into joins, parameterized predicates, and optional DISTINCT selection.

CatalogSearch example

@SearchForClass(value = Catalog.class, distinct = true)
public class CatalogSearch implements Search {

    @SearchProperty(
        value = "name",
        isLike = true,
        concat = @Concat({"name", "description"})
    )
    private String q;

    @SearchProperty(
        value = "products",
        isLike = false,
        join = @Join(value = "c.products p", left = true, fetch = true)
    )
    private List<UUID> productIds;

    // getters / setters
}

ProductSearch example

@SearchForClass(value = Product.class)
public class ProductSearch implements Search {

    @SearchProperty(value = "name", isLike = true)
    private String name;

    @SearchProperty(value = "catalog.id", isLike = false)
    private UUID catalogId;

    // getters / setters
}

Repository usage

Services extend the repository contract and use high-level search methods instead of composing JPQL manually in each repository.

Repository interface

public interface CatalogSearchRepository
    extends SearchRepository<Catalog, CatalogSearch> {
}

Service usage

// then in service:
List<Catalog> result =
    catalogSearchRepository.findBySearch(searchDto, pagination);

Generated JPQL

Query generation is deterministic and can be unit-tested directly. A search DTO with a non-null text query and product ids can produce JPQL like the following.

Conceptual JPQL output

SELECT DISTINCT c FROM Catalog c LEFT JOIN FETCH c.products p
 WHERE (UPPER(CONCAT(c.name,' ',c.description)) LIKE :param0)
   AND (c.products IN :param1)
 ORDER BY c.name

What this shows

  • DISTINCT from @SearchForClass
  • JOIN emitted once from join metadata
  • Concatenated LIKE expression
  • Parameterized values instead of raw string interpolation

Joins, sub-searches, and complex conditions

The module supports richer query construction than simple equality checks. Joins, grouped properties, pre-conditions, and nested search fragments can all participate in the final WHERE clause.

Join handling

JoinBuffer collects and normalizes joins, preventing duplicate JOIN clauses when multiple search properties reference the same path.

Nested sub-searches

@SubSearch allows a nested search object to generate its own WHERE fragment and be embedded into the parent search.

Pre-conditions and grouped properties

Raw JPQL fragments and grouped @SearchProperties definitions make it possible to combine generated predicates with more specific query rules.

Pagination, ordering, and post-filtering

Query generation is not limited to WHERE clauses. The repository implementation also coordinates ordering, pagination, and optional in-memory post-filtering when required.

Ordering

OrderBy is wired into QueryBufferParameters so QueryBuffer can append an ORDER BY clause.

Pagination

Pagination is applied through setFirstResult and setMaxResults when post-filtering is not required.

Optional post-filtering

When a custom QueryFilter is involved, pagination may be applied after the database fetch using PaginableResultDB.

Extending the condition builders

The module is extensible through small, annotation-driven builders. This makes it possible to introduce project-specific query semantics without rewriting the base infrastructure.

Builder resolution

The framework resolves a field annotation to a builder through @ConditionBufferBuilderClass, then asks the Spring-based factory for the concrete ConditionBufferBuilder.

Custom annotations

You can add new domain-specific annotations and small builder implementations when the default builders do not cover a project-specific use case.

Testing the generated queries

Because query generation is deterministic, it is straightforward to unit-test the produced JPQL and verify join, concat, and placeholder behavior.

Unit test example

@Test
public void buildsCatalogQuery_joinAndConcat_present() {
    CatalogSearch s = new CatalogSearch();
    s.setQ("summer");
    s.setProductIds(Collections.singletonList(UUID.randomUUID()));

    QueryBufferParameters<Catalog, Search> params =
        new QueryBufferParameters<>(s, Catalog.class, new OrderBy(), false);

    QueryBuffer<Catalog, Search> qb = new QueryBuffer<>(params);
    String query = qb.toString();

    Assertions.assertTrue(query.contains("LEFT JOIN") || query.contains("JOIN"));
    Assertions.assertTrue(query.toUpperCase().contains("UPPER(CONCAT"));
}

Testing guidance

  • Instantiate QueryBuffer directly
  • Assert the produced JPQL string
  • Reuse the project tests as patterns for new annotations and joins
  • Mock ApplicationContext when testing builder factories

Best practices

Dynamic JPQL generation should stay focused, explicit, and bounded.

Keep search DTOs small

Every exposed field can add more complexity to the generated JPQL, so keep search objects focused.

Paginate large queries

Prefer pagination whenever a search may return many rows.

Use join metadata intentionally

Use @Join to control LEFT, RIGHT, or FETCH joins and to avoid N+1 issues when appropriate.

Where to look in the code

The JPA module is centered around repository execution, buffers, and concrete condition builders.

SearchRepositoryImpl

jpa repository

.../jpa/repository/SearchRepositoryImpl.java

QueryBuffer

jpa buffer

.../jpa/buffer/QueryBuffer.java

JoinBuffer

jpa buffer

.../jpa/buffer/JoinBuffer.java

Condition builders

jpa builders

.../jpa/buffer/builder/*