Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,11 @@ storm-framework/
├── storm-spring/ # Spring integration (Java)
├── storm-kotlin-spring-boot-starter/ # Spring Boot starter (Kotlin)
├── storm-spring-boot-starter/ # Spring Boot starter (Java)
├── storm-spring-boot-test-autoconfigure/ # @DataStormTest test slice (both starters)
├── storm-ktor/ # Ktor integration (Kotlin)
├── storm-ktor-test/ # Ktor integration test support
├── storm-micrometer/ # Micrometer Observations binding
├── storm-gradle-plugin/ # Gradle plugin (id("st.orm"))
├── storm-kotlinx-serialization/ # kotlinx.serialization support
├── storm-jackson2/ # Jackson 2.x JSON support
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,10 @@ Storm is modular; you add only what your stack uses. Versions come from the BOM
| **Java + Spring Boot** | `storm-spring-boot-starter` |
| **+ your database** | one dialect (runtime): `storm-postgresql`, `storm-mysql`, `storm-mariadb`, `storm-oracle`, `storm-mssqlserver`, `storm-sqlite`, `storm-h2` |
| **+ JSON columns** | `storm-jackson2`, `storm-jackson3`, or `storm-kotlinx-serialization` |
| **+ metrics & tracing** | `storm-micrometer` (included in the Spring Boot starters) |
| **+ Spring Boot test slice** | `storm-spring-boot-test-autoconfigure` (test scope, `@DataStormTest`) |

The `storm-compiler-plugin-2.x` suffix matches your Kotlin major.minor version (e.g. `storm-compiler-plugin-2.0` for Kotlin 2.0.x). See [Installation](https://orm.st/docs/installation) for the full module overview.
The `storm-compiler-plugin-2.x` suffix matches your Kotlin major.minor version (e.g. `storm-compiler-plugin-2.0` for Kotlin 2.0.x). Gradle projects can skip the per-module setup entirely with the Storm Gradle plugin: `id("st.orm")` applies the BOM, core dependencies, metamodel processing, and compiler flags in one block. See [Installation](https://orm.st/docs/installation) for the full module overview.

### Dependency Management (BOM)

Expand Down
2 changes: 1 addition & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -646,7 +646,7 @@ Storm's Kotlin API uses the Storm compiler plugin to automatically wrap string i
See [String Templates](string-templates.md) for setup instructions for the compiler plugin.

:::tip Monitoring
Storm exposes runtime metrics for template compilation, dirty checking, and entity cache behavior through JMX MBeans. See [Metrics](metrics.md) for details.
Storm exposes runtime metrics for template compilation, dirty checking, and entity cache behavior through JMX MBeans; see [Metrics](metrics.md). For query and transaction metrics and tracing through your observability stack, add the `storm-micrometer` module (included in the Spring Boot starters).
:::

---
Expand Down
17 changes: 17 additions & 0 deletions docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,23 @@ Storm supports storing and reading JSON-typed columns. Pick the module that matc

See [JSON Support](json.md) for usage details.

### Observability

| Module | Provides |
|--------|----------|
| `storm-micrometer` | Micrometer Observations for queries and transactions (`storm.query`, `storm.transaction`), the OpenTelemetry database semantic conventions, and trace-context SQL comments |

The Spring Boot starters include `storm-micrometer`; Ktor applications add it explicitly. See the observability sections of [Spring Integration](spring-integration.md#observability) and [Ktor Integration](ktor-integration.md#observability).

### Testing

| Module | Provides |
|--------|----------|
| `storm-test` | `@StormTest` JUnit 5 extension and `SqlCapture`, framework-free |
| `storm-spring-boot-test-autoconfigure` | The `@DataStormTest` Spring Boot test slice (test scope) |

See [Testing](testing.md) and [Testing with @DataStormTest](spring-integration.md#testing-with-datastormtest).

## Module Overview

The following diagram shows how Storm's modules relate to each other. You only need the modules relevant to your language and integration choices.
Expand Down
4 changes: 4 additions & 0 deletions docs/metrics.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import TabItem from '@theme/TabItem';

Storm exposes runtime metrics through JMX (Java Management Extensions) MBeans. These metrics give you visibility into template compilation performance, dirty checking behavior, and entity cache efficiency. All MBeans are registered automatically when Storm initializes and aggregate across all `ORMTemplate` instances in the JVM.

:::tip Micrometer Observations
The metrics on this page are JMX-based and always on. For metrics and tracing through your observability stack, the `storm-micrometer` module reports every query and transaction as Micrometer Observations; see the observability sections of [Spring Integration](spring-integration.md#observability) and [Ktor Integration](ktor-integration.md#observability).
:::

To view these metrics, connect to the JVM with any JMX client (JConsole, VisualVM, or your monitoring platform) and navigate to the `st.orm` domain. If your application uses Spring Boot Actuator, the MBeans are also accessible through Actuator's JMX endpoint.

---
Expand Down
4 changes: 2 additions & 2 deletions docs/repositories.md
Original file line number Diff line number Diff line change
Expand Up @@ -581,8 +581,8 @@ Storm repositories are plain interfaces, so Spring cannot discover them through
@Configuration
class AcmeRepositoryBeanFactoryPostProcessor : RepositoryBeanFactoryPostProcessor() {

override val repositoryBasePackages: Array<String>
get() = arrayOf("com.acme.repository")
override fun getRepositoryBasePackages(): Array<String> =
arrayOf("com.acme.repository")
}
```

Expand Down
28 changes: 14 additions & 14 deletions website/src/pages/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const CSS = `
.storm-home .btn.primary.go{background:linear-gradient(100deg,#a78bfa,#818cf8 50%,#7dd3fc);border-color:transparent}
.storm-home .btn.primary.go:hover{filter:brightness(1.12)}

/* hero left aligned */
/* hero: left aligned */
.storm-home header{padding:46px 0 34px}
.storm-home h1{font-size:clamp(42px,7vw,78px);line-height:.97;letter-spacing:-.04em;font-weight:800;margin:0}
.storm-home .grad{background:linear-gradient(100deg,#a78bfa,#818cf8 50%,#7dd3fc);-webkit-background-clip:text;background-clip:text;color:transparent}
Expand Down Expand Up @@ -359,15 +359,15 @@ export default function Home() {
caption:"one-line queries to get all the data you need · no N+1",
code:[ C("// A user's city is loaded in the same query.\n"),
K("val "),P("user = userRepository."),F("getById"),P("("),N("1"),P(")\n"),
K("val "),P("cityName = user.city.name"),P(" "),C('// already loaded no N+1, no lazy-init\n\n'),
K("val "),P("cityName = user.city.name"),P(" "),C('// already loaded: no N+1, no lazy-init\n\n'),
C("// Filter across the graph, fully type-safe using the static metamodel.\n"),
K("val "),P("users = userRepository."),F("findAll"),P("(User_.city.name "),K("eq "),S('"Sunnyvale"'),P(")") ] },

{ name:'3 · repository', file:'UserRepository.kt',
caption:"your own type-safe queries · CRUD inherited",
code:[ C("// Custom return types are just records — define them in-place.\n"),
code:[ C("// Custom return types are just records. Define them in-place.\n"),
K("data class "),T("CityCount"),P("("),K("val "),P("city: "),T("City"),P(", "),K("val "),P("count: "),T("Long"),P(")\n\n"),
C("// Extend EntityRepository all CRUD comes for free.\n"),
C("// Extend EntityRepository: all CRUD comes for free.\n"),
K("interface "),T("UserRepository"),P(" : "),T("EntityRepository"),P("<"),T("User"),P(", "),T("Int"),P("> {\n"),
P(" "),K("fun "),F("findByCity"),P("(city: "),T("City"),P(") = "),F("findAll"),P("(User_.city "),K("eq "),P("city)\n\n"),
C(" // Query builder with SQL templates for the aggregate.\n"),
Expand All @@ -386,7 +386,7 @@ export default function Home() {
P(" orm "),K("insert "),T("User"),P("(email = "),S('"bob@acme.io"'),P(", name = "),S('"Bob"'),P(", city = city)\n"),
P("}\n"),
P("\n"),
C("// Full control when you need it: propagation, isolation, timeout plus post-tx hooks.\n"),
C("// Full control when you need it: propagation, isolation, timeout, plus post-tx hooks.\n"),
F("transaction"),P("(propagation = "),T("REQUIRES_NEW"),P(", isolation = "),T("REPEATABLE_READ"),P(", timeoutSeconds = "),N("5"),P(") {\n"),
P(" "),K("val "),P("city = orm "),K("insert "),T("City"),P("(name = "),S('"San Jose"'),P(", population = "),N("1_013_240"),P(", country = "),S('"US"'),P(")\n"),
P(" "),K("val "),P("user = orm "),K("insert "),T("User"),P("(email = "),S('"alice@acme.io"'),P(", name = "),S('"Alice"'),P(", city = city)\n"),
Expand All @@ -395,7 +395,7 @@ export default function Home() {
P("}") ] },

{ name:'5 · sql', file:'UserService.kt',
caption:"full SQL when you want it never locked in",
caption:"full SQL when you want it, never locked in",
code:[ C("// Full control of SQL, with typed columns and tables; rows map to any data class.\n"),
K("data class "),T("RankedCity"),P("("),K("val "),P("name: "),T("String"),P(", "),K("val "),P("rank: "),T("Long"),P(")\n\n"),
K("val "),P("ranked = orm."),F("query"),P(" { "),S('"""'),P("\n"),
Expand Down Expand Up @@ -430,9 +430,9 @@ export default function Home() {

// Generated SQL per scene (index-aligned with SCENES). Shown via the "Show SQL" toggle.
const SQL=[
null, // entities no query, so the Show SQL button is hidden for this scene
null, // entities: no query, so the Show SQL button is hidden for this scene

'<span class="sqlc">-- getById(1) joins the city graph, no N+1</span>\n'+
'<span class="sqlc">-- getById(1): joins the city graph, no N+1</span>\n'+
'<span class="sqlk">SELECT</span> u.id, u.email, u.name, c.id, c.name, c.population, c.country\n'+
'<span class="sqlk">FROM</span> "user" u\n'+
'<span class="sqlk">INNER JOIN</span> city c <span class="sqlk">ON</span> u.city_id = c.id\n'+
Expand Down Expand Up @@ -482,7 +482,7 @@ export default function Home() {
benefitsEl=document.getElementById('benefits'), codeareaEl=document.querySelector('.storm-home .codearea');
if(!codeEl) return;

// "Show SQL" toggle reveals the generated SQL for the current scene.
// "Show SQL" toggle: reveals the generated SQL for the current scene.
const editorEl=document.querySelector('.storm-home .editor'), sqlBtn=document.getElementById('sqlbtn'),
sqlBtnText=document.getElementById('sqlbtntext'), sqlPanel=document.getElementById('sqlpanel');
let showSql=false, curIdx=0;
Expand Down Expand Up @@ -546,7 +546,7 @@ export default function Home() {
fnameEl.textContent=sc.file;
statusEl.classList.remove('show');

// Benefits scene render the core-benefits grid instead of typed code.
// Benefits scene: render the core-benefits grid instead of typed code.
if(sc.grid){
codeareaEl.classList.add('show-benefits');
benefitsEl.innerHTML=sc.grid.map(b=>'<div class="bcell"><span class="bt">'+b.t+'</span><span class="bd">'+b.d+'</span></div>').join('');
Expand All @@ -573,7 +573,7 @@ export default function Home() {
// Rewind the horizontal scroller before typing. Desktop resets it for
// free (the near-empty first render collapses scrollWidth, clamping
// scrollLeft to 0), but iOS Safari keeps the stale offset when content
// shrinks so one touch pan (even an accidental diagonal swipe while
// shrinks, so one touch pan (even an accidental diagonal swipe while
// scrolling the page) would leave every later scene with the start of
// each line cut off.
codeEl.scrollLeft=0;
Expand Down Expand Up @@ -715,9 +715,9 @@ export default function Home() {
description:
'Storm is a type-safe, SQL-first Kotlin ORM. Define concise, immutable data-class entities and write one-line queries. Nested predicates and entity graphs compile to a single efficient query, eliminating accidental hidden N+1 queries. Drop to full SQL templates whenever you want; never locked in.',
featureList: [
'Direct database control every query explicit, no hidden N+1',
'Stateless, immutable records no proxies, no flush, no hidden state',
'Type-safe and injection-safe compile-time column and type checks, automatic bind parameters',
'Direct database control: every query explicit, no hidden N+1',
'Stateless, immutable records: no proxies, no flush, no hidden state',
'Type-safe and injection-safe: compile-time column and type checks, automatic bind parameters',
'One-line queries with an optional full SQL template engine',
'Works with PostgreSQL, MySQL, MariaDB, Oracle, SQL Server, SQLite and H2',
'Integrates with Spring Boot 3.x/4.x and Ktor',
Expand Down
2 changes: 1 addition & 1 deletion website/src/pages/quickstart.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ function buildBody(version) {
P('}');

const entity =
C('// Entities.kt plain data classes. This is the whole data layer.\n') +
C('// Entities.kt: plain data classes. This is the whole data layer.\n') +
K('data class ') + T('Director') + P('(\n') +
P(' ') + A('@PK') + P(' ') + K('val ') + P('id: ') + T('Int') + P(' = ') + N('0') + P(',\n') +
P(' ') + K('val ') + P('name: ') + T('String') + P(',\n') +
Expand Down
6 changes: 3 additions & 3 deletions website/src/pages/tutorials/build-a-rest-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ function buildBody(version) {
P(' ') + A('@PK') + P(' ') + K('val ') + P('id: ') + T('Int') + P(' = ') + N('0') + P(',\n') +
P(' ') + K('val ') + P('name: ') + T('String') + P(',\n') +
P(') : ') + T('Entity') + P('<') + T('Int') + P('>\n\n') +
C('// Bookmark.kt @FK means the folder is loaded in the same query\n') +
C('// Bookmark.kt: @FK means the folder is loaded in the same query\n') +
K('data class ') + T('Bookmark') + P('(\n') +
P(' ') + A('@PK') + P(' ') + K('val ') + P('id: ') + T('Int') + P(' = ') + N('0') + P(',\n') +
P(' ') + K('val ') + P('url: ') + T('String') + P(',\n') +
Expand All @@ -99,7 +99,7 @@ function buildBody(version) {
P('}');

const repo =
C('// Repositories.kt CRUD is inherited; add only your own queries\n') +
C('// Repositories.kt: CRUD is inherited; add only your own queries\n') +
K('interface ') + T('BookmarkRepository') + P(' : ') + T('EntityRepository') + P('<') + T('Bookmark') + P(', ') + T('Int') + P('> {\n') +
P(' ') + K('fun ') + F('findByFolderName') + P('(name: ') + T('String') + P('): ') + T('List') + P('<') + T('Bookmark') + P('> =\n') +
P(' ') + F('findAll') + P('(Bookmark_.folder.name ') + K('eq') + P(' name)\n') +
Expand Down Expand Up @@ -166,7 +166,7 @@ function buildBody(version) {
P('curl -s -X POST localhost:8080/bookmarks \\\n') +
P(" -H 'content-type: application/json' \\\n") +
P(' -d ') + S('\'{"url":"https://orm.st","title":"Storm","folderId":1}\'') + P('\n\n') +
C('# list them back each bookmark includes its folder object\n') +
C('# list them back: each bookmark includes its folder object\n') +
P('curl -s localhost:8080/bookmarks');

const test =
Expand Down
2 changes: 1 addition & 1 deletion website/src/pages/tutorials/exposed-ktor.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ ${navHtml('tutorials')}
<p>Repositories are stateless, so each read borrows a pooled connection just for the query; there is no transaction wrapper because none is needed. For quick endpoints, bare <code>entity&lt;User&gt;()</code> and <code>projection&lt;T&gt;()</code> extensions skip even the interface. Transactions appear where atomicity matters, and because they are suspend functions propagated through the coroutine context, they compose with Ktor naturally:</p>
${editor({file: 'Routes.kt', tag: 'Kotlin · Storm + Ktor', code: CODE_STORM_WRITE})}
<p>The <code>onCommit</code> callback keeps the event publication out of the transaction, so a rollback never announces an order that does not exist. See the <a class="tlink" href="/tutorials/exposed-transactions">transactions comparison</a> for the full mapping.</p>
<p>For applications with a service layer, the optional <code>storm-ktor-koin</code> module bridges the same repositories into Koin: <code>stormModule()</code> exposes each one under its own interface type, so services declare repositories as constructor parameters and register with <code>singleOf(::OrderService)</code>.</p>
<p>For applications with a service layer, the plugin exposes the template and every repository through Ktor's built-in dependency injection, each under its own interface type: services resolve them with <code>by dependencies</code>. Koin users keep the same wiring with a few lines of application code; the integration docs include the recipe.</p>

<h2><span class="hno">04</span>The translation table</h2>
<table class="cmp">
Expand Down
23 changes: 15 additions & 8 deletions website/static/skills/storm-repository-java.md
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,7 @@ var window = users.scroll(scrollable);
## Framework-Specific Repository Registration

### Spring Boot
With `storm-spring-boot-starter`, repository interfaces are auto-discovered and registered as beans — no configuration needed; just inject them. Only when using plain `storm-spring` (no starter) do you define a `RepositoryBeanFactoryPostProcessor` with `repositoryBasePackages`:
With `storm-spring-boot-starter`, repository interfaces are auto-discovered and registered as beans — no configuration needed; just inject them. Only when using plain `storm-spring` (no starter) do you switch scanning on with `@EnableStormRepositories(basePackages = ...)`, or define `RepositoryBeanFactoryPostProcessor(basePackages, ormTemplateBeanName, repositoryPrefix)` beans when multiple repository sets bind to different templates:
```java
@Service
public class UserService {
Expand All @@ -368,10 +368,21 @@ UserRepository userRepository = orm.repository(UserRepository.class);

## Transactions

The Java API has **no Storm-managed transaction API** (the `transaction { }` blocks are Kotlin-only). Do not invent methods like `orm.transaction(...)` — they do not exist.
Since 1.13, the Java API has a full programmatic transaction API: `Transactions.transaction(...)` with the same semantics as Kotlin's `transaction { }` blocks — all seven propagation modes, isolation, timeout, read-only, rollback-only, and commit/rollback callbacks. The block is value-returning (void blocks `return null`), checked exceptions propagate to the caller unchanged, and the transaction binds to the first `ORMTemplate` that executes inside it:
```java
import static st.orm.template.Transactions.transaction;
import static st.orm.TransactionPropagation.REQUIRES_NEW;

User created = transaction(tx -> userRepository.insertAndFetch(new User(null, email, "Alice", city)));

transaction(REQUIRES_NEW, tx -> {
tx.onCommit(() -> log.info("audit committed"));
return auditRepository.insertAndFetch(entry);
});
```

### Spring Boot
Use `@Transactional` on service methods (standard Spring):
Both styles work and cooperate. `@Transactional` on service methods remains first-class, and Storm's `transaction(...)` blocks run through Spring's transaction managers when the template is Spring-composed (the starter does this automatically): a Storm block inside a `@Transactional` method joins it.
```java
@Service
public class UserService {
Expand All @@ -383,11 +394,7 @@ public class UserService {
```

### Standalone
Without Spring, manage transactions at the JDBC level: wrap the `DataSource` in a
transaction-aware proxy or use a library that provides one (e.g. Spring's
`DataSourceTransactionManager`/`TransactionTemplate` with
`TransactionAwareDataSourceProxy`). Storm participates in whatever transaction is
active on the connection it obtains from the `DataSource`.
Without Spring, `Transactions.transaction(...)` manages real JDBC transactions directly on the template's `DataSource` — no framework transaction manager is involved, and the blocking API is virtual-thread friendly.

## Verification

Expand Down
Loading
Loading