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:
| Stereotype | Intended statement shape |
|---|---|
findOne | Read one modeled result. |
find | Read a collection through the generated query path. |
execute | Execute a statement that does not fit a read, update, or delete operation. |
update | Execute an updating statement. |
delete | Execute a deleting statement. |
The stereotype is written before the operation name:
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:
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:
| Declaration | Meaning |
|---|---|
no language property | JPQL |
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:
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:
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:
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:
override-package.org.iworkz.core.converter=com.example.shared.converterFor 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:
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.
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:
SELECT item
FROM CatalogItem item
WHERE item.sku = ?1 AND item.active = trueThe insertion expression has this form:
@{ partName before='...', separator='...', after='...', default='...' }beforeis emitted once before the first contributed part.separatoris emitted between contributed parts.afteris emitted once after at least one contributed part.defaultsupplies 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:
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.
