${html}`;
+ };
+ const selectedIdx = variants ? Math.max(0, variants.findIndex((v) => v.selected)) : 0;
+ const varSel = variants
+ ? ``
+ : '';
const sqlBtn = sql
? `Show SQL`
: '';
+ const copyText =
+ copy === true ? toPlain(variants ? variants[selectedIdx].code : code) : copy;
+ const copyBtn = copyText
+ ? `Copy`
+ : '';
const sqlConsole = sql
? `${sql}${code}
+ ${varSel}${sqlBtn}${copyBtn}
| Feature | Storm | JPA / Hibernate | jOOQ | Jimmer | Exposed | Ktorm | +Feature | Storm | JPA / Hibernate | jOOQ | Exposed | Ktorm |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Entity model | Immutable data class (~5 lines) | Mutable class (~30, ~10 with Lombok) | Generated from schema | Immutable interface (KSP-generated) | DSL table object (+ optional DAO) | Mutable interface (+ DSL table) | ||||||
| Immutable entities | Yes | No | Yes | Yes | DSL only | No | ||||||
| Type-safe queries | Yes | Criteria API | Yes | Yes | Yes | Yes | ||||||
| N+1 handling | Single-query entity graph | Common pitfall | Manual | Batched queries | Manual | Manual | ||||||
| Full SQL escape hatch | SQL templates | Native queries | It is SQL | SQL expressions | Raw exec() | Raw JDBC | ||||||
| Languages | Kotlin + Java | Kotlin + Java | Kotlin + Java | Kotlin + Java | Kotlin only | Kotlin only | ||||||
| License | Apache 2.0 | LGPL 2.1 | Commercial for some DBs | Apache 2.0 | Apache 2.0 | Apache 2.0 | ||||||
| Entity model | Immutable data class (~5 lines) | Mutable class (~30, ~10 with Lombok) | Generated from schema | DSL table object (+ optional DAO) | Mutable interface (+ DSL table) | |||||||
| Immutable entities | Yes | No | Yes | DSL only | No | |||||||
| Type-safe queries | Yes | Criteria API | Yes | Yes | Yes | |||||||
| N+1 handling | Single-query entity graph | Common pitfall | Manual | Manual | Manual | |||||||
| Query across relations | One line: joins and mapping derived from @FK | JPQL strings or Criteria builders | Implicit path joins + multiset | Manual joins and row mapping | Manual joins | |||||||
| Session state | None: no session, no flush | Persistence context, flush, dirty checking | None | DSL none · DAO transaction-bound | Per-entity change tracking | |||||||
| Deferred loading | Explicit Ref<T> | Proxies, session-bound | Not applicable | DAO lazy, transaction-bound | Manual joins | |||||||
| Standalone transactions | Propagation, isolation, timeout, commit hooks | EntityTransaction · JTA/Spring for more | Lambda API, nested | Nested, isolation config | Lambda API | |||||||
| Row mapping | Compile-time generated, reflection-free | Reflection / bytecode | Generated records | Manual (DSL) | Dynamic proxies | |||||||
| Full SQL escape hatch | SQL / SQL templates | Native queries | It is SQL | Raw exec() | Raw JDBC | |||||||
| Languages | Kotlin + Java | Kotlin + Java | Kotlin + Java | Kotlin only | Kotlin only | |||||||
| License | Apache 2.0 | LGPL 2.1 | Commercial for some DBs | Apache 2.0 | Apache 2.0 |
How would you design an ORM you would enjoy using? Immutable data-class entities? One-line queries, checked at compile time? No proxies, no N+1, no persistence context? That is ST/ORM.
-After 18 months of commercial use, it is ready to be challenged. Your move.
+Install Storm, define an entity as a plain Kotlin data class, and run a type-safe query. No persistence context, no proxies, no XML. Here is the whole path.
+Install Storm, define two linked entities as plain data classes, and query across the relation in one type-safe line. No persistence context, no proxies, no XML. Here is the whole path.
The fastest way in is the Storm CLI. It installs Storm-aware rules and skills for your AI coding assistant (Claude, Cursor, Copilot, Windsurf, Codex) and sets up a schema-aware MCP server, so the entities and queries it generates match your real schema.
- ${editor({file: 'terminal', tag: 'shell', code: cli})} -Prefer to wire the build by hand? Add the Storm modules yourself. This is the full Kotlin set for a runnable project on an in-memory H2 database; the BOM keeps the versions aligned.
- ${editor({file: 'build.gradle.kts', tag: 'Gradle · Kotlin DSL', code: install})} +Add the Storm modules to your build. This is the full Kotlin set for a runnable project on an in-memory H2 database; the BOM keeps the versions aligned.
+ ${editor({ + file: 'build.gradle.kts', + tag: 'Gradle · Kotlin DSL', + copy: true, + variants: KOTLIN_VARIANTS.map((v) => ({label: v.label, code: installFor(v), selected: v.selected})), + })}The ksp dependency generates the type-safe metamodel (Movie_), and the compiler plugin makes SQL templates injection-safe by default. On a real database, swap storm-h2 for your dialect and add its JDBC driver. See the installation guide for all options.
Entities are plain immutable data classes that implement Entity<ID>. Field names map to columns automatically (camelCase to snake_case). There is no base class to extend and no generated draft type to route through: the class you write is the object you get back.
Storm maps to an existing schema rather than creating one, so define the table with your migration tool (Flyway, Liquibase) or a one-off DDL script. Storm can verify at startup that your entities match it with validateSchema().
Open an ORMTemplate on your DataSource, ask for a repository, and query it. Toggle Show SQL to see exactly what Storm runs: one statement, fully parameterized, no surprises.
Working with an AI coding assistant? One command, run from the root of your project's workspace, installs Storm-aware rules and skills for it (Claude, Cursor, Copilot, Windsurf, Codex) and sets up a schema-aware MCP server, so the entities and queries it generates match your real schema.
+ ${editor({file: 'terminal', tag: 'shell', code: cli, copy: cliCommand})} + +Entities are plain immutable data classes that implement Entity<ID>. Field names map to columns automatically (camelCase to snake_case), and a relation is just a @FK field typed as the entity it points to. There is no base class to extend and no generated draft type to route through: the classes you write are the objects you get back.
Storm maps to an existing schema rather than creating one, so define the tables with your migration tool (Flyway, Liquibase) or a one-off DDL script; pick your database in the block below. Storm can verify at startup that your entities match it with validateSchema().
Open an ORMTemplate on your DataSource, insert a few records, and filter movies by their director's name in one type-safe line. Toggle Show SQL to see exactly what Storm runs: a single statement with the join included, fully parameterized, no N+1.
That is the core loop. Insert, findById, update, and remove all come for free on the repository; add your own one-line queries whenever you need them.
Start a plain Kotlin/Gradle project and add Ktor and Storm. The Ktor plugin manages the Ktor artifact versions, and the Storm BOM manages Storm's, so most dependencies need no version. We use H2 so there is nothing to install.
- ${editor({file: 'build.gradle.kts', tag: 'Gradle · Kotlin DSL', code: gradle})} + ${editor({ + file: 'build.gradle.kts', + tag: 'Gradle · Kotlin DSL', + copy: true, + variants: KOTLIN_VARIANTS.map((v) => ({label: v.label, code: gradleFor(v), selected: v.selected})), + })}Storm maps to an existing schema rather than generating one, which keeps migrations under your control. For this tutorial a small script is enough; H2 runs it on startup, so there is no migration tool to set up yet.
- ${editor({file: 'schema.sql', tag: 'SQL', code: schema})} +Storm maps to an existing schema rather than generating one, which keeps migrations under your control. For this tutorial a small script is enough; H2 runs it on startup, so there is no migration tool to set up yet. On another database, pick it in the block below.
+ ${editor({ + file: 'schema.sql', + tag: 'SQL', + copy: true, + variants: DATABASE_VARIANTS.map((d) => ({label: d.label, code: schemaFor(d), selected: d.selected})), + })}Two immutable data classes. A Bookmark belongs to a Folder; marking that reference @FK is the whole relationship. Field names map to columns automatically, so folder becomes the folder_id column.
The Storm Ktor plugin reads its DataSource from application.conf. No wiring code: install(Storm) builds a connection pool and, because the metamodel processor already indexed them, registers your repositories.
Extend EntityRepository and every CRUD method comes for free. Add your own one-line queries on top; Bookmark_ is the compile-time metamodel, so a typo in a field name fails to compile.
Extend EntityRepository and every CRUD method comes for free. Add your own one-line queries on top; Bookmark_ is the compile-time metamodel, so a typo in a field name fails to compile. FolderRepository shows the minimal case: one line, nothing to implement.
A standard Ktor main and module. Install Storm, register Storm's Jackson module so entities serialize cleanly, and mount the routes.
Read straight from the ORM, write inside transaction { }. Because Ktor and Storm are both coroutine-based, the transaction rides the request's coroutine with no proxies or annotations. call.orm and repository<T>() are extensions available right in the handler.
Read straight from the ORM, write inside transaction { }. Because Ktor and Storm are both coroutine-based, the transaction rides the request's coroutine with no proxies or annotations. repository<T>() is an extension available right in the handler.
The list endpoint is where Storm earns its keep. One call, one query: the folder for every bookmark is joined in, so there is no N+1 to discover later. Toggle Show SQL to see exactly what runs.
- ${editor({file: 'BookmarkRepository.kt', tag: 'Kotlin', code: listCode, sql: listSql})} + ${editor({file: 'BookmarkRepository.kt', tag: 'Kotlin', code: listCode, sql: listSql, copy: true})}Start the server and exercise it with curl. The created bookmark comes back with its full folder object, loaded in the same query that fetched the bookmark.
- ${editor({file: 'terminal', tag: 'shell', code: run})} + ${editor({file: 'terminal', tag: 'shell', code: run, copy: true})}storm-ktor-test spins up an in-memory database from your schema script and captures the SQL. Here we assert the create path is a single INSERT, so an accidental N+1 or extra round-trip fails the build rather than slipping into production.
An empty folder to a running, tested API: two entities, a joined relationship, four routes, and a repository, with no persistence context, no proxies, and no N+1. From here:
@@ -233,8 +259,8 @@ ${navHtml('tutorials')} diff --git a/website/src/pages/tutorials/exposed-entities.js b/website/src/pages/tutorials/exposed-entities.js index 04a30ec07..82a34f235 100644 --- a/website/src/pages/tutorials/exposed-entities.js +++ b/website/src/pages/tutorials/exposed-entities.js @@ -150,7 +150,7 @@ ${navHtml('tutorials')} diff --git a/website/src/pages/tutorials/exposed-ktor.js b/website/src/pages/tutorials/exposed-ktor.js index 9a248e568..2f75c3cad 100644 --- a/website/src/pages/tutorials/exposed-ktor.js +++ b/website/src/pages/tutorials/exposed-ktor.js @@ -102,7 +102,7 @@ ${navHtml('tutorials')} diff --git a/website/src/pages/tutorials/exposed-n-plus-one.js b/website/src/pages/tutorials/exposed-n-plus-one.js index f27d1c6d1..3fab1e34b 100644 --- a/website/src/pages/tutorials/exposed-n-plus-one.js +++ b/website/src/pages/tutorials/exposed-n-plus-one.js @@ -124,7 +124,7 @@ ${navHtml('tutorials')} diff --git a/website/src/pages/tutorials/exposed-queries.js b/website/src/pages/tutorials/exposed-queries.js index db44bf47f..7969a3b0a 100644 --- a/website/src/pages/tutorials/exposed-queries.js +++ b/website/src/pages/tutorials/exposed-queries.js @@ -123,7 +123,7 @@ ${navHtml('tutorials')} diff --git a/website/src/pages/tutorials/exposed-schema.js b/website/src/pages/tutorials/exposed-schema.js index 50d25d123..4251d3e74 100644 --- a/website/src/pages/tutorials/exposed-schema.js +++ b/website/src/pages/tutorials/exposed-schema.js @@ -82,7 +82,7 @@ ${navHtml('tutorials')} diff --git a/website/src/pages/tutorials/exposed-transactions.js b/website/src/pages/tutorials/exposed-transactions.js index b613c6668..81fce88e9 100644 --- a/website/src/pages/tutorials/exposed-transactions.js +++ b/website/src/pages/tutorials/exposed-transactions.js @@ -133,7 +133,7 @@ ${navHtml('tutorials')} diff --git a/website/src/pages/tutorials/exposed-upserts.js b/website/src/pages/tutorials/exposed-upserts.js index 05914a4ea..91273d2dd 100644 --- a/website/src/pages/tutorials/exposed-upserts.js +++ b/website/src/pages/tutorials/exposed-upserts.js @@ -111,7 +111,7 @@ ${navHtml('tutorials')} diff --git a/website/src/pages/tutorials/index.js b/website/src/pages/tutorials/index.js index 6080029c4..48b9aa22c 100644 --- a/website/src/pages/tutorials/index.js +++ b/website/src/pages/tutorials/index.js @@ -165,7 +165,7 @@ ${navHtml('tutorials')} Composite and natural keys Entity caching and dirty checking -Want one of these sooner, or a topic that is not listed? Open an issue on GitHub.
+Want one of these sooner, or a topic that is not listed? Open an issue on GitHub.
${FOOT_HTML} diff --git a/website/src/pages/tutorials/json-columns.js b/website/src/pages/tutorials/json-columns.js index 7fb2cdfb5..0a72498bc 100644 --- a/website/src/pages/tutorials/json-columns.js +++ b/website/src/pages/tutorials/json-columns.js @@ -100,7 +100,7 @@ ${navHtml('tutorials')} diff --git a/website/src/pages/tutorials/mapped-collections.js b/website/src/pages/tutorials/mapped-collections.js index 841e10ed1..a57bbd99f 100644 --- a/website/src/pages/tutorials/mapped-collections.js +++ b/website/src/pages/tutorials/mapped-collections.js @@ -142,7 +142,7 @@ ${navHtml('tutorials')} diff --git a/website/src/pages/tutorials/n-plus-one.js b/website/src/pages/tutorials/n-plus-one.js index 7e6806df0..ddc96168e 100644 --- a/website/src/pages/tutorials/n-plus-one.js +++ b/website/src/pages/tutorials/n-plus-one.js @@ -179,7 +179,7 @@ ${navHtml('tutorials')} diff --git a/website/src/pages/tutorials/observability.js b/website/src/pages/tutorials/observability.js index 5336a803d..eeb10d7f3 100644 --- a/website/src/pages/tutorials/observability.js +++ b/website/src/pages/tutorials/observability.js @@ -74,7 +74,7 @@ ${navHtml('tutorials')} diff --git a/website/src/pages/tutorials/optimistic-locking.js b/website/src/pages/tutorials/optimistic-locking.js index d5e0c18a7..7f81da90d 100644 --- a/website/src/pages/tutorials/optimistic-locking.js +++ b/website/src/pages/tutorials/optimistic-locking.js @@ -119,7 +119,7 @@ ${navHtml('tutorials')} diff --git a/website/src/pages/tutorials/pagination.js b/website/src/pages/tutorials/pagination.js index 49f903525..df9910f8d 100644 --- a/website/src/pages/tutorials/pagination.js +++ b/website/src/pages/tutorials/pagination.js @@ -124,7 +124,7 @@ ${navHtml('tutorials')} diff --git a/website/src/pages/tutorials/projections.js b/website/src/pages/tutorials/projections.js index e834099da..2a6667656 100644 --- a/website/src/pages/tutorials/projections.js +++ b/website/src/pages/tutorials/projections.js @@ -164,7 +164,7 @@ ${navHtml('tutorials')} diff --git a/website/src/pages/tutorials/query-results.js b/website/src/pages/tutorials/query-results.js index f0bf7a7f8..f92bfe7e1 100644 --- a/website/src/pages/tutorials/query-results.js +++ b/website/src/pages/tutorials/query-results.js @@ -139,7 +139,7 @@ ${navHtml('tutorials')} diff --git a/website/src/pages/tutorials/sealed-entities.js b/website/src/pages/tutorials/sealed-entities.js index 1bf23bc0c..475f47372 100644 --- a/website/src/pages/tutorials/sealed-entities.js +++ b/website/src/pages/tutorials/sealed-entities.js @@ -97,7 +97,7 @@ ${navHtml('tutorials')} diff --git a/website/src/pages/tutorials/sql-templates.js b/website/src/pages/tutorials/sql-templates.js index d30013049..16e972b0d 100644 --- a/website/src/pages/tutorials/sql-templates.js +++ b/website/src/pages/tutorials/sql-templates.js @@ -127,7 +127,7 @@ ${navHtml('tutorials')} diff --git a/website/src/pages/tutorials/streaming.js b/website/src/pages/tutorials/streaming.js index ebd331642..c184a22cd 100644 --- a/website/src/pages/tutorials/streaming.js +++ b/website/src/pages/tutorials/streaming.js @@ -87,7 +87,7 @@ ${navHtml('tutorials')} diff --git a/website/src/pages/tutorials/testing.js b/website/src/pages/tutorials/testing.js index 7fffc9a6b..bdf5c11fd 100644 --- a/website/src/pages/tutorials/testing.js +++ b/website/src/pages/tutorials/testing.js @@ -88,7 +88,7 @@ ${navHtml('tutorials')} diff --git a/website/src/pages/tutorials/transactions.js b/website/src/pages/tutorials/transactions.js index 6b2a47ecf..7c6b39bfe 100644 --- a/website/src/pages/tutorials/transactions.js +++ b/website/src/pages/tutorials/transactions.js @@ -145,7 +145,7 @@ ${navHtml('tutorials')} diff --git a/website/src/pages/tutorials/upserts.js b/website/src/pages/tutorials/upserts.js index 01655d6ec..841e22a1e 100644 --- a/website/src/pages/tutorials/upserts.js +++ b/website/src/pages/tutorials/upserts.js @@ -119,7 +119,7 @@ ${navHtml('tutorials')} diff --git a/website/versioned_docs/version-1.12.0/comparison.md b/website/versioned_docs/version-1.12.0/comparison.md index e6a19ddf6..2e01bdc5f 100644 --- a/website/versioned_docs/version-1.12.0/comparison.md +++ b/website/versioned_docs/version-1.12.0/comparison.md @@ -15,6 +15,7 @@ The following tables provide a side-by-side comparison of concrete features acro |---------|-------|-----|-------------|---------|------|------|--------|---------|-------| | Lines per entity | ~5 | ~301 | ~301 | ~20+ | Generated | ~15 | ~10 | ~12 | ~15 | | Immutable entities | Yes | No | No | Yes | Yes | Yes | Yes | DSL only | No | +| Session state | None | Persistence context | Via JPA | None | None | None | None | DAO only | Entity tracking | | Polymorphism | Yes2 | Yes | Via JPA | No | No | No | No8 | No | No | | Automatic relationships | Yes | Yes3 | Via JPA | No | No | No | Yes | DAO only | No | | Cascade persist | No | Yes | Yes | No | No | No | Yes | No | No | @@ -33,8 +34,9 @@ The following tables provide a side-by-side comparison of concrete features acro | Feature | Storm | JPA | Spring Data | MyBatis | jOOQ | JDBI | Jimmer | Exposed | Ktorm | |---------|-------|-----|-------------|---------|------|------|--------|---------|-------| | Type-safe queries | Yes | Criteria | No | No | Yes | No | Yes | Yes | Yes | -| SQL Templates | Yes | No | No | XML/Ann | Yes | Yes | Native9 | No | No | +| SQL / SQL templates | Yes | Native queries | Via JPA | XML/Ann | Yes | Yes | Native9 | exec() | Raw JDBC | | N+1 prevention | Yes | No | No | No | Manual | Manual | Yes | No | No | +| Query across relations | One line | JPQL/Criteria | Derived/JPQL | Manual SQL | Path joins | Manual SQL | Implicit joins | Manual joins | Manual joins | | Lazy loading | Refs | Yes | Yes | No | No | No | Fetchers | Yes | Yes | | Scrolling | Yes | No | Yes | No | Yes | No | No | No | No | | JSON columns | Yes | Yes4 | Via JPA | Manual | Yes | Module | Yes | Yes | Module |