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
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,33 @@ 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). 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 Micrometer binding lands with #203 and #207).
- `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) { }`.

### 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.

### 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`.

## [1.12.0] - 2026-07-06

Feature release centered on the reified Kotlin query API. Breaking changes are accepted with no deprecation shims (1.12 policy).
Expand Down
2 changes: 1 addition & 1 deletion docs/api-kotlin.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ The Kotlin API does not depend on any preview features. All APIs are stable and

### storm-kotlin-spring

Spring Framework integration for Kotlin. Provides `RepositoryBeanFactoryPostProcessor` for repository auto-discovery and injection, `@EnableTransactionIntegration` for bridging Storm's programmatic transactions with Spring's `@Transactional`, and transaction-aware coroutine support. Add this module when you use Spring Framework without Spring Boot.
Spring Framework integration for Kotlin. Provides `RepositoryBeanFactoryPostProcessor` for repository auto-discovery and injection, and `springOrmTemplate` for composing a template that bridges Storm's programmatic transactions with Spring's `@Transactional`. Add this module when you use Spring Framework without Spring Boot.

```kotlin
implementation("st.orm:storm-kotlin-spring:@@STORM_VERSION@@")
Expand Down
25 changes: 15 additions & 10 deletions docs/spring-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,15 +114,18 @@ class ORMConfiguration(private val dataSource: DataSource) {

### Transaction Integration

By default, Storm manages its own transactions independently of Spring's transaction context. The `@EnableTransactionIntegration` annotation bridges the two systems so that Storm's programmatic `transaction` and `transactionBlocking` blocks participate in Spring-managed transactions. Without this annotation, a transaction block inside a `@Transactional` method would open a separate database connection and transaction.
A template created with `dataSource.orm` manages its own transactions, independently of Spring's transaction context: a transaction block inside a `@Transactional` method would open a separate database connection and transaction. To wire a template to Spring's transaction management instead, compose it with `springOrmTemplate`, which hands the Spring-aware connection and transaction providers to that specific template. Since 1.13 the integration is per template rather than JVM-global, so multiple application contexts, and plain templates next to Spring-managed ones, coexist without interference.

```kotlin
@EnableTransactionIntegration
@Configuration
class ORMConfiguration(private val dataSource: DataSource) {
@EnableTransactionManagement
class ORMConfiguration {

@Bean
fun ormTemplate(): ORMTemplate = dataSource.orm
fun ormTemplate(
dataSource: DataSource,
transactionManagers: ObjectProvider<PlatformTransactionManager>,
): ORMTemplate = springOrmTemplate(dataSource) { transactionManagers.orderedStream().toList() }
}
```

Expand Down Expand Up @@ -460,9 +463,9 @@ The Spring Boot Starter modules provide zero-configuration setup for Storm. Add

The starter auto-configures:

1. **`ORMTemplate` bean** created from the auto-configured `DataSource`. If you define your own `ORMTemplate` bean, the auto-configured one backs off.
1. **`ORMTemplate` bean** created from the auto-configured `DataSource`, composed with the Spring-aware integration strategies below. If you define your own `ORMTemplate` bean, the auto-configured one backs off.
2. **Repository scanning** via `AutoConfiguredRepositoryBeanFactoryPostProcessor`, which discovers repository interfaces in the `@SpringBootApplication` base package (and its sub-packages). If you define your own `RepositoryBeanFactoryPostProcessor` bean, the auto-configured one backs off.
3. **Transaction integration** (Kotlin only) by automatically activating `SpringTransactionConfiguration`, removing the need for `@EnableTransactionIntegration`.
3. **Transaction integration** through Spring-aware `ConnectionProvider` and `TransactionTemplateProvider` beans, contributed when a `PlatformTransactionManager` is present and handed to the template it creates. Nothing is registered globally: each application context gets its own, correctly matched transaction integration. Define your own `ConnectionProvider` or `TransactionTemplateProvider` bean to override, and optionally contribute `ExceptionMapper` or `QueryObserver` beans, which are applied to the template the same way.
4. **Configuration properties** bound from `storm.*` in `application.yml`/`application.properties`, passed to the `ORMTemplate` via `StormConfig`.

### Minimal Spring Boot Setup (with Starter)
Expand Down Expand Up @@ -549,11 +552,13 @@ If you use the integration module directly (without the starter), you need to co
class Application

@Configuration
@EnableTransactionIntegration
class StormConfig(private val dataSource: DataSource) {
class StormConfig {

@Bean
fun ormTemplate() = dataSource.orm
fun ormTemplate(
dataSource: DataSource,
transactionManagers: ObjectProvider<PlatformTransactionManager>,
): ORMTemplate = springOrmTemplate(dataSource) { transactionManagers.orderedStream().toList() }
}

@Configuration
Expand Down Expand Up @@ -586,7 +591,7 @@ public void doWork() {

## Transaction Propagation

When `@EnableTransactionIntegration` is active, Storm's programmatic transactions participate in Spring's transaction propagation. This means a `transaction` or `transactionBlocking` block checks for an existing Spring-managed transaction before starting a new one. If a transaction already exists, the block joins it. If not, it creates a new independent transaction.
When a template is wired to Spring's transaction management (via `springOrmTemplate` or the starter), Storm's programmatic transactions participate in Spring's transaction propagation. This means a `transaction` or `transactionBlocking` block checks for an existing Spring-managed transaction before starting a new one. If a transaction already exists, the block joins it. If not, it creates a new independent transaction.

Understanding this behavior is important for controlling atomicity. When multiple operations must commit or roll back as a unit, they need to share the same transaction. When operations should be independent (for example, logging that should persist even if the main operation fails), they need separate transactions.

Expand Down
27 changes: 18 additions & 9 deletions docs/transactions.md
Original file line number Diff line number Diff line change
Expand Up @@ -971,43 +971,52 @@ withTransactionOptionsBlocking(isolation = SERIALIZABLE) {
}
```

### How Transactions Bind to Templates

Since 1.13, a `transaction` or `transactionBlocking` block binds to the first `ORMTemplate` that executes inside it. Opening the block only records the requested options (propagation, isolation, timeout, read-only); the actual transaction is opened by that first template's transaction provider. This means the block automatically uses whatever transaction system the template is configured with, whether that is Storm's own JDBC transactions or a platform bridge such as Spring's transaction management. A block that never touches a template completes as a no-op.

Templates that should share a transaction must use the same transaction provider instance. This is automatic for repositories of one application (the Spring Boot starter and the Ktor plugin configure one provider per application context or plugin installation). Mixing templates with *different* transaction providers inside one block fails fast with a descriptive error, since a single commit cannot span two transaction systems.

### Spring-Managed Transactions

While Storm's programmatic transaction API works standalone, many applications use Spring's transaction management for its declarative `@Transactional` support and integration with other Spring components. Storm integrates seamlessly with Spring's transaction management.

When `@EnableTransactionIntegration` is configured, Storm's programmatic `transaction` blocks automatically detect and participate in Spring-managed transactions. This gives you the best of both worlds: Spring's declarative transaction boundaries with Storm's coroutine-friendly transaction blocks.
When a template is wired to Spring's transaction management, Storm's programmatic `transactionBlocking` blocks run through Spring's `PlatformTransactionManager` and participate in Spring-managed transactions. This gives you the best of both worlds: Spring's declarative transaction boundaries with Storm's programmatic transaction blocks. The suspending `transaction` variant is not supported with Spring-managed transactions; use `transactionBlocking` there.

#### Configuration

Enable Spring integration in your configuration class:
The Spring Boot starter wires this automatically when a `PlatformTransactionManager` is present. Without the starter, compose the template with `springOrmTemplate`:

```kotlin
@EnableTransactionIntegration
@Configuration
class ORMConfiguration(private val dataSource: DataSource) {
@EnableTransactionManagement
class ORMConfiguration {
@Bean
fun ormTemplate() = ORMTemplate.of(dataSource)
fun ormTemplate(
dataSource: DataSource,
transactionManagers: ObjectProvider<PlatformTransactionManager>,
): ORMTemplate = springOrmTemplate(dataSource) { transactionManagers.orderedStream().toList() }
}
```

#### Combining Declarative and Programmatic Transactions

You can use Spring's `@Transactional` annotation alongside Storm's programmatic `transaction` blocks. Storm will join the existing Spring transaction:
You can use Spring's `@Transactional` annotation alongside Storm's programmatic `transactionBlocking` blocks. Storm will join the existing Spring transaction:

```kotlin
@Service
class UserService(private val orm: ORMTemplate) {

@Transactional
suspend fun createUserWithOrders(user: User, orders: List<Order>) {
fun createUserWithOrders(user: User, orders: List<Order>) {
// Spring starts the transaction

transaction {
transactionBlocking {
// Storm joins the Spring transaction (REQUIRED propagation by default)
orm insert user
}

transaction {
transactionBlocking {
// Still in the same Spring transaction
orders.forEach { orm insert it }
}
Expand Down
14 changes: 5 additions & 9 deletions storm-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,14 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<!-- Run tests on the classpath: the test suite registers Spring-aware ConnectionProvider and
TransactionTemplateProvider implementations via META-INF/services (see st.orm.core.testsupport),
which ServiceLoader only picks up for the unnamed module. Classpath execution also resolves
jakarta.persistence for @DataJpaTest without module-path workarounds. -->
<useModulePath>false</useModulePath>
<argLine>
@{argLine}
--add-opens java.base/java.lang=ALL-UNNAMED
--add-opens storm.core/st.orm.core=ALL-UNNAMED
--add-opens storm.core/st.orm.core.repository=ALL-UNNAMED
--add-opens storm.core/st.orm.core.repository.spring=ALL-UNNAMED
--add-opens storm.core/st.orm.core.model=ALL-UNNAMED
<!-- storm.core only `requires static jakarta.persistence`, so the module is not resolved at
runtime. @DataJpaTest boots Hibernate/JPA (on the classpath) which needs
jakarta.persistence at runtime; resolve it explicitly so the JPA tests run on the module
path too. -->
--add-modules jakarta.persistence
-Dstorm.ansi_escaping=true
</argLine>
</configuration>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,8 @@
import st.orm.core.spi.CacheRetention;
import st.orm.core.spi.EntityCache;
import st.orm.core.spi.EntityCacheMetrics;
import st.orm.core.spi.Providers;
import st.orm.core.spi.TransactionContext;
import st.orm.core.spi.TransactionTemplate;
import st.orm.core.spi.TransactionScope;
import st.orm.core.template.Column;
import st.orm.core.template.Model;
import st.orm.core.template.ORMTemplate;
Expand Down Expand Up @@ -82,8 +81,6 @@ public class EntityRepositoryImpl<E extends Entity<ID>, ID>
*/
private static final ThreadLocal<Boolean> CALLBACK_ACTIVE = ThreadLocal.withInitial(() -> Boolean.FALSE);

private final TransactionTemplate TRANSACTION_TEMPLATE = Providers.getTransactionTemplate();

protected final int defaultBatchSize;
protected final List<Column> primaryKeyColumns;
protected final GenerationStrategy generationStrategy;
Expand Down Expand Up @@ -672,10 +669,18 @@ public E insertAndFetch(@Nonnull E entity) {
*/
protected Optional<EntityCache<E, ID>> entityCache() {
//noinspection unchecked
return TRANSACTION_TEMPLATE.currentContext()
return currentTransactionContext()
.map(ctx -> (EntityCache<E, ID>) ctx.entityCache(model().type(), cacheRetention));
}

/**
* Returns the transaction context that is active for this repository's template, or empty when no transaction is
* active. This is an observing lookup: it never starts a transaction.
*/
private Optional<TransactionContext> currentTransactionContext() {
return Optional.ofNullable(TransactionScope.peekContext(ormTemplate.transactionTemplateProvider()));
}

/**
* Returns true if the transaction isolation level is {@code REPEATABLE_READ} or higher.
*
Expand All @@ -686,7 +691,7 @@ protected Optional<EntityCache<E, ID>> entityCache() {
* @since 1.8
*/
protected boolean isRepeatableRead() {
return TRANSACTION_TEMPLATE.currentContext()
return currentTransactionContext()
.map(TransactionContext::isRepeatableRead)
.orElse(false);
}
Expand Down
Loading
Loading