From 7e8e3e3b333a51d29266966aed760335a59bf9fc Mon Sep 17 00:00:00 2001 From: Leon van Zantvoort Date: Wed, 15 Jul 2026 13:22:43 +0200 Subject: [PATCH 1/2] docs(website): website improvements --- CHANGELOG.md | 88 ++++++++++----------------------- docs/comparison.md | 37 +++++++------- docs/ktor-integration.md | 4 +- docs/transactions.md | 4 +- website/src/pages/benchmarks.js | 2 +- website/src/pages/comparison.js | 4 +- website/static/llms-full.txt | 78 +++++++++++++++-------------- 7 files changed, 94 insertions(+), 123 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8dcd06be8..cbb020ce7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,69 +10,29 @@ for the CLI, to [npm](https://www.npmjs.com/package/@storm-orm/cli) (`@storm-orm/cli`). Full release notes for every version are on the [GitHub Releases](https://github.com/storm-orm/storm-framework/releases) page. -## [Unreleased] - -Framework integration points are now instance-scoped (#198), and the Ktor integration standardizes on Ktor's built-in dependency injection (#206). Breaking changes are accepted with no deprecation shims. - -### Added - -- `ORMTemplate.builder(dataSource)` / `builder(connection)` (core, Java 21 and Kotlin APIs) with instance-scoped integration strategies: `connectionProvider`, `transactionTemplateProvider`, `exceptionMapper` and `queryObserver`. `ServiceLoader` discovery remains the fallback for templates built without explicit strategies. -- `ExceptionMapper` SPI: maps failures raised during query execution to the exception thrown to the caller, enabling platform hierarchies such as Spring's `DataAccessException` (the translation itself lands with #199). -- `QueryObserver` SPI: observes query executions (operation, entity/projection type, execution kind, timing, outcome) for metrics and tracing bindings (the Spring Boot auto-configuration lands with #203). -- `springOrmTemplate(dataSource) { transactionManagers }` (storm-kotlin-spring): the canonical plain-Spring composition, replacing `@EnableTransactionIntegration`. -- `st.orm.spring.SpringTransactionTemplateProvider` (storm-spring): gives Java applications transaction-scoped entity caching under Spring-managed transactions without reflective probes. -- The Ktor plugin gains `connectionProvider`, `transactionTemplateProvider`, `exceptionMapper` and `queryObserver` slots on `install(Storm) { }`. -- The Ktor plugin exposes the `ORMTemplate` and every registered repository through Ktor's built-in dependency injection (`ktor-server-di`), each repository under its own interface type: `val visits: VisitRepository by dependencies`. Disable with `registerDependencies = false`. -- The Ktor plugin supports multiple databases: `database("name") { }` blocks declare additional databases with their own template, repositories, schema validation, migration hook and lifecycle, configured in code or under `storm.databases..*` in HOCON. The packages declared per database partition repositories and schema validation; access goes through `orm("name")`, `repository("name")` and named dependency injection. -- New `storm-micrometer` module: `MicrometerQueryObserver` reports query executions as Micrometer Observations named `storm.query`, with low-cardinality key values for the operation, execution kind and data type, and the SQL statement as a high-cardinality value for trace handlers. Naming and key values are overridable via a custom `ObservationConvention`. -- The Ktor plugin binds query observations automatically: register an `ObservationRegistry` in the dependency container and every query is observed, tagged `storm.database=` (`primary` for the primary database). An explicit `queryObserver` takes precedence; without a registry, queries run unobserved. -- `transactional { }` route DSL (storm-ktor): every route declared inside the block runs in its own transaction, opened before the handler, committed on completion and rolled back on exception, with the same options as `transaction { }`. The transaction binds to the first template the handler touches, so named databases work unchanged. -- Storm Gradle plugin (`id("st.orm")`, published to the Gradle Plugin Portal, version-aligned with the BOM): one plugin application imports the BOM, adds the core dependencies for the Kotlin or Java path, wires the metamodel processor (KSP or annotation processor), selects the Kotlin compiler-plugin variant matching the project's Kotlin version, and sets the Java preview flags. A `storm { }` extension covers opt-outs and the variant override. -- Programmatic transactions for Java: `Transactions.transaction(...)` in storm-java21 with the same semantics as Kotlin's `transaction { }` — all seven propagation modes, isolation, timeout, read-only, rollback-only, and commit/rollback callbacks — blocking and virtual-thread friendly, with checked exceptions propagating to the caller unchanged. Options via the `st.orm.TransactionOptions` record; global and thread-scoped defaults via `setGlobalTransactionOptions` / `withTransactionOptions`. -- The transaction vocabulary is now shared by both language APIs from storm-foundation: `st.orm.TransactionPropagation`, `st.orm.TransactionIsolation`, `st.orm.TransactionTimedOutException`, `st.orm.UnexpectedRollbackException`, and the language-neutral `st.orm.Transaction` handle (the Kotlin `Transaction` extends it with suspend callback overloads). -- The Ktor integration docs gain an Error Handling section with StatusPages recipes mapping Storm exceptions to HTTP responses (missing row to 404, constraint violation and optimistic lock to 409), including portable SQL-state-based constraint detection. -- `Sql.dataType()`: the primary entity or projection type of a statement, now also derived for SELECT statements from the selected or queried table, and reported to query observers as the statement's data type. -- Java Spring applications gain the full programmatic-transaction bridge: `SpringTransactionTemplateProvider` constructed with the application's transaction managers runs Storm's `Transactions.transaction(...)` blocks through Spring's `PlatformTransactionManager`, joins active `@Transactional` transactions, and picks the manager matching each template's `DataSource` in multi-data-source applications. `SpringOrmTemplate.of(dataSource, transactionManagers)` is the canonical plain-Spring composition for Java. -- Shared Spring Boot auto-configurations in storm-spring (`st.orm.spring.boot`): `StormTransactionAutoConfiguration` (Spring-aware provider beans), `StormValidationAutoConfiguration` (startup schema validation), and the `storm.*` configuration properties (`StormProperties`), used by both starters. Configuration keys are unchanged. -- SQL failures raised by Storm translate to Spring's `DataAccessException` hierarchy in Spring applications: `SpringExceptionMapper` (storm-spring) translates on vendor error codes with `SQLException` subclass and SQL state fallback, auto-configured by both starters (`storm.exception-translation.enabled=false` to disable) and applied by the `SpringOrmTemplate.of` / `springOrmTemplate` compositions. Failures without a `SQLException` cause keep Storm's own exceptions. -- The Spring Boot starters bind query observations automatically: with an `ObservationRegistry` bean present (Actuator provides one), every query executed by the auto-configured template reports as a `storm.query` Micrometer Observation via the shared `StormObservationAutoConfiguration`. The starters ship storm-micrometer; override the convention with an `ObservationConvention` bean, replace the binding with a `QueryObserver` bean, or disable via `management.observations.enable.storm.query=false`. -- `OtelDatabaseObservationConvention` (storm-micrometer): opt-in OpenTelemetry database client semantic conventions on query observations — `db.system.name` (derived from the JDBC URL), `db.operation.name`, and `db.query.text` (placeholders only) alongside the `storm.*` key values, so Storm queries surface in the database tooling of OTLP-capable observability backends. Activated with `storm.observations.semantic-conventions=otel` in the Spring Boot starters (database product detected from the DataSource) or by registering the convention in the Ktor dependency container. -- Trace context in SQL statements: the `SqlCommenter` hook (template builder, all language stacks) appends per-execution comment content to statements after all processing and caching; `TraceContextSqlCommenter` (storm-micrometer) renders the current span as a sqlcommenter-style W3C `traceparent` comment, correlating database-side diagnostics such as slow query logs with traces. Opt-in everywhere — `storm.tracing.sql-comments=true` in the Spring Boot starters (with a `Tracer` present), the `sqlCommenter` slot in the Ktor plugin — since a per-execution comment defeats prepared statement caching. Hostile content is rejected (comment terminator, semicolons), and the emitted comment is padded so MySQL/MariaDB executable-comment and optimizer-hint markers are never interpreted. -- Transactions are observed alongside queries: every physical transaction (outermost block or `REQUIRES_NEW`, blocking, suspend, and Spring-bridged alike) reports as a `storm.transaction` Micrometer Observation with duration, outcome (`committed`/`rolled_back`), propagation, and read-only key values; joined blocks are not double-counted. The `QueryObserver` SPI gains a default no-op transaction hook, so existing observers and wiring are unaffected. -- `@DataStormTest` provides an embedded test database on Spring Boot 4 as well: a fallback in the slice activates when Boot's own test-database replacement (relocated to spring-boot-jdbc-test-autoconfigure in Boot 4) is absent, so the slice behaves identically on both generations; `spring.test.database.replace=none` opts out for Testcontainers setups. -- Trace-context SQL comments gain a sampled-only mode: `TraceContextSqlCommenter(tracer, onlySampled)` and `storm.tracing.sql-comments=sampled` comment only statements of sampled traces, aligning the prepared-statement-caching cost with the correlation benefit; unknown property values fail fast. -- `@EnableStormRepositories(basePackages = ...)` (storm-spring, usable from both language stacks): switches on repository scanning in plain Spring applications, mirroring `@EnableJpaRepositories`; without packages, the annotated class's package is scanned. In Spring Boot the annotation doubles as the explicit override: the auto-configured scanning backs off. The `RepositoryBeanFactoryPostProcessor` adapters also gain configuring constructors (base packages, template bean name, prefix), so multi-template applications define one bean per repository set instead of one subclass. -- `@DataStormTest` test slice in the new storm-spring-boot-test-autoconfigure module, the counterpart of `@DataJpaTest`: starts only the `DataSource`, Storm's auto-configuration, and SQL initialization (plus Flyway/Liquibase when present), keeps regular components out, replaces the data source with an embedded database on Spring Boot 3 (opt out via `spring.test.database.replace=none` for Testcontainers `@ServiceConnection` setups), and rolls back a per-test transaction. One module serves both starters and both Spring Boot 3 and 4: the slice imports the starters' identically named auto-configuration classes, lists relocated platform classes as optional imports, and registers its type exclusion through a context customizer instead of the relocated `@TypeExcludeFilters`. - -### Changed - -- `transaction { }` / `transactionBlocking { }` now bind to the first template that executes inside the block: the block records the requested options, and the template's transaction provider opens the actual transaction on first use. Signatures and semantics are unchanged for single-integration applications; a block that never touches a template completes as a no-op, and mixing templates with different transaction providers in one block fails fast. -- The `TransactionTemplate` SPI is reshaped from callback-wrapping `execute()` to `open()`/`complete()` handles to support the lazy binding. -- Ambiguous `ServiceLoader` resolution of connection or transaction template providers (two enabled candidates without a defined order) now throws a descriptive error naming the candidates instead of silently picking one; provider enablement is re-evaluated per resolution instead of being frozen at first use. -- The Spring Boot starters contribute Spring-aware `ConnectionProvider`/`TransactionTemplateProvider` beans (backing off to user-defined beans) and consume optional `ExceptionMapper`/`QueryObserver` beans when creating the template. -- Ktor is upgraded from 3.1.2 to 3.2.3, the newest Ktor consumable with the project's Kotlin 2.0 toolchain (Ktor 3.3+ requires Kotlin 2.2). The `ktor.version` property now lives in the parent pom. -- The Ktor plugin's configuration class is renamed from `StormConfiguration` to `StormPluginConfig`, removing the confusion with core's `StormConfig`. Application code is unaffected unless it named the type explicitly; the `install(Storm) { }` receiver is inferred. -- The default JDBC transaction machinery moved from storm-kotlin to storm-core as Java (`st.orm.core.spi.JdbcTransactionContext`, `JdbcTransactionTemplateProviderImpl`, `JdbcConnectionProviderImpl`), replacing the core stubs that rejected transactions: core-only Java applications now get real JDBC transactions. The blocking transaction orchestration lives once in core (`TransactionRunner`), driven by both the Java API and Kotlin's `transactionBlocking { }`; the Kotlin transaction enums and exceptions moved from `st.orm.template` to `st.orm` (imports change; behavior identical). The `TransactionScope`/`TransactionTemplate` SPI now carries the typed enums instead of string/int values. -- The Spring repository scanning, autowire-candidate resolution, and AOP proxying engine is single-sourced in storm-spring (`AbstractRepositoryBeanFactoryPostProcessor`); storm-kotlin-spring now depends on storm-spring and contributes only the Kotlin bindings. The Kotlin `RepositoryBeanFactoryPostProcessor` and `springOrmTemplate` moved to `st.orm.spring.kotlin`, and Kotlin subclasses override the engine's methods instead of properties: `override val repositoryBasePackages` becomes `override fun getRepositoryBasePackages()`, likewise `getOrmTemplateBeanName()` and `getRepositoryPrefix()`. Java subclasses are unaffected. -- One starter per application: the Java and Kotlin stacks share class names (`st.orm.template.ORMTemplate`, `st.orm.repository.Repository`), so storm-spring-boot-starter and storm-kotlin-spring-boot-starter cannot be mixed in one application. The starters now share their transaction, validation, and properties auto-configurations from storm-spring. -- The auto-configured `ORMTemplate` and startup schema validation condition on a single `DataSource` candidate: applications exposing several `DataSource` beans (one pool per domain) boot cleanly with the auto-configured template backing off, or binding to the `@Primary` pool when one is marked. -- The repository AOP post-processor bean is renamed from `javaRepositoryProxyingPostProcessor`/`kotlinRepositoryProxyingPostProcessor` to `stormRepositoryProxyingPostProcessor`. -- Java applications without a `PlatformTransactionManager` no longer get a Spring-bound `ConnectionProvider`: the template falls back to Storm's own JDBC transactions (previously connections were bound through `DataSourceUtils` unconditionally). -- Spring Boot applications: SQL failures from Storm repositories and templates now surface as Spring `DataAccessException` subtypes instead of `PersistenceException` (translation is auto-configured; see Added). Catch blocks and rollback rules that reference `PersistenceException` for SQL failures need updating, or set `storm.exception-translation.enabled=false` to keep the previous behavior. - -### Removed - -- `@EnableTransactionIntegration` and `SpringTransactionConfiguration` (storm-kotlin-spring): the static, JVM-global transaction manager list is gone. Use `springOrmTemplate(...)` (plain Spring) or the starter (Spring Boot); multiple application contexts in one JVM no longer interfere. -- The Spring modules no longer register `ServiceLoader` providers, and the reflective Spring probes are removed from storm-core and storm-kotlin: templates never silently enlist in Spring transactions based on classpath presence. Plain templates created with `ORMTemplate.of(dataSource)` inside a Spring application now run independently of Spring transactions; compose with `springOrmTemplate` or the builder to integrate. This also removes the "programmatic and Spring managed transactions cannot be mixed" guard, superseded by the per-block provider check. -- `Providers.getConnection` / `Providers.releaseConnection`: connections are acquired through the template's own connection provider. -- `st.orm.spring.impl.SpringConnectionProviderImpl`, `SpringTransactionTemplateProviderImpl` and `TransactionAwareConnectionProviderImpl` are replaced by the public `st.orm.spring.SpringConnectionProvider` and `SpringTransactionTemplateProvider`. -- `st.orm.spring.impl.ResolverRegistration` (both language stacks): the autowire-candidate resolver is installed by the scanning engine itself. -- `storm-ktor-koin`: Ktor's built-in dependency injection is the supported DI path. Koin users keep full capability with a few lines of application code; the Ktor integration docs include the recipe. - -### Security - -- Schema validation reads database metadata through bound parameters instead of building the metadata queries with escaped string literals (storm-core `DatabaseSchema`). The catalog and schema names are database-supplied and were already correctly escaped, so this is defense in depth: no metadata query is assembled from concatenated values. -- The kotlinx-serialization module validates that a `Ref` target resolves to a Storm `Data` type before loading it, and loads it without running static initializers until that check passes. A serializer that reported an unrelated class name can no longer trigger that class's initialization. +## [1.13.0] - 2026-07-19 + +Feature release: write sets, GraalVM native images, the Storm Gradle plugin, Java transaction parity, and Micrometer observability. + +- Added write sets (`orm.writeSet()`, Kotlin also `orm.writeSet { }`): one write action over a mixed-type collection of entities, ordered by foreign key dependencies and batched per type, with generated keys propagated by instance identity. `insert` and `upsert` extend the passed entities with their insertion closure of unsaved referenced entities; `update` and `remove` write exactly what is passed. Entities go in as varargs or any `Iterable`; `AndFetch` variants return the persisted state. +- Added `getResultGroupedBy` / `getResultGroupedByRef` query terminals: the one-to-many read grouped during hydration in a single query, compile-time typed through the new `TypedMetamodel`. +- Added GraalVM native image support: reachability metadata, Spring AOT hints and AOT-participating repository scanning for Spring Boot; a storm-core GraalVM feature driven by the compile-time type index covers Ktor and plain JVM applications. +- Added the Storm Gradle plugin (`id("st.orm")`): imports the BOM, adds the language-path dependencies, wires the metamodel processor and the matching compiler-plugin variant, and sets the preview flags; configuration-cache compatible. +- Added programmatic transactions for Java (`Transactions.transaction(...)`) with the semantics of Kotlin's `transaction { }`, a shared transaction vocabulary in storm-foundation, and a Spring bridge that joins `@Transactional` transactions. +- Added query and transaction observability: `storm.query` and `storm.transaction` Micrometer Observations (new storm-micrometer module), auto-bound by the Spring Boot starters and the Ktor plugin, with opt-in OpenTelemetry database semantic conventions and trace-context SQL comments. +- Added `@EnableStormRepositories`, the `@DataStormTest` test slice (new storm-spring-boot-test-autoconfigure module), and shared starter auto-configurations; SQL failures now translate to Spring's `DataAccessException` hierarchy. +- Added to the Ktor plugin: repositories and the template through Ktor's built-in dependency injection, multiple databases via `database("name") { }`, and the `transactional { }` route DSL. +- Changed: framework integration points are instance-scoped. `ORMTemplate.builder(...)` composes `connectionProvider`, `transactionTemplateProvider`, `exceptionMapper` and `queryObserver`; `transaction { }` binds to the first template used inside the block; ambiguous provider resolution fails fast; templates never silently enlist in Spring transactions based on classpath presence. +- Changed: models are null-marked by default, aligning Java with Kotlin and JSpecify. Nullable annotations are load-bearing: a bare `@FK` joins with an INNER JOIN, `@Nullable @FK` with a LEFT JOIN. +- Changed: records are constructed through generated instantiators instead of reflection, removing the last reflective call from row mapping; leaner read and transaction hot paths. +- Changed: the Spring repository scanning engine is single-sourced in storm-spring with the Kotlin bindings in `st.orm.spring.kotlin`; one starter per application; Ktor upgraded to 3.2.3. +- Removed `@EnableTransactionIntegration`, the Spring `ServiceLoader` providers and reflective probes, and `storm-ktor-koin` (Ktor's built-in dependency injection is the supported path; the docs include the Koin recipe). +- Fixed the dynamic proxy interface order of monitored resources (deterministic, keeping GraalVM proxy registrations valid); hardened schema metadata queries and `Ref` deserialization. + +## [1.12.1] - 2026-07-12 + +- Fixed metamodel references to components of compound primary keys resolving to the underlying columns. +- Website and documentation updates. ## [1.12.0] - 2026-07-06 @@ -196,6 +156,8 @@ Feature release centered on the reified Kotlin query API. Breaking changes are a For releases prior to 1.3.2, see the [GitHub Releases](https://github.com/storm-orm/storm-framework/releases) page. +[1.13.0]: https://github.com/storm-orm/storm-framework/releases/tag/v1.13.0 +[1.12.1]: https://github.com/storm-orm/storm-framework/releases/tag/v1.12.1 [1.12.0]: https://github.com/storm-orm/storm-framework/releases/tag/v1.12.0 [1.11.6]: https://github.com/storm-orm/storm-framework/releases/tag/v1.11.6 [1.11.5]: https://github.com/storm-orm/storm-framework/releases/tag/v1.11.5 diff --git a/docs/comparison.md b/docs/comparison.md index 6bfbb1072..7dc9b073a 100644 --- a/docs/comparison.md +++ b/docs/comparison.md @@ -311,7 +311,9 @@ The following frameworks are Kotlin-only. Storm supports both Kotlin and Java. ## Storm vs Exposed -Exposed is JetBrains' official Kotlin database framework. It offers two APIs: a DSL that mirrors SQL syntax and a DAO layer for ORM-style access. Exposed defines tables as Kotlin objects rather than annotations on data classes. Storm and Exposed share the goal of idiomatic Kotlin database access but differ in entity design (mutable DAO entities vs. immutable data classes) and relationship loading strategy (lazy references vs. eager single-query loading). +Exposed is JetBrains' official Kotlin database framework. It offers two APIs: a DSL that mirrors SQL syntax and a DAO layer for ORM-style access. Exposed defines tables as Kotlin objects rather than annotations on data classes. Storm and Exposed share the goal of idiomatic Kotlin database access but differ in entity design (mutable DAO entities vs. immutable data classes) and relationship loading strategy (Storm loads an entity graph in a single query; Exposed uses explicit joins with `ResultRow` mapping in the DSL, and lazy or eager relationship delegates in the DAO). + +_Comparison reviewed against Exposed 1.3.1. Last reviewed: 2026-07-15._ | Aspect | Storm | Exposed | | --- | --- | --- | @@ -320,9 +322,9 @@ Exposed is JetBrains' official Kotlin database framework. It offers two APIs: a | **APIs** | Unified ORM + SQL Templates | DSL (SQL) + DAO (ORM) | | **Table Definition** | Annotations on data classes | DSL objects (`object Users : Table()`) | | **Entities (DAO)** | Immutable data classes (Kotlin) / records (Java) | Mutable, extend `Entity` class | -| **Relationships** | Loading in single query | Lazy references, manual loading | -| **N+1 Problem** | Prevented by design; requires explicit opt-in | Possible with DAO | -| **Coroutines** | First-class from the start | Supported (added later) | +| **Relationships** | Loading in single query | DSL: explicit joins; DAO: relationship delegates with lazy/eager loading options | +| **N+1 Problem** | Prevented by design; requires explicit opt-in | Possible with DAO; mitigated through eager loading or explicit queries | +| **Coroutines** | First-class from the start | Suspend transaction APIs (JDBC and R2DBC) | | **Type Safety** | Metamodel DSL | Column references | | **Transactions** | Optional, programmatic + declarative | `transaction {}` block, declarative via Spring module | @@ -332,7 +334,7 @@ Both Storm and Exposed use a `transaction { }` block for programmatic transactio Exposed's native API supports two modes: shared nesting (the default, where inner blocks join the outer transaction) and savepoint-based nesting (via `useNestedTransactions = true`). For other propagation behaviors, Exposed relies on Spring's `@Transactional` through its `SpringTransactionManager` integration module. -Storm supports all seven standard propagation modes natively in its `transaction { }` block, without requiring Spring: +Storm supports seven Spring-style transaction propagation modes natively in its `transaction { }` block, without requiring Spring. These behaviors are familiar from Spring's `@Transactional`; they are conventions from that world rather than SQL or JDBC standards: | Propagation | Storm | Exposed | |-------------|-------|---------| @@ -376,15 +378,15 @@ transaction { | Aspect | Storm | Exposed | |--------|-------|---------| | API style | Lambda (`onCommit { }`) | Interface (`StatementInterceptor`) | -| Suspend support | Yes (JDBC) | R2DBC only (`SuspendStatementInterceptor`) | -| Nested transaction behavior | Deferred to physical commit | Fires after savepoint release (data not yet durable) | -| Callback isolation | Yes (remaining callbacks still run on failure) | No (exception propagates, skipping remaining interceptors) | +| Suspend callbacks | Yes (JDBC) | Suspend interceptors are R2DBC only (`SuspendStatementInterceptor`) | +| Nested transaction behavior | Deferred to outer physical commit | Independent nested transactions via savepoints; verify intended boundary | +| Callback failure handling | Remaining callbacks still run; first error surfaced | See Exposed's interceptor behavior | | Global interceptors | No | Yes (via `ServiceLoader`) | | Additional hooks | No | `beforeCommit`, `beforeRollback`, `beforeExecution`, `afterExecution` | -The most significant behavioral difference is with nested transactions. Exposed's `afterCommit` fires on the nested transaction's own "commit," which for savepoint-based nesting is just a savepoint release, not an actual database commit. If the outer transaction subsequently rolls back, the `afterCommit` callback will have already executed despite the data never becoming durable. Storm avoids this by deferring callbacks to the outermost physical transaction. +The most significant difference is where callbacks bind with nested transactions. With `useNestedTransactions` enabled, Exposed implements independent nested transactions using savepoints. Applications relying on `afterCommit` callbacks should verify whether their desired boundary is the nested transaction or the outer physical commit. Storm explicitly defers joined and nested callbacks to the outer physical transaction, so a callback registered inside a nested scope only runs once that outer transaction is durably committed. -Storm's callback isolation behavior (remaining callbacks still execute when one fails) follows the same approach as Spring's `TransactionSynchronization`, where post-commit and post-completion callbacks are invoked independently. Since callbacks fire after the transaction outcome is final, there is nothing to undo; silently skipping remaining side effects because of one failure would be worse than running them all and surfacing the first exception. +Storm's callback failure handling (remaining callbacks still execute when one fails) follows the same approach as Spring's `TransactionSynchronization`, where post-commit and post-completion callbacks are invoked independently. Since callbacks fire after the transaction outcome is final, there is nothing to undo, so Storm runs every registered callback and surfaces the first exception rather than skipping the rest. Exposed's `StatementInterceptor` also provides hooks that Storm intentionally does not offer: `beforeCommit`, `beforeRollback`, and statement-level interceptors (`beforeExecution`, `afterExecution`). In Storm's stateless model, pre-commit logic belongs at the end of the `transaction { }` block itself, since there is no persistence context to flush or dirty state to reconcile before the commit. Statement-level observability is covered by Storm's [`@SqlLog`](sql-logging.md) annotation and `SqlCapture` test utility rather than a general interceptor mechanism. @@ -400,9 +402,9 @@ transaction { } ``` -This is convenient for prototyping and simple applications. For production use, JetBrains recommends pairing Exposed with a dedicated migration tool like Flyway or Liquibase, since `SchemaUtils` does not handle column removal, type changes, or data migration. +Beyond `SchemaUtils`, Exposed provides schema utilities, dedicated JDBC and R2DBC migration modules (`exposed-migration-jdbc` and `exposed-migration-r2dbc`, with `MigrationUtils`), and a Gradle plugin for generating migration scripts by comparing table definitions against the live database schema. Generated migrations must be reviewed and integrated into an execution workflow such as Flyway, Liquibase, or manual deployment; Exposed does not automatically apply generated migrations. See JetBrains' [migrations guide](https://www.jetbrains.com/help/exposed/migrations.html) for the full workflow. -Storm does not include schema management or migration utilities. Schema management is expected to be handled externally using tools like Flyway, Liquibase, or plain SQL scripts. Storm's [schema validation](validation.md) feature can verify at startup that entity definitions match the database schema, catching mismatches early without modifying the schema itself. +Storm intentionally provides schema validation but no schema-generation or migration facilities. Schema management is expected to be handled externally using tools like Flyway, Liquibase, or plain SQL scripts. Storm's [schema validation](validation.md) feature can verify at startup that entity definitions match the database schema, catching mismatches early without modifying the schema itself. ### When to Choose Storm @@ -415,13 +417,14 @@ Storm does not include schema management or migration utilities. Schema manageme ### When to Choose Exposed -- You prefer DSL-based table definitions -- You want to switch between SQL DSL and DAO styles - You like the JetBrains ecosystem integration -- You need fine-grained control over lazy loading -- You need R2DBC support for reactive database access* +- You prefer a SQL-oriented DSL for table definitions and queries +- You want an optional DAO API alongside the DSL +- You need JDBC or R2DBC support, including reactive, non-blocking database access* +- You want built-in schema and migration tooling (`SchemaUtils`, `MigrationUtils`, the Gradle plugin) +- You value a mature Kotlin community and ecosystem -*Storm uses JDBC and relies on JVM virtual threads for non-blocking I/O instead of R2DBC. +*Storm uses blocking JDBC, which can be executed efficiently on JVM virtual threads, rather than a non-blocking R2DBC driver. ## Storm vs Ktorm diff --git a/docs/ktor-integration.md b/docs/ktor-integration.md index 6d24d86a9..034dc406c 100644 --- a/docs/ktor-integration.md +++ b/docs/ktor-integration.md @@ -242,7 +242,7 @@ One semantic difference from calling `transaction { }` manually: the handler, in ### Nested Transactions and Propagation -Storm supports all standard propagation modes. Nested transactions are useful when composing services that each define their own transactional requirements. For example, an audit log that should persist even if the main operation fails needs its own independent transaction. +Storm supports the full set of Spring-style propagation modes. Nested transactions are useful when composing services that each define their own transactional requirements. For example, an audit log that should persist even if the main operation fails needs its own independent transaction. ```kotlin post("/orders") { @@ -258,7 +258,7 @@ post("/orders") { } ``` -All seven standard propagation modes are supported: `REQUIRED` (default), `REQUIRES_NEW`, `NESTED`, `SUPPORTS`, `MANDATORY`, `NOT_SUPPORTED`, and `NEVER`. See [Transactions](transactions.md) for the full propagation matrix and detailed examples. +All seven Spring-style propagation modes are supported: `REQUIRED` (default), `REQUIRES_NEW`, `NESTED`, `SUPPORTS`, `MANDATORY`, `NOT_SUPPORTED`, and `NEVER`. See [Transactions](transactions.md) for the full propagation matrix and detailed examples. ### Read-Only Transactions diff --git a/docs/transactions.md b/docs/transactions.md index 5c8a8175f..5de7a1ed6 100644 --- a/docs/transactions.md +++ b/docs/transactions.md @@ -14,7 +14,7 @@ Storm works directly with JDBC transactions and supports both programmatic and d Storm for Kotlin provides a fully programmatic transaction solution (following the style popularized by [Exposed](https://github.com/JetBrains/Exposed)) that is **completely coroutine-friendly**. It supports **all isolation levels and propagation modes** found in traditional transaction management systems. You can freely switch coroutine dispatchers within a transaction (offload CPU-bound work to `Dispatchers.Default` or IO work to `Dispatchers.IO`) and still remain in the **same active transaction**. -While Storm's `transaction { }` blocks look similar to Exposed's, Storm goes further by supporting all seven standard propagation modes (`REQUIRED`, `REQUIRES_NEW`, `NESTED`, `MANDATORY`, `SUPPORTS`, `NOT_SUPPORTED`, `NEVER`). Exposed's native transaction API only supports basic nesting (shared transaction) and savepoint-based nesting (`useNestedTransactions = true`), without the ability to suspend an outer transaction, enforce transactional context, or run non-transactionally. See [Storm vs Exposed](comparison.md#storm-vs-exposed) for a detailed comparison. +While Storm's `transaction { }` blocks look similar to Exposed's, Storm goes further by supporting seven Spring-style propagation modes (`REQUIRED`, `REQUIRES_NEW`, `NESTED`, `MANDATORY`, `SUPPORTS`, `NOT_SUPPORTED`, `NEVER`). Exposed's native transaction API only supports basic nesting (shared transaction) and savepoint-based nesting (`useNestedTransactions = true`), without the ability to suspend an outer transaction, enforce transactional context, or run non-transactionally. See [Storm vs Exposed](comparison.md#storm-vs-exposed) for a detailed comparison. The API is designed around Kotlin's type system and coroutine model. Import the transaction functions from `st.orm.template` and the option enums from `st.orm` (shared with the Java API): @@ -69,7 +69,7 @@ transactionBlocking { Propagation modes are one of the most powerful features of enterprise transaction management, yet they're often misunderstood. They control how transactions interact when code calls another transactional method. This is essential for building composable services where each method can define its transactional requirements independently. -Storm supports all seven standard propagation modes. Understanding when to use each mode helps you build robust, maintainable applications where components work correctly both standalone and when composed together. +Storm supports seven Spring-style propagation modes. Understanding when to use each mode helps you build robust, maintainable applications where components work correctly both standalone and when composed together. #### REQUIRED (Default) diff --git a/website/src/pages/benchmarks.js b/website/src/pages/benchmarks.js index d4eaa5c74..df41fc243 100644 --- a/website/src/pages/benchmarks.js +++ b/website/src/pages/benchmarks.js @@ -429,7 +429,7 @@ ${navHtml('benchmarks')}

At a glance

Seven implementations, one database, one discipline: same schema, same data, same transaction boundaries, every score a real network round trip away from PostgreSQL. Mean latency per operation, lower is better. Cells are tinted by distance from the fastest framework in the row, green through red. Percentages are overhead over raw JDBC. Raw JDBC is the reference floor.

${matrixHtml()} -

Every library has strong rows, but Storm's is the only column that never runs hot. On every workload Storm is either the fastest framework or close behind it, while every alternative has at least one workload where it costs a third more than the best, and most cost far more than that. A real network round trip sits inside every score, so the pure mapping gap is larger still. Absolute times depend on the hardware they were measured on; the relative comparisons are the point.

+

Each library excels in certain areas. Storm has the lowest average measured overhead over raw JDBC across these eight workloads, while individual workloads favor different libraries: raw JDBC is fastest everywhere by construction, and Exposed's DSL edges Storm on the 1,000-row three-table join and the read-modify-update workload. Across the suite Storm is consistently the fastest full framework or within a third of it. A real network round trip sits inside every score, so the pure mapping gap is larger still. Absolute times depend on the hardware they were measured on; the relative comparisons are the point.

Per-workload charts: the same numbers with their reported error diff --git a/website/src/pages/comparison.js b/website/src/pages/comparison.js index fae1c1fd9..65b7ff462 100644 --- a/website/src/pages/comparison.js +++ b/website/src/pages/comparison.js @@ -53,7 +53,7 @@ const FRAMEWORKS = [ { name: 'Exposed', slug: 'storm-vs-exposed', - desc: 'JetBrains’ Kotlin framework, defining tables as DSL objects. Storm declares the model once as annotated data classes and supports all seven transaction propagation modes without Spring.', + desc: 'JetBrains’ Kotlin framework, defining tables as DSL objects. Storm declares the model once as annotated data classes and supports seven Spring-style transaction propagation modes without requiring Spring.', chips: ['Kotlin only', 'DSL tables'], }, { @@ -74,7 +74,7 @@ function buildBody() { Entity modelImmutable data class (~5 lines)Mutable class (~30, ~10 with Lombok)Generated from schemaDSL table object (+ optional DAO)Mutable interface (+ DSL table) Immutable entitiesYesNoYesDSL onlyNo Type-safe queriesYesCriteria APIYesYesYes - N+1 handlingSingle-query entity graphCommon pitfallManualManualManual + N+1 handlingSingle-query entity graphCommon pitfallManualManual joins (DSL) · eager-load (DAO)Manual Query across relationsOne line: joins and mapping derived from @FKJPQL strings or Criteria buildersImplicit path joins + multisetManual joins and row mappingManual joins Session stateNone: no session, no flushPersistence context, flush, dirty checkingNoneDSL none · DAO transaction-boundPer-entity change tracking Deferred loadingExplicit Ref<T>Proxies, session-boundNot applicableDAO lazy, transaction-boundManual joins diff --git a/website/static/llms-full.txt b/website/static/llms-full.txt index 872de575b..211fbde4a 100644 --- a/website/static/llms-full.txt +++ b/website/static/llms-full.txt @@ -18,7 +18,7 @@ > GitHub: https://github.com/storm-orm/storm-framework > License: Apache 2.0 -# Generated: 2026-07-14T23:35:52Z +# Generated: 2026-07-15T10:39:11Z ======================================== ## Source: index.md @@ -5903,7 +5903,7 @@ Storm works directly with JDBC transactions and supports both programmatic and d Storm for Kotlin provides a fully programmatic transaction solution (following the style popularized by [Exposed](https://github.com/JetBrains/Exposed)) that is **completely coroutine-friendly**. It supports **all isolation levels and propagation modes** found in traditional transaction management systems. You can freely switch coroutine dispatchers within a transaction (offload CPU-bound work to `Dispatchers.Default` or IO work to `Dispatchers.IO`) and still remain in the **same active transaction**. -While Storm's `transaction { }` blocks look similar to Exposed's, Storm goes further by supporting all seven standard propagation modes (`REQUIRED`, `REQUIRES_NEW`, `NESTED`, `MANDATORY`, `SUPPORTS`, `NOT_SUPPORTED`, `NEVER`). Exposed's native transaction API only supports basic nesting (shared transaction) and savepoint-based nesting (`useNestedTransactions = true`), without the ability to suspend an outer transaction, enforce transactional context, or run non-transactionally. See [Storm vs Exposed](comparison.md#storm-vs-exposed) for a detailed comparison. +While Storm's `transaction { }` blocks look similar to Exposed's, Storm goes further by supporting seven Spring-style propagation modes (`REQUIRED`, `REQUIRES_NEW`, `NESTED`, `MANDATORY`, `SUPPORTS`, `NOT_SUPPORTED`, `NEVER`). Exposed's native transaction API only supports basic nesting (shared transaction) and savepoint-based nesting (`useNestedTransactions = true`), without the ability to suspend an outer transaction, enforce transactional context, or run non-transactionally. See [Storm vs Exposed](comparison.md#storm-vs-exposed) for a detailed comparison. The API is designed around Kotlin's type system and coroutine model. Import the transaction functions from `st.orm.template` and the option enums from `st.orm` (shared with the Java API): @@ -5958,7 +5958,7 @@ transactionBlocking { Propagation modes are one of the most powerful features of enterprise transaction management, yet they're often misunderstood. They control how transactions interact when code calls another transactional method. This is essential for building composable services where each method can define its transactional requirements independently. -Storm supports all seven standard propagation modes. Understanding when to use each mode helps you build robust, maintainable applications where components work correctly both standalone and when composed together. +Storm supports seven Spring-style propagation modes. Understanding when to use each mode helps you build robust, maintainable applications where components work correctly both standalone and when composed together. #### REQUIRED (Default) @@ -12159,9 +12159,9 @@ orm.entity(User.class).upsert(new User("alice@example.com", "Alice", birthDate, Inserting an object graph that spans several tables normally requires careful choreography: insert the parents first, collect their generated keys, rebuild the children against those keys, and repeat for every level. Write sets lift that burden. -A write set applies one write operation to a heterogeneous collection of entities. Storm partitions the entities by type, orders them by their foreign key dependencies, and writes them in dependency-ordered batches, propagating generated primary keys to dependent entities. `insert` and `upsert` extend the *explicit members* (the entities you supply) with *discovered members*: unsaved entities transitively reachable through insertable foreign key fields. This discovery is called the *insertion closure*. `update` and `remove` operate on the explicit members only. +A write set applies one write operation to a heterogeneous collection of entities. Storm partitions the entities by type, orders them by their foreign key dependencies, and writes them in dependency-ordered batches, propagating generated primary keys to dependent entities. `insert` and `upsert` extend the *explicit members* (the entities you supply) with *discovered members*: unsaved entities transitively reachable through insertable foreign key fields. This discovery is called the *insertion closure*. `update` and `remove` operate on the explicit members only. Every action accepts the entities as varargs or as any `Iterable`. -Write sets are not a cascade in the JPA sense. There are no mapping annotations, no persistence context and no session-wide cascade: all writes derive from the entities supplied to the call and, for insert and upsert, their insertion closure. The per-row semantics of every verb are identical to the corresponding repository operation, including entity callbacks and dirty checking. +Write sets are not a cascade in the JPA sense. There are no mapping annotations, no persistence context and no session-wide cascade: all writes derive from the entities supplied to the call and, for insert and upsert, their insertion closure. The per-row semantics of every action are identical to the corresponding repository operation, including entity callbacks and dirty checking. Coming from JPA? [JPA Cascades vs Write Sets](jpa-cascades-vs-write-sets.md) compares the two models side by side. A write set is obtained from the ORM template, or from any repository (`writeSet()` on a repository is a convenience that delegates to the underlying template; it is not scoped to the repository's entity type): @@ -12170,7 +12170,7 @@ A write set is obtained from the ORM template, or from any repository (`writeSet ```kotlin orm.writeSet().insert(entities) -// or scoped; each verb executes immediately, the block only groups the calls +// or scoped; each action executes immediately, the block only groups the calls transaction { orm.writeSet { insert(newEntities) @@ -12199,7 +12199,7 @@ val wolfie = Pet(name = "Wolfie", birthDate = birthDate, type = dog, owner = own val rex = Pet(name = "Rex", birthDate = birthDate, type = dog, owner = owner) // same owner instance val visit = Visit(visitDate = today, description = "Check-up", pet = wolfie) -orm.writeSet().insert(listOf(wolfie, rex, visit)) +orm.writeSet().insert(wolfie, rex, visit) ``` [Java] @@ -12210,7 +12210,7 @@ var wolfie = new Pet("Wolfie", birthDate, dog, owner); var rex = new Pet("Rex", birthDate, dog, owner); // same owner instance var visit = new Visit(today, "Check-up", wolfie); -orm.writeSet().insert(List.of(wolfie, rex, visit)); +orm.writeSet().insert(wolfie, rex, visit); ``` This particular graph needs three dependency-ordered batch operations, regardless of how many pets or visits the set contains: owners first, then pets, then visits. The owner was never passed explicitly; it becomes a discovered member because the pet values hold it in their foreign key field. That is the insertion closure at work: a record whose foreign key field holds an unsaved entity is a value that describes both rows, and inserting the value inserts both. @@ -12236,7 +12236,7 @@ Key propagation correlates by instance, not by `equals`. A `copy()` of an unsave [Kotlin] ```kotlin -val inserted = orm.writeSet().insertAndFetch(listOf(wolfie, visit)) +val inserted = orm.writeSet().insertAndFetch(wolfie, visit) val fetchedPet = inserted[0] as Pet // fetchedPet.owner carries the generated key val fetchedVisit: Visit = orm.writeSet().insertAndFetch(visit) // single root, typed @@ -12245,7 +12245,7 @@ val fetchedVisit: Visit = orm.writeSet().insertAndFetch(visit) // single root, [Java] ```java -var inserted = orm.writeSet().insertAndFetch(List.of(wolfie, visit)); +var inserted = orm.writeSet().insertAndFetch(wolfie, visit); var fetchedPet = (Pet) inserted.get(0); // fetchedPet.owner() carries the generated key Visit fetchedVisit = orm.writeSet().insertAndFetch(visit); // single root, typed @@ -12258,14 +12258,14 @@ Foreign key fields typed as `Ref` participate through entity-wrapped refs, which ```kotlin val pet = Pet(name = "Shadow", birthDate = birthDate, type = dog, owner = Ref.of(owner)) -orm.writeSet().insert(listOf(pet)) // owner is inserted first, the ref binds the generated key +orm.writeSet().insert(pet) // owner is inserted first, the ref binds the generated key ``` [Java] ```java var pet = new Pet("Shadow", birthDate, dog, Ref.of(owner)); -orm.writeSet().insert(List.of(pet)); // owner is inserted first, the ref binds the generated key +orm.writeSet().insert(pet); // owner is inserted first, the ref binds the generated key ``` Id-only refs such as `Ref.of(Owner::class, 42)` are pointers to known rows: they bind as foreign key values and are never written. An id-only ref carrying a default id cannot describe a new entity and fails fast. Note that ref equality is based on type and id, so refs wrapping distinct unsaved instances compare equal until the instances are persisted; do not use unsaved refs as map keys. @@ -12307,7 +12307,7 @@ val user = User(name = "Alice") // unsaved val role = orm.entity().findByName("admin") // saved val userRole = UserRole(UserRolePk(user.id, role.id), user, role) -orm.writeSet().insert(listOf(userRole)) // user is inserted first; its key lands in userRolePk.userId +orm.writeSet().insert(userRole) // user is inserted first; its key lands in userRolePk.userId ``` [Java] @@ -12317,28 +12317,28 @@ var user = new User("Alice"); // unsaved var role = orm.entity(Role.class).findByName("admin"); // saved var userRole = new UserRole(new UserRolePk(user.id(), role.id()), user, role); -orm.writeSet().insert(List.of(userRole)); // user is inserted first; its key lands in userRolePk.userId +orm.writeSet().insert(userRole); // user is inserted first; its key lands in userRolePk.userId ``` The unsaved entity's default id in the key component is a placeholder; the write set overwrites it with the generated key. Only when no insertable primary key component carries the column value does the reference fail fast (see below). ## Update, Upsert and Remove -The remaining verbs follow the same contract, each with the ordering that suits it: +The remaining actions follow the same contract, each with the ordering that suits it: -- `update` groups the explicit members by type and updates them with the usual semantics, including transaction-scoped dirty checking: unchanged entities are skipped. Referenced entities are never updated implicitly. Unsaved members are rejected; a row that does not exist cannot be updated. +- `update` groups the explicit members by type and updates them with the usual semantics, including transaction-scoped dirty checking: unchanged entities are skipped. Referenced entities are never updated implicitly: an updated `Owner` held inside a `Pet` you update is not written, it contributes only its primary key. Pass both when both changed; dirty checking skips whichever members are unchanged. Unsaved members are rejected; a row that does not exist cannot be updated. - `upsert` applies the per-repository upsert semantics to the explicit members and inserts the discovered members of the insertion closure, with keys propagating as for insert. Explicit membership takes precedence: a keyed entity that is both supplied and referenced by another member is upserted, and is written before the members that reference it. Whether an upsert can create a row for a member carrying a preset key follows the dialect's per-repository upsert behavior. - `remove` deletes exactly the explicit members, children before parents. Members are correlated by entity type and primary key rather than by instance, so a member referencing another member is removed first regardless of which instance its foreign key field holds. Referenced entities are never removed implicitly. [Kotlin] ```kotlin -orm.writeSet().remove(listOf(owner, pet, visit)) // executed as: visit, pet, owner +orm.writeSet().remove(owner, pet, visit) // executed as: visit, pet, owner ``` [Java] ```java -orm.writeSet().remove(List.of(owner, pet, visit)); // executed as: visit, pet, owner +orm.writeSet().remove(owner, pet, visit); // executed as: visit, pet, owner ``` ## Fail-Fast Behavior @@ -12358,7 +12358,7 @@ Write sets complete Storm's persistence story without introducing a session. Eac - **Save-or-update decisions** belong to [upserts](upserts.md), which resolve conflicts atomically in the database. - **Skipping unchanged rows and partial updates** belong to [dirty checking](dirty-checking.md), driven by the transaction-scoped entity cache. - **Cascading deletes of dependent rows** belong to the schema: declare `ON DELETE CASCADE` on the foreign key constraint. Storm does not discover children reactively. -- **Implicit updates of referenced entities** do not exist. A keyed entity referenced by a set member only provides its key; modifying it requires an explicit update. +- **Implicit updates of referenced entities** do not exist. A keyed entity referenced by a set member only provides its key; modifying it requires an explicit update. One rule covers every action: a write set writes the entities you name, plus the entities your values make necessary. An unsaved referenced entity is necessary (its dependent cannot be written without its key, and a row that does not exist cannot be a stale copy); a keyed referenced entity never is: it is the state that was hydrated when the value was read, and treating that snapshot as write intent would silently overwrite newer database state. - **Re-planning after entity callbacks** does not happen. Callbacks run inside the per-type repository operations, after members are discovered and the execution order is planned; a callback that alters foreign key fields does not change what is discovered or in which order it is written. @@ -18067,7 +18067,9 @@ The following frameworks are Kotlin-only. Storm supports both Kotlin and Java. ## Storm vs Exposed -Exposed is JetBrains' official Kotlin database framework. It offers two APIs: a DSL that mirrors SQL syntax and a DAO layer for ORM-style access. Exposed defines tables as Kotlin objects rather than annotations on data classes. Storm and Exposed share the goal of idiomatic Kotlin database access but differ in entity design (mutable DAO entities vs. immutable data classes) and relationship loading strategy (lazy references vs. eager single-query loading). +Exposed is JetBrains' official Kotlin database framework. It offers two APIs: a DSL that mirrors SQL syntax and a DAO layer for ORM-style access. Exposed defines tables as Kotlin objects rather than annotations on data classes. Storm and Exposed share the goal of idiomatic Kotlin database access but differ in entity design (mutable DAO entities vs. immutable data classes) and relationship loading strategy (Storm loads an entity graph in a single query; Exposed uses explicit joins with `ResultRow` mapping in the DSL, and lazy or eager relationship delegates in the DAO). + +_Comparison reviewed against Exposed 1.3.1. Last reviewed: 2026-07-15._ | Aspect | Storm | Exposed | | --- | --- | --- | @@ -18076,9 +18078,9 @@ Exposed is JetBrains' official Kotlin database framework. It offers two APIs: a | **APIs** | Unified ORM + SQL Templates | DSL (SQL) + DAO (ORM) | | **Table Definition** | Annotations on data classes | DSL objects (`object Users : Table()`) | | **Entities (DAO)** | Immutable data classes (Kotlin) / records (Java) | Mutable, extend `Entity` class | -| **Relationships** | Loading in single query | Lazy references, manual loading | -| **N+1 Problem** | Prevented by design; requires explicit opt-in | Possible with DAO | -| **Coroutines** | First-class from the start | Supported (added later) | +| **Relationships** | Loading in single query | DSL: explicit joins; DAO: relationship delegates with lazy/eager loading options | +| **N+1 Problem** | Prevented by design; requires explicit opt-in | Possible with DAO; mitigated through eager loading or explicit queries | +| **Coroutines** | First-class from the start | Suspend transaction APIs (JDBC and R2DBC) | | **Type Safety** | Metamodel DSL | Column references | | **Transactions** | Optional, programmatic + declarative | `transaction {}` block, declarative via Spring module | @@ -18088,7 +18090,7 @@ Both Storm and Exposed use a `transaction { }` block for programmatic transactio Exposed's native API supports two modes: shared nesting (the default, where inner blocks join the outer transaction) and savepoint-based nesting (via `useNestedTransactions = true`). For other propagation behaviors, Exposed relies on Spring's `@Transactional` through its `SpringTransactionManager` integration module. -Storm supports all seven standard propagation modes natively in its `transaction { }` block, without requiring Spring: +Storm supports seven Spring-style transaction propagation modes natively in its `transaction { }` block, without requiring Spring. These behaviors are familiar from Spring's `@Transactional`; they are conventions from that world rather than SQL or JDBC standards: | Propagation | Storm | Exposed | |-------------|-------|---------| @@ -18132,15 +18134,15 @@ transaction { | Aspect | Storm | Exposed | |--------|-------|---------| | API style | Lambda (`onCommit { }`) | Interface (`StatementInterceptor`) | -| Suspend support | Yes (JDBC) | R2DBC only (`SuspendStatementInterceptor`) | -| Nested transaction behavior | Deferred to physical commit | Fires after savepoint release (data not yet durable) | -| Callback isolation | Yes (remaining callbacks still run on failure) | No (exception propagates, skipping remaining interceptors) | +| Suspend callbacks | Yes (JDBC) | Suspend interceptors are R2DBC only (`SuspendStatementInterceptor`) | +| Nested transaction behavior | Deferred to outer physical commit | Independent nested transactions via savepoints; verify intended boundary | +| Callback failure handling | Remaining callbacks still run; first error surfaced | See Exposed's interceptor behavior | | Global interceptors | No | Yes (via `ServiceLoader`) | | Additional hooks | No | `beforeCommit`, `beforeRollback`, `beforeExecution`, `afterExecution` | -The most significant behavioral difference is with nested transactions. Exposed's `afterCommit` fires on the nested transaction's own "commit," which for savepoint-based nesting is just a savepoint release, not an actual database commit. If the outer transaction subsequently rolls back, the `afterCommit` callback will have already executed despite the data never becoming durable. Storm avoids this by deferring callbacks to the outermost physical transaction. +The most significant difference is where callbacks bind with nested transactions. With `useNestedTransactions` enabled, Exposed implements independent nested transactions using savepoints. Applications relying on `afterCommit` callbacks should verify whether their desired boundary is the nested transaction or the outer physical commit. Storm explicitly defers joined and nested callbacks to the outer physical transaction, so a callback registered inside a nested scope only runs once that outer transaction is durably committed. -Storm's callback isolation behavior (remaining callbacks still execute when one fails) follows the same approach as Spring's `TransactionSynchronization`, where post-commit and post-completion callbacks are invoked independently. Since callbacks fire after the transaction outcome is final, there is nothing to undo; silently skipping remaining side effects because of one failure would be worse than running them all and surfacing the first exception. +Storm's callback failure handling (remaining callbacks still execute when one fails) follows the same approach as Spring's `TransactionSynchronization`, where post-commit and post-completion callbacks are invoked independently. Since callbacks fire after the transaction outcome is final, there is nothing to undo, so Storm runs every registered callback and surfaces the first exception rather than skipping the rest. Exposed's `StatementInterceptor` also provides hooks that Storm intentionally does not offer: `beforeCommit`, `beforeRollback`, and statement-level interceptors (`beforeExecution`, `afterExecution`). In Storm's stateless model, pre-commit logic belongs at the end of the `transaction { }` block itself, since there is no persistence context to flush or dirty state to reconcile before the commit. Statement-level observability is covered by Storm's [`@SqlLog`](sql-logging.md) annotation and `SqlCapture` test utility rather than a general interceptor mechanism. @@ -18156,9 +18158,9 @@ transaction { } ``` -This is convenient for prototyping and simple applications. For production use, JetBrains recommends pairing Exposed with a dedicated migration tool like Flyway or Liquibase, since `SchemaUtils` does not handle column removal, type changes, or data migration. +Beyond `SchemaUtils`, Exposed provides schema utilities, dedicated JDBC and R2DBC migration modules (`exposed-migration-jdbc` and `exposed-migration-r2dbc`, with `MigrationUtils`), and a Gradle plugin for generating migration scripts by comparing table definitions against the live database schema. Generated migrations must be reviewed and integrated into an execution workflow such as Flyway, Liquibase, or manual deployment; Exposed does not automatically apply generated migrations. See JetBrains' [migrations guide](https://www.jetbrains.com/help/exposed/migrations.html) for the full workflow. -Storm does not include schema management or migration utilities. Schema management is expected to be handled externally using tools like Flyway, Liquibase, or plain SQL scripts. Storm's [schema validation](validation.md) feature can verify at startup that entity definitions match the database schema, catching mismatches early without modifying the schema itself. +Storm intentionally provides schema validation but no schema-generation or migration facilities. Schema management is expected to be handled externally using tools like Flyway, Liquibase, or plain SQL scripts. Storm's [schema validation](validation.md) feature can verify at startup that entity definitions match the database schema, catching mismatches early without modifying the schema itself. ### When to Choose Storm @@ -18171,13 +18173,14 @@ Storm does not include schema management or migration utilities. Schema manageme ### When to Choose Exposed -- You prefer DSL-based table definitions -- You want to switch between SQL DSL and DAO styles - You like the JetBrains ecosystem integration -- You need fine-grained control over lazy loading -- You need R2DBC support for reactive database access* +- You prefer a SQL-oriented DSL for table definitions and queries +- You want an optional DAO API alongside the DSL +- You need JDBC or R2DBC support, including reactive, non-blocking database access* +- You want built-in schema and migration tooling (`SchemaUtils`, `MigrationUtils`, the Gradle plugin) +- You value a mature Kotlin community and ecosystem -*Storm uses JDBC and relies on JVM virtual threads for non-blocking I/O instead of R2DBC. +*Storm uses blocking JDBC, which can be executed efficiently on JVM virtual threads, rather than a non-blocking R2DBC driver. ## Storm vs Ktorm @@ -18838,6 +18841,7 @@ This guide helps you transition from JPA/Hibernate to Storm. The two frameworks | JPQL / Criteria API | Type-safe DSL / SQL Templates | | EntityManager | ORMTemplate | | `@OneToMany`, `@ManyToOne` | `@FK` annotation | +| Cascade types on mappings | Write sets per call site | ## Entity Migration @@ -19150,6 +19154,8 @@ Storm approach (query the "many" side): val orders = orm.findAll(Order_.user eq user) ``` +Without child collections there is also nothing to cascade over. Persisting a graph of related entities is a per-call decision made with [write sets](write-sets.md); [JPA Cascades vs Write Sets](jpa-cascades-vs-write-sets.md) walks through the translation and the reasoning behind it. + ## Transaction Migration Storm supports both Spring's `@Transactional` annotation and its own programmatic `transaction {}` block. If you are migrating a Spring application, your existing `@Transactional` annotations continue to work unchanged. Storm participates in the same Spring-managed transaction. The programmatic API is useful when you want explicit control over isolation levels, propagation, or when working outside of Spring entirely. From 5bed8037e11d471f0be9363aca4b7e53e421e933 Mon Sep 17 00:00:00 2001 From: Leon van Zantvoort Date: Wed, 15 Jul 2026 13:27:02 +0200 Subject: [PATCH 2/2] docs: last-reviewed dates and version anchors for all framework comparisons --- docs/comparison.md | 16 ++++++++++++++++ website/static/llms-full.txt | 18 +++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/docs/comparison.md b/docs/comparison.md index 7dc9b073a..11b152820 100644 --- a/docs/comparison.md +++ b/docs/comparison.md @@ -9,6 +9,8 @@ There is no universally "best" database framework. Each has strengths suited to The following tables provide a side-by-side comparison of concrete features across all frameworks discussed on this page. "Yes" and "No" indicate built-in support; "Manual" means the feature is achievable but requires explicit effort from the developer. +_All comparisons on this page last reviewed 2026-07-15 against: Hibernate ORM 7.4.5, Spring Data JPA 4.1.0, MyBatis 3.5.19, jOOQ 3.21.6, JDBI 3.54.0, Jimmer 0.11.0, Exposed 1.3.1, and Ktorm 4.2.1._ + ### Entity & Data Modeling | Feature | Storm | JPA | Spring Data | MyBatis | jOOQ | JDBI | Jimmer | Exposed | Ktorm | @@ -78,6 +80,8 @@ The following tables provide a side-by-side comparison of concrete features acro JPA (typically implemented by Hibernate) is the most widely used persistence framework in the Java ecosystem. It provides a full object-relational mapping layer with managed entities and second-level caching. Storm takes a fundamentally different approach: entities are plain values with no managed state, and database interactions are explicit rather than implicit. This makes Storm simpler to reason about at the cost of JPA's more automated (but less predictable) features. +_Comparison reviewed against Hibernate ORM 7.4.5. Last reviewed: 2026-07-15._ + | Aspect | Storm | JPA/Hibernate | |--------|-------|------------------------------------------| | **Entities** | Immutable records/data classes | Mutable classes with getters/setters | @@ -117,6 +121,8 @@ JPA (typically implemented by Hibernate) is the most widely used persistence fra Spring Data JPA wraps JPA with a repository abstraction that derives query implementations from method names. It reduces boilerplate but inherits all of JPA's runtime complexity (proxies, managed state, lazy loading). Storm's Spring integration provides similar repository convenience with explicit query bodies instead of naming conventions. +_Comparison reviewed against Spring Data JPA 4.1.0. Last reviewed: 2026-07-15._ + | Aspect | Storm | Spring Data JPA | |--------|-------|-----------------| | **Foundation** | Custom ORM | JPA/Hibernate | @@ -144,6 +150,8 @@ Spring Data JPA wraps JPA with a repository abstraction that derives query imple MyBatis is a SQL mapper that gives you full control over every query. You write SQL in XML files or annotations and map results to POJOs manually. Storm sits at a higher abstraction level, inferring SQL from entity definitions while still allowing raw SQL when needed. The trade-off is flexibility vs. automation: MyBatis never generates SQL for you, while Storm handles the common cases and lets you drop to raw SQL for complex queries. +_Comparison reviewed against MyBatis 3.5.19. Last reviewed: 2026-07-15._ + | Aspect | Storm | MyBatis | |--------|-------|---------| | **Approach** | Stateless ORM | SQL mapper | @@ -177,6 +185,8 @@ MyBatis is a SQL mapper that gives you full control over every query. You write jOOQ generates Java code from your database schema, providing a type-safe SQL DSL that mirrors the structure of your tables. Storm also treats the database schema as the source of truth, but instead of generating code from the schema, you write entity definitions that reflect it, and the metamodel is generated from those entities. Both frameworks provide compile-time type safety, but queries look very different. jOOQ excels at complex SQL (window functions, CTEs, recursive queries) where its DSL closely follows SQL syntax, but this means every join, column reference, and condition must be spelled out explicitly. Storm queries are more concise: the metamodel and automatic join derivation from `@FK` annotations let you write queries that focus on what you want rather than how to join it. Storm excels at entity-oriented operations where automatic relationship handling and repository patterns reduce boilerplate. +_Comparison reviewed against jOOQ 3.21.6. Last reviewed: 2026-07-15._ + | Aspect | Storm | jOOQ | |--------|-------|------| | **Approach** | Schema-reflective ORM | Schema-driven code generation | @@ -208,6 +218,8 @@ jOOQ generates Java code from your database schema, providing a type-safe SQL DS JDBI is a lightweight SQL convenience library that sits just above JDBC. It handles parameter binding, result mapping, and connection management without imposing an object model. Storm provides more structure with entity definitions, automatic relationship loading, and a repository pattern. Choose JDBI when you want minimal abstraction and full SQL control; choose Storm when you want the framework to handle common patterns while still allowing raw SQL escape hatches. +_Comparison reviewed against JDBI 3.54.0. Last reviewed: 2026-07-15._ + | Aspect | Storm | JDBI | |--------|-------|------| | **Level** | Stateless ORM | Low-level SQL mapping | @@ -233,6 +245,8 @@ JDBI is a lightweight SQL convenience library that sits just above JDBC. It hand Jimmer is a modern Kotlin and Java ORM that, like Storm, is built on immutable entities and a stateless model with no persistence context, and it eliminates the N+1 problem by design. The two frameworks share a great deal of philosophy. Where they differ is conciseness and concept count. Jimmer trades some concision for GraphQL-style dynamic object fetching: entities are interfaces you interact with through generated drafts, reading a foreign-key id without loading the association requires an extra `@IdView` property, and every query ends in an explicit `select` with an object fetcher. That fetcher, together with Jimmer's dedicated DTO language, is a genuine strength for shape-controlled API responses. Storm keeps the model a plain data class and the query a single line, and treats any data class as a result type. +_Comparison reviewed against Jimmer 0.11.0. Last reviewed: 2026-07-15._ + Defining an entity: ```kotlin @@ -430,6 +444,8 @@ Storm intentionally provides schema validation but no schema-generation or migra Ktorm is a lightweight Kotlin ORM that uses entity interfaces and DSL-based table definitions. It requires no code generation and has minimal dependencies. Storm differs primarily in its use of immutable data classes (instead of mutable interfaces), automatic relationship loading, and optional metamodel generation for compile-time type safety. +_Comparison reviewed against Ktorm 4.2.1. Last reviewed: 2026-07-15._ + | Aspect | Storm | Ktorm | | --- | --- | --- | | **Language** | Kotlin + Java | Kotlin only | diff --git a/website/static/llms-full.txt b/website/static/llms-full.txt index 211fbde4a..46758031c 100644 --- a/website/static/llms-full.txt +++ b/website/static/llms-full.txt @@ -18,7 +18,7 @@ > GitHub: https://github.com/storm-orm/storm-framework > License: Apache 2.0 -# Generated: 2026-07-15T10:39:11Z +# Generated: 2026-07-15T11:26:56Z ======================================== ## Source: index.md @@ -17765,6 +17765,8 @@ There is no universally "best" database framework. Each has strengths suited to The following tables provide a side-by-side comparison of concrete features across all frameworks discussed on this page. "Yes" and "No" indicate built-in support; "Manual" means the feature is achievable but requires explicit effort from the developer. +_All comparisons on this page last reviewed 2026-07-15 against: Hibernate ORM 7.4.5, Spring Data JPA 4.1.0, MyBatis 3.5.19, jOOQ 3.21.6, JDBI 3.54.0, Jimmer 0.11.0, Exposed 1.3.1, and Ktorm 4.2.1._ + ### Entity & Data Modeling | Feature | Storm | JPA | Spring Data | MyBatis | jOOQ | JDBI | Jimmer | Exposed | Ktorm | @@ -17834,6 +17836,8 @@ The following tables provide a side-by-side comparison of concrete features acro JPA (typically implemented by Hibernate) is the most widely used persistence framework in the Java ecosystem. It provides a full object-relational mapping layer with managed entities and second-level caching. Storm takes a fundamentally different approach: entities are plain values with no managed state, and database interactions are explicit rather than implicit. This makes Storm simpler to reason about at the cost of JPA's more automated (but less predictable) features. +_Comparison reviewed against Hibernate ORM 7.4.5. Last reviewed: 2026-07-15._ + | Aspect | Storm | JPA/Hibernate | |--------|-------|------------------------------------------| | **Entities** | Immutable records/data classes | Mutable classes with getters/setters | @@ -17873,6 +17877,8 @@ JPA (typically implemented by Hibernate) is the most widely used persistence fra Spring Data JPA wraps JPA with a repository abstraction that derives query implementations from method names. It reduces boilerplate but inherits all of JPA's runtime complexity (proxies, managed state, lazy loading). Storm's Spring integration provides similar repository convenience with explicit query bodies instead of naming conventions. +_Comparison reviewed against Spring Data JPA 4.1.0. Last reviewed: 2026-07-15._ + | Aspect | Storm | Spring Data JPA | |--------|-------|-----------------| | **Foundation** | Custom ORM | JPA/Hibernate | @@ -17900,6 +17906,8 @@ Spring Data JPA wraps JPA with a repository abstraction that derives query imple MyBatis is a SQL mapper that gives you full control over every query. You write SQL in XML files or annotations and map results to POJOs manually. Storm sits at a higher abstraction level, inferring SQL from entity definitions while still allowing raw SQL when needed. The trade-off is flexibility vs. automation: MyBatis never generates SQL for you, while Storm handles the common cases and lets you drop to raw SQL for complex queries. +_Comparison reviewed against MyBatis 3.5.19. Last reviewed: 2026-07-15._ + | Aspect | Storm | MyBatis | |--------|-------|---------| | **Approach** | Stateless ORM | SQL mapper | @@ -17933,6 +17941,8 @@ MyBatis is a SQL mapper that gives you full control over every query. You write jOOQ generates Java code from your database schema, providing a type-safe SQL DSL that mirrors the structure of your tables. Storm also treats the database schema as the source of truth, but instead of generating code from the schema, you write entity definitions that reflect it, and the metamodel is generated from those entities. Both frameworks provide compile-time type safety, but queries look very different. jOOQ excels at complex SQL (window functions, CTEs, recursive queries) where its DSL closely follows SQL syntax, but this means every join, column reference, and condition must be spelled out explicitly. Storm queries are more concise: the metamodel and automatic join derivation from `@FK` annotations let you write queries that focus on what you want rather than how to join it. Storm excels at entity-oriented operations where automatic relationship handling and repository patterns reduce boilerplate. +_Comparison reviewed against jOOQ 3.21.6. Last reviewed: 2026-07-15._ + | Aspect | Storm | jOOQ | |--------|-------|------| | **Approach** | Schema-reflective ORM | Schema-driven code generation | @@ -17964,6 +17974,8 @@ jOOQ generates Java code from your database schema, providing a type-safe SQL DS JDBI is a lightweight SQL convenience library that sits just above JDBC. It handles parameter binding, result mapping, and connection management without imposing an object model. Storm provides more structure with entity definitions, automatic relationship loading, and a repository pattern. Choose JDBI when you want minimal abstraction and full SQL control; choose Storm when you want the framework to handle common patterns while still allowing raw SQL escape hatches. +_Comparison reviewed against JDBI 3.54.0. Last reviewed: 2026-07-15._ + | Aspect | Storm | JDBI | |--------|-------|------| | **Level** | Stateless ORM | Low-level SQL mapping | @@ -17989,6 +18001,8 @@ JDBI is a lightweight SQL convenience library that sits just above JDBC. It hand Jimmer is a modern Kotlin and Java ORM that, like Storm, is built on immutable entities and a stateless model with no persistence context, and it eliminates the N+1 problem by design. The two frameworks share a great deal of philosophy. Where they differ is conciseness and concept count. Jimmer trades some concision for GraphQL-style dynamic object fetching: entities are interfaces you interact with through generated drafts, reading a foreign-key id without loading the association requires an extra `@IdView` property, and every query ends in an explicit `select` with an object fetcher. That fetcher, together with Jimmer's dedicated DTO language, is a genuine strength for shape-controlled API responses. Storm keeps the model a plain data class and the query a single line, and treats any data class as a result type. +_Comparison reviewed against Jimmer 0.11.0. Last reviewed: 2026-07-15._ + Defining an entity: ```kotlin @@ -18186,6 +18200,8 @@ Storm intentionally provides schema validation but no schema-generation or migra Ktorm is a lightweight Kotlin ORM that uses entity interfaces and DSL-based table definitions. It requires no code generation and has minimal dependencies. Storm differs primarily in its use of immutable data classes (instead of mutable interfaces), automatic relationship loading, and optional metamodel generation for compile-time type safety. +_Comparison reviewed against Ktorm 4.2.1. Last reviewed: 2026-07-15._ + | Aspect | Storm | Ktorm | | --- | --- | --- | | **Language** | Kotlin + Java | Kotlin only |