Skip to content

Repository statements and composition

Advanced SpringBoot contract

The statement body is application code embedded in the model. Review it with the same care as handwritten JPQL or SQL.

SpringBoot can turn statements on operations of a persistent entity into Spring Data repository queries. A statement can contain JPQL or native SQL. Larger statements can be assembled from a root statement and named parts connected through augments or, for an explicit template reference, the SpringBoot-specific commandTemplate property.

The underlying CMN syntax is described in Services and operations. CMN stores statement bodies and the base-operation relationship; this page defines how the SpringBoot persistence cartridge interprets them.

1. Explicit statement operations on entities

Custom repository statements are modeled as operations inside a persistent complex type. Use an explicit persistence stereotype to select the generated operation shape:

StereotypeIntended statement shape
findOneRead one modeled result.
findRead a collection through the generated query path.
executeExecute a statement that does not fit a read, update, or delete operation.
updateExecute an updating statement.
deleteExecute a deleting statement.

The stereotype is written before the operation name:

cmn
type<entity> CatalogItem {
    id**: Id
    sku*: String

    <findOne> loadBySku(sku*: String): CatalogItem =>
        SELECT item
        FROM CatalogItem item
        WHERE item.sku = ?1;;
}

This entity operation contributes a repository operation and a corresponding data-access operation. It does not create an HTTP endpoint. Model a resource and provide it from a component when the operation also belongs to a generated HTTP boundary.

For a custom statement, use a descriptive operation name plus the explicit stereotype, as in <findOne> loadBySku. These stereotypes are declared by the Java profile. A complete CMN model that uses them explicitly must import org.joinedworkz.facilities.common.profiles.java; see the complete model in Native result projections.

2. Java conventions and SpringBoot statements

SpringBoot inherits the Java profile's operation naming conventions. The conventions and their automatic stereotype assignment are independent of SpringBoot. They form a separate modeling path from explicit JPQL or SQL.

For statement-free operations, the current SpringBoot persistence generator evaluates the automatically assigned findFirstBy, findBy, findOneBy, countWith, existsWith, updateJust, and deleteWhere stereotypes. A common convention-based declaration is:

cmn
type<entity> CatalogItem {
    id**: Id
    sku*: String
    active*: Boolean

    findBy(sku)
    findOneBy(sku)
    countWith(active)
    existsWith(sku)
}

SpringBoot reports a warning when a findBy* or findOneBy* operation also contains a statement. For an explicit statement, use a name such as loadBySku or searchActiveItems together with <findOne>, <find>, <update>, <delete>, or another applicable explicit stereotype.

Although updateWhere* belongs to the generic Java profile vocabulary, a complete SpringBoot generator contract for it is not documented. Do not infer SpringBoot support from the automatic stereotype assignment.

3. Select JPQL or native SQL

The language property selects how SpringBoot emits the repository query:

DeclarationMeaning
no language propertyJPQL
language='jpql'JPQL, stated explicitly
language='sql'Native SQL

The explicit values are lowercase. Any other value is invalid.

For JPQL, use generated entity names and entity field names. SpringBoot emits the composed body as a Spring Data @Query statement:

cmn
type<entity> CatalogItem {
    id**: Id
    sku*: String

    <findOne> loadBySku(sku*: String): CatalogItem language='jpql' =>
        SELECT item
        FROM CatalogItem item
        WHERE item.sku = ?1;;
}

For native SQL, use the effective database table and column names. SpringBoot marks the generated repository query as native:

cmn
type CatalogItemSummary {
    id: Id
    sku: String
    title: String
}

type<entity> CatalogItem {
    id**: Id
    sku*: String
    title*: String

    <findOne> loadSummaryBySku(sku*: String): CatalogItemSummary
        language='sql' =>
        SELECT
            item.id AS "id",
            item.sku AS "sku",
            item.title AS "title"
        FROM catalog_item item
        WHERE item.sku = ?1;;
}

The examples use positional Spring Data parameters. ?1 refers to the first operation parameter, ?2 to the second, and so on. Keep the operation signature and statement positions synchronized.

4. Native result projections

A native query returns database rows rather than managed entities. Model a complex result type and give every selected column an alias matching the generated DTO field name:

cmn
type CatalogItemSummary {
    id: Id
    sku: String
    title: String
}

With the default decorated DTO naming, the preceding native operation exposes CatalogItemSummaryDto from its generated data-access method. The repository query receives a Jakarta Persistence Tuple, and the generated tuple converter reads the aliases id, sku, and title into the modeled result.

The generated tuple converter injects the replaceable runtime-glue type org.iworkz.core.converter.CommonValueConverter. A project must either supply a compatible Spring bean at that package or map the narrow converter package to its own implementation:

properties
override-package.org.iworkz.core.converter=com.example.shared.converter

For scalar projection fields, that implementation provides <T> T convertTo(Object value, Class<T> targetType). A projection containing a multiple field also requires <T> List<T> convertToList(Object value, Class<T> targetType). The concrete database can return UUIDs, timestamps, arrays, and other values in driver-specific representations, so these conversions belong to tested application glue. Ensure that Spring component scanning includes the override package. Do not broaden the package override to unrelated org.iworkz.core contracts unless the project also replaces those contracts.

The complete synthetic model fixes its database identifiers explicitly so that the SQL does not depend on a naming-flavor default:

cmn
core package org.example.catalog

import org.joinedworkz.facilities.common.base
import org.joinedworkz.facilities.common.profiles.java
import org.joinedworkz.facilities.profiles.springboot

platform SpringBoot

type CatalogItemSummary {
    id: Id
    sku: String
    title: String
}

type<entity> CatalogItem tableName='catalog_item' {
    id**: Id columnName='id'
    sku*: String columnName='sku'
    title*: String columnName='title'
    active*: Boolean columnName='active'

    <findOne> loadSummaryBySku(sku*: String): CatalogItemSummary
        language='sql' =>
        SELECT
            item.id AS "id",
            item.sku AS "sku",
            item.title AS "title"
        FROM catalog_item item
        WHERE item.sku = ?1;;
}

Aliases are part of this generated mapping contract. A database expression or renamed column still needs the expected result alias. For richer projections, model every returned field and keep its SQL alias aligned with the effective DTO field name, including the selected DTO naming mode.

5. Compose statements with augments

The preferred composition path uses CMN's augments relationship. A base operation supplies an unnamed root statement containing a named insertion point. Operations along the augmentation chain contribute statement parts with that name.

cmn
type<entity> CatalogItem {
    id**: Id
    sku*: String
    active*: Boolean

    abstract catalogItemQueryTemplate() =>
        SELECT item
        FROM CatalogItem item
        @{ criteria before='WHERE ', separator=' AND ' };;

    abstract catalogItemBySkuCondition() augments catalogItemQueryTemplate
        criteria => item.sku = ?1;;

    <findOne> loadActiveBySku(sku*: String): CatalogItem
        augments catalogItemBySkuCondition
        language='jpql'
        criteria => item.active = true;;
}

SpringBoot begins with catalogItemQueryTemplate, collects the two criteria parts in base-to-derived order and inserts them at the named position. The resulting JPQL body is:

sql
SELECT item
FROM CatalogItem item
WHERE item.sku = ?1 AND item.active = true

The insertion expression has this form:

text
@{ partName before='...', separator='...', after='...', default='...' }
  • before is emitted once before the first contributed part.
  • separator is emitted between contributed parts.
  • after is emitted once after at least one contributed part.
  • default supplies text when no operation contributes the named part.

All four options are optional. When several parts exist and no separator is configured, they are separated by a line break.

The final generated operation declares its own parameters, result, stereotype, and language. augments supplies the statement-composition chain; it does not inherit that operation signature.

6. Select a template with commandTemplate

commandTemplate is an advanced SpringBoot property for selecting a template operation without making it the CMN base operation:

cmn
type<entity> CatalogItem {
    id**: Id
    title*: String

    abstract catalogItemQueryTemplate() =>
        SELECT item
        FROM CatalogItem item
        @{ criteria before='WHERE ', separator=' AND ' };;

    <findOne> loadByTitle(title*: String): CatalogItem
        commandTemplate=catalogItemQueryTemplate
        language='jpql'
        criteria => item.title = ?1;;
}

The referenced operation contributes only the base statement for SpringBoot statement composition. It does not contribute parameters, a result, stereotypes, or language; declare all of those on the generated operation.

Prefer augments when the modeled operation really specializes another operation. Use commandTemplate when the relationship exists only to reuse a statement template. If both are present, augments takes precedence.

7. Composition and query boundaries

  • The root template needs one unnamed statement body. Named bodies are parts referenced from its @{ ... } insertion expressions.
  • Use a statement-part name at most once in one operation. Contribute further parts with the same name from later operations in the augmentation chain.
  • A template part can contain another insertion expression. Recursive insertion is invalid.
  • JPQL uses entity and Java field names; native SQL uses effective database identifiers and is database-specific.
  • CMN does not parse or type-check the JPQL or SQL body. A syntactically valid CMN model can still contain a query rejected by Spring Data, Hibernate, or the target database.
  • Changing a statement body changes replaceable generated repository and data-access output. Regenerate from a clean replaceable-output state and run the application's persistence tests.
  • A native projection depends on its selected aliases. Review the generated tuple converter together with the query whenever the result type or DTO naming changes.