Query store (experimental)

The query store is experimental: it may change or be removed. Use the JPA store for read models that must persist.

A @QueryProjection keeps its read models in the framework’s tables in the application’s database, and lets the application query them by chosen properties. The read model is a plain class, stored as JSON.

Setup

Add spring-ddd-starter-cqrs-jdbc-query. Two settings apply:

  • spring.ddd.cqrs.projection.query.enabled defaults to true; with false the store is left out, and a @QueryProjection then fails the startup.

  • spring.ddd.cqrs.jdbc.initialize-schema is always (the default), embedded or never, and says where the framework creates a missing table. It also controls the READ_MODEL_EPOCHS table, so never stops that table being created too, for JPA projections as well.

    An existing table is never altered, only checked at startup, and a missing one fails the startup where tables are not created.

Declaring

@QueryProjection(name, errorHandler, table) declares the projection. Each projection has one table of its own, named after the projection unless table names it: letters, digits and _, not starting with a digit, at most 63 characters.

@QueryField marks a property the application queries by. unique makes its value unique across the read models, and type sets how it is stored and sorted: TEXT (the default), NUMBER, TEMPORAL or BOOLEAN.

class AccountSummary(
    @ReadModelKey var accountId: AccountId = AccountId(),
    @QueryField(unique = true) var iban: String = "",
    @QueryField var owner: String = "",
    @QueryField(type = QueryFieldType.NUMBER) var balance: BigDecimal = BigDecimal.ZERO,
)

@QueryProjection(name = "account-summary", errorHandler = DeadLetterKeyErrorHandler::class)
class AccountSummaryProjection {

    @InitReadModel
    fun init(id: AccountId) = AccountSummary(accountId = id)

    // listeners, see Writing a projection
}

Querying

QueryProjectionRepository<T, ID> offers the application’s queries. Finder names, OrderBy, Sort and Pageable may name @QueryField properties only, and the usual operators are allowed. A finder returns a single result only for one equality on a unique field. A finder the store cannot run fails the startup.

interface AccountSummaryRepository : QueryProjectionRepository<AccountSummary, AccountId> {
    fun findByIban(iban: String): AccountSummary?
    fun findByOwnerOrderByBalanceDesc(owner: String): List<AccountSummary>
}

The repository’s id type is the type of the read model’s own id; see The read model’s own id. To serve the read models over HTTP, see Read models over HTTP.

Ceilings

  • Text sorts as text: a BigDecimal kept as TEXT does not sort numerically, so declare it NUMBER.

  • A text field holds up to 255 characters.

  • Times are kept to the microsecond, in UTC; the stored read model keeps full precision.

  • An In or NotIn list of more than about 2100 elements fails on SQL Server.

  • On MySQL, MariaDB and SQL Server, whose default collations ignore case, text equality ignores case.