Home/Quickstart

Zero to Storm
in 5 minutes.

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.

Kotlin~5 minJDK 21+

1Set up

Apply the Storm Gradle plugin. It imports the BOM and wires the core dependencies, the metamodel processor, and the Kotlin compiler plugin, so all that is left for a runnable project is an in-memory H2 database.

build.gradle.kts Gradle · Kotlin DSL Copy
1
2
3
4
5
6
7
8
9
10
11
// build.gradle.kts
plugins {
    kotlin("jvm") version "2.0.21"
    id("com.google.devtools.ksp") version "2.0.21-1.0.28"
    id("st.orm") version "1.13.1"
}

dependencies {
    runtimeOnly("st.orm:storm-h2")          // zero-setup in-memory database
    runtimeOnly("com.h2database:h2:2.3.232")
}
1
2
3
4
5
6
7
8
9
10
11
// build.gradle.kts
plugins {
    kotlin("jvm") version "2.1.21"
    id("com.google.devtools.ksp") version "2.1.21-2.0.2"
    id("st.orm") version "1.13.1"
}

dependencies {
    runtimeOnly("st.orm:storm-h2")          // zero-setup in-memory database
    runtimeOnly("com.h2database:h2:2.3.232")
}
1
2
3
4
5
6
7
8
9
10
11
// build.gradle.kts
plugins {
    kotlin("jvm") version "2.2.21"
    id("com.google.devtools.ksp") version "2.2.21-2.0.5"
    id("st.orm") version "1.13.1"
}

dependencies {
    runtimeOnly("st.orm:storm-h2")          // zero-setup in-memory database
    runtimeOnly("com.h2database:h2:2.3.232")
}
1
2
3
4
5
6
7
8
9
10
11
// build.gradle.kts
plugins {
    kotlin("jvm") version "2.3.21"
    id("com.google.devtools.ksp") version "2.3.10"
    id("st.orm") version "1.13.1"
}

dependencies {
    runtimeOnly("st.orm:storm-h2")          // zero-setup in-memory database
    runtimeOnly("com.h2database:h2:2.3.232")
}
1
2
3
4
5
6
7
8
9
10
11
// build.gradle.kts
plugins {
    kotlin("jvm") version "2.4.0"
    id("com.google.devtools.ksp") version "2.3.10"
    id("st.orm") version "1.13.1"
}

dependencies {
    runtimeOnly("st.orm:storm-h2")          // zero-setup in-memory database
    runtimeOnly("com.h2database:h2:2.3.232")
}

The plugin generates the type-safe metamodel (Movie_) through KSP and makes SQL templates injection-safe by default through the compiler plugin, with no extra dependencies to declare. On a real database, swap storm-h2 for your dialect and add its JDBC driver. See the installation guide for all options.

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.

terminal shell Copy
1
2
# from the root of your project's workspace
npx @storm-orm/cli init

2Define two linked entities

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.

Entities.kt Kotlin Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Entities.kt: plain data classes. This is the whole data layer.
data class Director(
    @PK val id: Int = 0,
    val name: String,
) : Entity<Int>

// A relation is a @FK field typed as the entity it points to.
data class Movie(
    @PK val id: Int = 0,
    val title: String,
    val year: Int,
    val rating: Double,
    @FK val director: Director,
) : Entity<Int>

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().

schema.sql SQL Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
-- create the tables with your migration tool, or run this once
CREATE TABLE director (
    id    INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    name  VARCHAR(100)
);

CREATE TABLE movie (
    id           INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    title        VARCHAR(200),
    year         INT,
    rating       DOUBLE PRECISION,
    director_id  INT REFERENCES director(id)
);
1
2
3
4
5
6
7
8
9
10
11
12
13
-- create the tables with your migration tool, or run this once
CREATE TABLE director (
    id    INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    name  VARCHAR(100)
);

CREATE TABLE movie (
    id           INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    title        VARCHAR(200),
    year         INT,
    rating       DOUBLE PRECISION,
    director_id  INT REFERENCES director(id)
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- create the tables with your migration tool, or run this once
CREATE TABLE director (
    id    INT PRIMARY KEY AUTO_INCREMENT,
    name  VARCHAR(100)
);

CREATE TABLE movie (
    id           INT PRIMARY KEY AUTO_INCREMENT,
    title        VARCHAR(200),
    year         INT,
    rating       DOUBLE,
    director_id  INT,
    FOREIGN KEY (director_id) REFERENCES director(id)
);
1
2
3
4
5
6
7
8
9
10
11
12
13
-- create the tables with your migration tool, or run this once
CREATE TABLE director (
    id    NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    name  VARCHAR2(100)
);

CREATE TABLE movie (
    id           NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    title        VARCHAR2(200),
    year         NUMBER,
    rating       BINARY_DOUBLE,
    director_id  NUMBER REFERENCES director(id)
);
1
2
3
4
5
6
7
8
9
10
11
12
13
-- create the tables with your migration tool, or run this once
CREATE TABLE director (
    id    INT IDENTITY PRIMARY KEY,
    name  VARCHAR(100)
);

CREATE TABLE movie (
    id           INT IDENTITY PRIMARY KEY,
    title        VARCHAR(200),
    year         INT,
    rating       FLOAT,
    director_id  INT REFERENCES director(id)
);
1
2
3
4
5
6
7
8
9
10
11
12
13
-- create the tables with your migration tool, or run this once
CREATE TABLE director (
    id    INTEGER PRIMARY KEY,
    name  TEXT
);

CREATE TABLE movie (
    id           INTEGER PRIMARY KEY,
    title        TEXT,
    year         INTEGER,
    rating       REAL,
    director_id  INTEGER REFERENCES director(id)
);

3Query across the relation

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.

Main.kt Kotlin Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Open an ORM on any JDBC DataSource. Thread-safe; create it once.
val orm = dataSource.orm

// Insert returns a copy with the generated id.
val nolan = orm insert Director(name = "Christopher Nolan")
val villeneuve = orm insert Director(name = "Denis Villeneuve")

orm insert Movie(title = "Interstellar", year = 2014, rating = 8.7, director = nolan)
orm insert Movie(title = "Oppenheimer", year = 2023, rating = 8.3, director = nolan)
orm insert Movie(title = "Dune: Part Two", year = 2024, rating = 8.5, director = villeneuve)

// A repository for Movie: every CRUD method included, nothing to implement.
val movies = orm.entity<Movie>()

// One type-safe line across the relation. Movie_ is generated at compile time,
// so a typo in the path fails to compile.
val byChris = movies.findAll(Movie_.director.name like "Chris%")

// The director is loaded in the same query: no proxies, no N+1.
val director = byChris.first().director.name
generated sql
-- orm insert Movie(...)
INSERT INTO movie (title, year, rating, director_id) VALUES (?, ?, ?, ?)

-- movies.findAll(Movie_.director.name like "Chris%")
-- one statement, join included: the whole graph, no N+1
SELECT m.id, m.title, m.year, m.rating, d.id, d.name
FROM movie m
INNER JOIN director d ON m.director_id = d.id
WHERE d.name LIKE ?

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.

4Where to next

You have the whole shape of Storm in three steps. From here:

Build a real app →

Something not work, or not make sense? Tell us on Discord.