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.
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.
npx @storm-orm/cli init
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.
// build.gradle.kts plugins { kotlin("jvm") version "2.0.21" id("com.google.devtools.ksp") version "2.0.21-1.0.28" } dependencies { implementation(platform("st.orm:storm-bom:1.12.0")) implementation("st.orm:storm-kotlin") runtimeOnly("st.orm:storm-core") runtimeOnly("st.orm:storm-h2") // zero-setup in-memory database runtimeOnly("com.h2database:h2:2.2.224") ksp("st.orm:storm-metamodel-ksp") kotlinCompilerPluginClasspath("st.orm:storm-compiler-plugin-2.0") }
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.
// Movie.kt — a plain data class. This is the whole entity. data class Movie( @PK val id: Int = 0, val title: String, val year: Int, val rating: Double, ) : Entity<Int>
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().
-- create the table with your migration tool, or run this once CREATE TABLE movie ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200), year INT, rating DOUBLE );
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.
// Open an ORM on any JDBC DataSource. Thread-safe; create it once. val orm = dataSource.orm // A repository for Movie: every CRUD method included, nothing to implement. val movies = orm.entity<Movie>() // Insert returns a copy with the generated id. val saved = orm insert Movie(title = "Dune: Part Two", year = 2024, rating = 8.5) // One type-safe line. Movie_ is generated at compile time, so a typo fails to compile. val recent = movies.findAll(Movie_.year eq 2024)
-- orm insert Movie(...) INSERT INTO movie (title, year, rating) VALUES (?, ?, ?) -- movies.findAll(Movie_.year eq 2024) SELECT m.id, m.title, m.year, m.rating FROM movie m WHERE m.year = ?
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.
You have the whole shape of Storm in three steps. From here: