Skip to main content
Version: 1.11.2

First Query

Once you can insert and fetch records (see First Entity), the next step is querying. This page covers the query patterns you will use most often: filtering with predicates, using repositories, streaming results, and writing type-safe queries with the metamodel.

Filtering with Predicates

The simplest way to query is with predicate methods directly on the ORM template or entity repository.

val users = orm.entity(User::class)

// Find all users in a city
val usersInCity: List<User> = users.findAll(User_.city eq city)

// Find a single user by email
val user: User? = users.find(User_.email eq "alice@example.com")

// Combine conditions with and / or
val results: List<User> = users.findAll(
(User_.city eq city) and (User_.name like "A%")
)

// Check existence
val exists: Boolean = users.existsById(userId)

// Count
val count: Long = users.count()

These predicate methods use the Static Metamodel (User_, City_), which is generated at compile time. The compiler catches typos and type mismatches in field references before your code runs.

Custom Repositories

For domain-specific queries that you will reuse, define a custom repository interface. This keeps query logic in a single place and makes it testable through interface substitution.

interface UserRepository : EntityRepository<User, Int> {

fun findByEmail(email: String): User? =
find(User_.email eq email)

fun findByNameInCity(name: String, city: City): List<User> =
findAll((User_.city eq city) and (User_.name eq name))

fun streamByCity(city: City): Flow<User> =
select { User_.city eq city }
}

// Get the repository from the ORM template
val userRepository = orm.repository<UserRepository>()

// Use it
val user = userRepository.findByEmail("alice@example.com")
val usersInCity = userRepository.findByNameInCity("Alice", city)

Custom repositories inherit all built-in CRUD operations (insert, findById, update, remove, etc.) from EntityRepository. You only add methods for domain-specific queries.

See Repositories for the full repository pattern, Spring integration, and scrolling.

Query Builder

For queries that need ordering, pagination, joins, or aggregation, use the fluent query builder.

val users = orm.entity(User::class)

// Ordering and pagination
val page = users.select()
.where(User_.city eq city)
.orderBy(User_.name)
.limit(10)
.resultList

// Joins (for entities not directly referenced by @FK)
val roles = orm.entity(Role::class)
.select()
.innerJoin(UserRole::class).on(Role::class)
.whereAny(UserRole_.user eq user)
.resultList

// Aggregation
data class CityCount(val city: City, val count: Long)

val counts = users.select(CityCount::class) { "${City::class}, COUNT(*)" }
.groupBy(User_.city)
.resultList

See Queries for the full query reference, including scrolling, distinct results, and compound field handling.

Streaming

For large result sets, streaming avoids loading all rows into memory at once. Rows are fetched lazily from the database as you consume them.

Kotlin uses Flow, which provides automatic resource management through structured concurrency:

val users: Flow<User> = orm.entity(User::class).selectAll()

// Process each row
users.collect { user -> println(user.name) }

// Transform and collect
val emails: List<String> = users.map { it.email }.toList()

See Batch Processing and Streaming for bulk operations and advanced streaming patterns.

SQL Templates

When the query builder does not cover your use case (for example, CTEs, window functions, or database-specific syntax), SQL Templates give you full control over the SQL while retaining type safety and parameterized values.

val users = orm.query {
"""SELECT ${User::class}
FROM ${User::class}
WHERE ${User_.city} = $city
ORDER BY ${User_.name}"""
}.resultList<User>()

With the Storm compiler plugin, interpolated expressions are automatically processed by the template engine: entity types expand to column lists, metamodel fields resolve to column names, and values become parameterized placeholders.

See SQL Templates for the full template reference.

Summary

Storm provides multiple query styles that you can mix freely:

StyleBest for
Predicate methods (find, findAll)Simple single-entity lookups
Custom repositoriesReusable domain-specific queries
Query builderOrdering, pagination, joins, aggregation
SQL TemplatesComplex SQL, CTEs, window functions

Start with the simplest approach that fits your query. Move to a more powerful style only when needed.

Next Steps

  • Queries -- full query reference
  • Repositories -- repository pattern and Spring integration
  • Entities -- annotations, nullability, and naming conventions
  • Relationships -- one-to-one, many-to-one, many-to-many
  • Metamodel -- compile-time type-safe field references