# Storm Framework > Storm is an ORM for Kotlin 2.0+ and Java 21+, built for agentic coding and > performance. > > Every detail lives in the model. Entities are plain Kotlin data classes or Java > records: one class is the table, its keys and its relations, and the queries > follow from it. Underneath is a thin layer over JDBC. There is no persistence > context, no transparent lazy loading, no proxy generation, and no entity state > management, so the code is exactly what runs. The domain model gives one-line, > type-safe queries across relations, returning the whole entity graph in one > statement. > > For coding agents: the Storm CLI installs rules and skills for Claude Code, > Cursor, GitHub Copilot, Windsurf and Codex, plus an optional local MCP server > that exposes schema metadata (table definitions, column types, constraints) > while keeping database credentials away from the LLM. Built-in verification > (validateSchema(), SqlCapture) lets the agent check its own work before > anything is committed. > > Get started: `npx @storm-orm/cli init` (existing project) or > `npx @storm-orm/cli demo` (empty directory) Website: https://orm.st GitHub: https://github.com/storm-orm/storm-framework License: Apache 2.0 ## Working with a coding agent - [Agentic Coding](https://orm.st/docs/ai): why Storm suits agents, CLI setup, verify-then-trust workflow - [AI Tools Reference](https://orm.st/docs/ai-reference): configuration locations, rules and skills per tool - [Database Connections & MCP](https://orm.st/docs/database-and-mcp): schema-aware MCP server, read-only data access, security - Every docs page is also available as plain Markdown: append `.md` to its URL (for example https://orm.st/docs/entities.md). ## Documentation - [Introduction](https://orm.st/docs/) - [Getting Started](https://orm.st/docs/getting-started) - [Entities](https://orm.st/docs/entities) - [Projections](https://orm.st/docs/projections) - [Relationships](https://orm.st/docs/relationships) - [Repositories](https://orm.st/docs/repositories) - [Queries](https://orm.st/docs/queries) - [Metamodel](https://orm.st/docs/metamodel) - [Refs](https://orm.st/docs/refs) - [Entity Design](https://orm.st/docs/entity-design) - [Transactions](https://orm.st/docs/transactions) - [Spring Integration](https://orm.st/docs/spring-integration) - [Ktor Integration](https://orm.st/docs/ktor-integration) - [Database Dialects](https://orm.st/docs/dialects) - [Testing](https://orm.st/docs/testing) - [Converters](https://orm.st/docs/converters) - [JSON Columns](https://orm.st/docs/json) - [Polymorphism](https://orm.st/docs/polymorphism) - [Entity Lifecycle](https://orm.st/docs/entity-lifecycle) - [Serialization](https://orm.st/docs/serialization) - [Validation](https://orm.st/docs/validation) - [Batch Processing and Streaming](https://orm.st/docs/batch-streaming) - [Upserts](https://orm.st/docs/upserts) - [SQL Templates](https://orm.st/docs/sql-templates) - [Hydration](https://orm.st/docs/hydration) - [Dirty Checking](https://orm.st/docs/dirty-checking) - [Entity Cache](https://orm.st/docs/entity-cache) - [Configuration](https://orm.st/docs/configuration) - [SQL Logging](https://orm.st/docs/sql-logging) - [Metrics](https://orm.st/docs/metrics) - [Storm vs Other Frameworks](https://orm.st/docs/comparison) - [FAQ](https://orm.st/docs/faq) - [Migration from JPA](https://orm.st/docs/migration-from-jpa) - [API Reference: Kotlin](https://orm.st/docs/api-kotlin) - [API Reference: Java](https://orm.st/docs/api-java) ## Full Documentation For large-context tools, the complete documentation is available at: https://orm.st/llms-full.txt ## Quick Reference ### Entity Definition Kotlin: ```kotlin data class City( @PK val id: Int = 0, val name: String, val population: Long ) : Entity data class User( @PK val id: Int = 0, val email: String, val name: String, @FK val city: City // Non-nullable FK = INNER JOIN ) : Entity ``` Java: ```java record City(@PK Integer id, String name, long population ) implements Entity {} record User(@PK Integer id, String email, String name, @FK City city ) implements Entity {} ``` ### Annotations - `@PK` - Primary key. Supports auto-generated keys (default value = 0 or null). - `@FK` - Foreign key. References another entity. Nullability determines JOIN type. - `@UK` - Unique key. Enables `findBy(UK)` lookups. - `@Version` - Optimistic locking version field. - `@Persist` - Marks fields that should be persisted but are not auto-detected. - `@DbTable("name")` - Override the default table name. - `@DbColumn("name")` - Override the default column name. - `@Inline` - Embeds fields of a nested type directly into the parent table. ### Composite Primary Keys For join/junction tables, wrap key columns in a separate data class with raw types only — never `@FK`, entity types, or `Ref` inside the PK class. Declare `@FK` fields on the entity with `@Persist(insertable = false, updatable = false)`. ```kotlin data class UserRolePk(val userId: Int, val roleId: Int) data class UserRole( @PK(generation = NONE) val id: UserRolePk, @FK @Persist(insertable = false, updatable = false) val user: User, @FK @Persist(insertable = false, updatable = false) val role: Role ) : Entity ``` ### Primary Key as Foreign Key For dependent one-to-one relationships or extension tables, use both `@PK` and `@FK` on the same field: ```kotlin data class UserProfile( @PK(generation = NONE) @FK val user: User, val bio: String? ) : Entity ``` ### CRUD Operations Kotlin: ```kotlin val orm = ORMTemplate.of(dataSource) // Insert (returns entity with generated ID) val city = orm insert City(name = "Sunnyvale", population = 161_884) // Find by ID val found: City? = orm.findById(city.id) // Update orm update city.copy(population = 160_000) // Remove orm remove city // Find with predicate val users: List = orm.findAll(User_.city.name eq "Sunnyvale") ``` Java: ```java var orm = ORMTemplate.of(dataSource); var cities = orm.entity(City.class); // Insert City city = cities.insert(new City(0, "Sunnyvale", 161_884)); // Find by ID Optional found = cities.findById(city.id()); // Update cities.update(new City(city.id(), "Sunnyvale", 160_000)); // Remove cities.remove(city); // Query List users = orm.entity(User.class).select() .where(User_.city.name, EQUALS, "Sunnyvale") .getResultList(); ``` ### Repositories Kotlin: ```kotlin interface UserRepository : EntityRepository { fun findByCityName(name: String) = findAll(User_.city.name eq name) } val userRepository = orm.repository() ``` Java: ```java interface UserRepository extends EntityRepository { default List findByCityName(String name) { return select().where(User_.city.name, EQUALS, name).getResultList(); } } UserRepository userRepository = orm.repository(UserRepository.class); ``` ### Type-Safe Queries (Metamodel) Generated metamodel classes (User_, City_) provide compile-time checked field references: Kotlin: ```kotlin val users = orm.entity(User::class) .select() .where(User_.city.name eq "Sunnyvale") .orderBy(User_.name) .resultList ``` Java: ```java List users = orm.entity(User.class) .select() .where(User_.city.name, EQUALS, "Sunnyvale") .orderBy(User_.name) .getResultList(); ``` ### Relationships and Refs FK nullability controls JOIN type: - `@FK val city: City` (non-null) = INNER JOIN - `@FK val city: City?` (nullable) = LEFT JOIN Ref for deferred loading (avoids eager graph traversal): ```kotlin data class User( @PK val id: Int = 0, @FK val city: Ref // Loads city ID only; fetch city on demand ) : Entity val cityName = user.city.fetch().name // Fetches when needed ``` A Ref stays queryable. Filter, order, and select through it with the metamodel; Storm adds the join for the referenced table on demand, only when the query navigates beyond the FK. Selecting the root leaves the field an unloaded Ref: ```kotlin val users = orm.entity() .select() .where(User_.city.country.name eq "United States") // joins city + country on demand .resultList ``` Nodes beyond a Ref are navigation-only: usable in where/orderBy/groupBy/having/selected columns, but not in value operations. Group by the reference itself with resultGroupedByRef, not resultGroupedBy on a beyond-reference path. Besides a path, the referenced table can be named by type: selecting it (select(City::class)) joins it and hydrates it with its own FKs, and joining onto it (innerJoin().on()) brings it in; both share the occurrence a navigating path resolves against, so a query that does both joins it once, and a table joined explicitly keeps that occurrence. Prefer a Ref for FKs not hydrated on most reads to avoid eager join fan-out while staying queryable. Navigation may cross more than one reference across distinct tables. A Ref carries the target's primary key (ref.id() reads it without fetching), so User_.city.id resolves to the FK column and needs no join, matching entity-FK behavior; any other target column joins. A self-referential FK must be a Ref, and it is navigable: the table joins itself, one alias per occurrence. The typed metamodel navigates a cycle two hops deep (generated metamodels build children eagerly and cannot recurse); deeper cyclic paths are named as strings, which the engine resolves to any depth. ### Dependencies (BOM) Kotlin (Gradle) — apply the Storm plugin (id "st.orm", Gradle 8.5+). It imports the BOM and adds storm-kotlin, storm-core, the KSP metamodel processor, and the compiler plugin, so no Storm dependencies are declared by hand. On Kotlin 2.3 and newer it also applies KSP itself, as long as the Kotlin plugin is declared before it; on Kotlin 2.0 through 2.2, add id("com.google.devtools.ksp") with the version paired to the Kotlin version (e.g. 2.0.21-1.0.28 for Kotlin 2.0.21): ```kotlin plugins { kotlin("jvm") version "2.4.0" id("st.orm") version "1.14.1" } ``` Java (Maven): ```xml st.orm storm-bom 1.14.1 pom import st.orm storm-java21 st.orm storm-core runtime ``` ### Spring Boot Starter Kotlin (Gradle) — apply the Storm plugin, then add the starter: ```kotlin plugins { kotlin("jvm") version "2.4.0" id("st.orm") version "1.14.1" } dependencies { implementation("st.orm:storm-kotlin-spring-boot-starter") } ``` Java (Maven): ```xml st.orm storm-spring-boot-starter ``` ### Ktor Integration Kotlin (Gradle) — apply the Storm plugin, then add the Ktor module: ```kotlin plugins { kotlin("jvm") version "2.4.0" id("st.orm") version "1.14.1" } dependencies { implementation("st.orm:storm-ktor") } ``` ### Available Modules Core: storm-core, storm-kotlin, storm-java21 Spring: storm-spring, storm-kotlin-spring, storm-spring-boot-starter, storm-kotlin-spring-boot-starter Ktor: storm-ktor, storm-ktor-test JSON: storm-jackson2, storm-jackson3, storm-kotlinx-serialization Dialects: storm-postgresql, storm-mysql, storm-mariadb, storm-oracle, storm-mssqlserver Build: storm-bom, storm-compiler-plugin-2.0, storm-compiler-plugin-2.1 Gradle plugin: id "st.orm" (Gradle Plugin Portal) — one-line setup for Kotlin/Java, imports the BOM and wires metamodel + compiler plugin Metamodel: storm-metamodel-processor (Java), storm-metamodel-ksp (Kotlin) Testing: storm-test, storm-h2 (test runtime), com.h2database:h2 (test runtime — required, storm-h2 declares it as provided) Validation: storm-kotlin-validator ## API Design: Prefer the Simplest Approach Always use the simplest method that meets your needs. Escalate only when the simpler level cannot express what you need. **Kotlin** has four levels: | Level | Approach | Best for | |-------|----------|----------| | 1 | Convenience methods (`find`, `findAll`, `removeAll`, `count`, `exists`) | Simple lookups and operations | | 2 | Builder with predicate (`select(predicate)`, `delete(predicate)`) | Filtered queries needing ordering, pagination, or joins | | 3 | Block DSL (`select { }`, `delete { }`) | Complex queries with multiple joins and conditions | | 4 | SQL Templates | CTEs, window functions, database-specific features | **Java** has three levels (no block DSL): | Level | Approach | Best for | |-------|----------|----------| | 1 | Convenience methods (`findBy`, `findAllBy`, `removeAllBy`, `countBy`, `existsBy`) | Simple lookups and operations | | 2 | Builder (`select().where()`, `delete().where()`) | Most queries needing ordering, pagination, or joins | | 3 | SQL Templates | CTEs, window functions, database-specific features | ### When to use each — and when NOT to | Need | Use (simplest) | Don't use (unnecessarily complex) | |------|----------------|-----------------------------------| | All rows as list | `findAll()` | `select().resultList` | | Filter by single field | `findAllBy(field, value)` | `select().where(field, op, value).resultList` | | Filter by predicate (Kotlin) | `findAll(predicate)` | `select(predicate).resultList` | | Single result by predicate (Kotlin) | `find(predicate)` | `select(predicate).optionalResult` | | Delete by predicate (Kotlin) | `removeAll(predicate)` | `delete(predicate).executeUpdate()` | | Delete by field | `removeAllBy(field, value)` | `delete().where(field, op, value).executeUpdate()` | | Filtered + **ordering/pagination** | `select(predicate).orderBy(...)` | convenience methods (can't add ordering) | | Aggregates, CTEs, window functions | SQL Template | QueryBuilder (can't express these) | ### SQL Template Escalation SQL Templates are an escape hatch. Three rules: 1. **Code-first:** If it can be done with QueryBuilder methods (joins, where, orderBy, groupBy, having), do it in code. 2. **Metamodel in templates:** When you do need a template fragment, use metamodel references (`${User_.email}`, not `"email"`). 3. **Full SQL last resort:** Full `SELECT...FROM` templates only for totally custom queries (CTEs, UNIONs, window functions). ### The delete/remove distinction - `remove` operates on entities or ids you already have (immediate execution): `remove(entity)`, `removeById(id)`, `removeAll()`, `removeAll(predicate)` (Kotlin). - `delete` builds a query to find and delete rows by criteria (returns `QueryBuilder`): `delete().where(...).executeUpdate()`. ## Critical Gotchas 1. **QueryBuilder is immutable.** Every builder method returns a new instance. Always use the returned value: ```kotlin // WRONG - discards the where clause val query = orm.entity(User::class).select() query.where(User_.name eq "Alice") // returned value ignored! // CORRECT val query = orm.entity(User::class).select() .where(User_.name eq "Alice") ``` 2. **No lazy loading, no proxies, no persistence context.** Entities are plain records/data classes. There is no managed state, no dirty tracking, no automatic flush. You explicitly call insert/update/remove. 3. **No collection fields on entities.** Storm entities cannot have List fields. Relationships are modeled from the child side using @FK. To get a user's orders, query from Order with a filter on User. 4. **Close Java Streams.** In Java, `select().getResultStream()` returns a Stream backed by a database cursor. Always use try-with-resources: ```java try (Stream stream = users.select().getResultStream()) { stream.forEach(System.out::println); } ``` In Kotlin, `select().resultFlow` returns a Flow which handles cleanup automatically. 5. **DELETE/UPDATE without WHERE throws by default.** To prevent accidental bulk operations, Storm throws if you omit a WHERE clause. Use `unsafe()` if you intentionally want to affect all rows: ```kotlin orm.entity(User::class).delete().unsafe().executeUpdate() // Deletes all users ``` 6. **FK nullability determines JOIN type.** A non-nullable @FK produces INNER JOIN; a nullable @FK produces LEFT JOIN. This is by design, not a bug. 7. **Use Ref for deferred loading, without losing queryability.** If you do not want Storm to eagerly load a related entity, wrap the FK type in Ref. This loads only the foreign key value and defers the full entity fetch until you call fetch(). The reference stays queryable: filter, order, and select through it with the metamodel (User_.city.country.name), and Storm adds the join only for the query that navigates beyond the FK. Nodes beyond a Ref are navigation-only: usable in where/orderBy/groupBy/having/selected columns, but not in value operations (group by the reference with resultGroupedByRef). The referenced table can also be named by type rather than by path: selecting it hydrates it with its own FKs, and joining onto it brings it in. A self-referential FK must be a Ref and is navigable: the table joins itself, one alias per occurrence. The typed metamodel goes two hops deep; deeper cyclic paths are named as strings. Prefer Ref for FKs not hydrated on most reads to avoid eager join fan-out. 8. **Use Ref for map keys and set membership.** Prefer `Ref` (via `.ref()`) for map keys, set membership, and identity-based lookups. `Ref` provides identity-based `equals`/`hashCode` on the primary key. Do not use full entities as map keys (relies on data class equals over all fields) or formatted strings for entity lookup. When a projection already returns `Ref`, use it directly without calling `.ref()` again. 9. **Metamodel navigation depth.** Multiple levels of navigation are allowed on the root entity of a query. However, joined (non-root) entities can only navigate one level deep. For deeper navigation from a joined entity, explicitly join the intermediate entity. 10. **Use `t()` for parameter binding in lambdas (Kotlin, requires compiler plugin).** Inside `.having {}` and other lambda expressions, use the `t()` function to ensure proper parameter binding and SQL injection protection: ```kotlin // CORRECT .having { "COUNT(DISTINCT ${t(column)}) = ${t(numTypes)}" } // WRONG - raw interpolation bypasses parameter binding .having { "COUNT(DISTINCT $column) = $numTypes" } ```