1.13.0 #358
zantvoort
announced in
Announcements
1.13.0
#358
Replies: 1 comment
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Feature and performance release. Highlights: write sets persist mixed-type entity graphs in one dependency-ordered call; compiled query plans reuse processed statements across executions; GraalVM native image support across all stacks; the Storm Gradle plugin; full Java programmatic-transaction parity with Kotlin; and queries and transactions reported as Micrometer Observations. Read, write, and transaction hot paths are leaner throughout. Integration points are instance-scoped, the Ktor integration uses Ktor's built-in dependency injection, and models are null-marked by default.
Added
orm.writeSet()(and Kotlin's scopedorm.writeSet { }) applies one write action to a mixed-type entity collection, ordering statements by foreign-key dependencies (parents first forinsert/upsert, children first forremove), batching per type per level, and propagating generated keys by instance identity.insert/upsertextend to the insertion closure (transitively reachable unsaved entities), so a multi-level graph persists in one call;update/removewrite exactly the passed entities.AndFetchvariants return persisted state in input order. Cycles, and unsaved members passed toupdate/remove, fail fast before anything is written.getResultGroupedBy(...)/getResultGroupedByRef(...)query-builder terminals (all APIs): run the unchanged select and group rows during hydration into an insertion-ordered, unmodifiable map keyed by a parent path — the one-to-many read in a single query. The newTypedMetamodelrejects grouping by aRef-mapped path at compile time.Refin queries (all APIs): filter, order, group, and select through aRefforeign key with the metamodel (User_.city.country.namewherecityis aRef<City>), with the join for the referenced table materialized on demand. A root select still emits the reference as its foreign-key column with no join, and an unreferenced reference join is pruned; a custom selected column beyond a reference adds the join and selects the column. A path may cross more than one reference across distinct tables. The target's primary key is part of the reference:Ref.id()reads it without fetching, soUser_.city.idresolves to the foreign key column that holds it and needs no join. This makes a path mean the same thing whether the relationship is declared as an entity or aRef, since an entity foreign key already resolved its primary key to that column. Beyond-reference nodes implement the new navigation-onlyNavigablesupertype (notMetamodel), so value operations such asgetResultGroupedBy/getValuedo not compile against them, while the reference node itself stays a value metamodel whose value is theRef(sogetResultGroupedByRefworks).groupByandhavingaccept navigation-only nodes alongsidewhereandorderBy. A reference that returns to a table already on the path, whether a self-reference or a longer cycle, joins that table to itself with one alias per occurrence, so a predicate or ordering resolves against the occurrence the path reached. A nullable reference keeps its outer join in that case, so ordering by a column beyond it does not drop rows. The typed metamodel navigates a cycle two hops deep, since generated metamodels construct their children eagerly and cannot recurse; the query engine has no depth limit, so deeper cyclic paths are named as strings.Refbecomes cheaper to adopt — it removes the eager join from every read while keeping the relationship queryable, so it is the tool for preventing join fan-out in wide or deep graphs.Refas part of the query (all APIs):select().fetch(User_.city, User_.city.country)names the references the statement resolves, selecting the referenced table's columns in place of the foreign key column so the reference comes back loaded andRef.fetch()returns without querying. The record type is unchanged — the field stays aRef, so the same type serves queries that resolve it and queries that do not — and reference identity, equality andunload()are unaffected. The plan is prefix-closed, so namingUser_.city.countryresolvesUser_.cityas well and the deeper path is the only one to write; a reference the plan does not name stays a foreign key column. A reference is always a to-one foreign key, so resolving one widens the row without multiplying it, and a cycle stays bounded by the depth the path names (fetch(Node_.parent.parent)is exactly two levels). A nullable reference keeps its outer join, so a null foreign key yields a null reference.fetchacceptsNavigable<T, ? extends Data>, which rejects scalar and inline-record paths at compile time; a path that crosses no reference, a reference to a sealed type, and a query that does not select a record are each rejected with a descriptive error.Sql.fetchPaths()reports the resolved references of a statement.Ref.getOrThrow()(all APIs) completes the accessor pair:fetch()/fetchOrNull()resolve on demand,getOrThrow()/getOrNull()read what is already loaded.getOrThrow()never queries and fails with a message naming the fix, so pairing it withfetch(...)turns a plan that no longer covers a path into an immediate error instead of a silent query per row.QueryTemplate.plan(...)/QueryBuilder.plan()process a template once into a reusable, immutableQueryPlan, then bind per execution —bind(record),bindValue(value)for primary-key and unique-key lookups,query()for parameter-less statements — skipping the per-call template processing. Repositories reuse cached plans for their fixed-shape operations (singleinsert/update/remove, delete by id/ref,findById/getByIdand ref/unique-key lookups,findAll,count/exists,removeAll); JPA templates and interceptor-customized scopes fall back to per-call processing.native-image.properties.ORMTemplate.builder(dataSource)/builder(connection)(all APIs) with instance-scoped strategies:connectionProvider,transactionTemplateProvider,exceptionMapper,queryObserver.ServiceLoaderdiscovery remains the fallback.ExceptionMapperSPI: maps query-execution failures to the exception thrown to the caller, enabling hierarchies such as Spring'sDataAccessException.QueryObserverSPI: observes query executions (operation, type, execution kind, timing, outcome) for metrics and tracing.DIRECT, orFETCHfor a statement resolving aRef— surfaced as the low-cardinalitystorm.origintag on Micrometer observations, on captured statements instorm-test, and in SQL log summaries.st.orm.sqllogger reports every executed statement at DEBUG (per-type child loggers such asst.orm.sql.Ownernarrow it), and at TRACE renders parameter values into the statement, producing console-ready SQL. Compiled plans and the template cache stay in effect, so the observed path is the production path.sqlLog { },sqlLogContext(), andtransaction { }carry the scope), with a try-with-resourcesSqlLog.open(...)for Java; the summary reports through thest.orm.sql.perflogger, which is also the switch that enables recording. Opt-in call-site attribution names the application frame behind each row (a coroutine context built withsqlLogContext()carries the launch site, so work on another dispatcher is attributed to the frame that launched it), rows produced or affected are counted per statement (a count a driver or an early-closed stream left inexact is a marked lower bound), reads the transaction's entity cache served without a statement are counted alongside the fetches they saved, and an opt-in hydration-shape display appends each read's declared shape, short (j2 c12 d3: joins, columns, graph depth; flat types show none) or full (naming the joined-entity graph), derived from the type at rendering; writes carry none, since a write touches its own table rather than the graph its type declares. Scopes cover the JDBC and JPA template paths alike.storm.sql-log.enabled(Spring Boot) andsqlLog = true(Ktor) wrap each unit of work and name the summary after its boundary. In Spring that is every way work enters the application: HTTP requests through a servlet filter, and@Scheduledtasks and message listeners (@KafkaListener,@RabbitListener,@JmsListener,@SqsListener, and the handler methods of class-level listeners) through a proxy around the entry-point method, named after it (ReportJob.nightly) — matched by annotation name so an absent library costs nothing, withstorm.sql-log.entry-pointsreplacing the set. Thresholds (threshold.statements,threshold.duration) turn the report into a production guardrail that logs only the units exceeding them, at WARN.SqlCapturerecords executions rather than built statements: each captured statement carries its origin, bound parameter values, and duration.springOrmTemplate(dataSource) { transactionManagers }(storm-kotlin-spring): the canonical plain-Spring composition, replacing@EnableTransactionIntegration.st.orm.spring.SpringTransactionTemplateProvider(storm-spring): transaction-scoped entity caching for Java under Spring-managed transactions, without reflective probes.connectionProvider,transactionTemplateProvider,exceptionMapper, andqueryObserverslots oninstall(Storm) { }.ORMTemplateand every repository through Ktor's built-in dependency injection, each under its interface type (val visits: VisitRepository by dependencies); disable withregisterDependencies = false.database("name") { }blocks (own template, repositories, validation, migration, lifecycle), in code or understorm.databases.<name>.*in HOCON; access viaorm("name"),repository<T>("name"), and named dependency injection.storm-micrometermodule:MicrometerQueryObserverreports query executions asstorm.queryMicrometer Observations, overridable via a customObservationConvention.ObservationRegistryis registered, taggedstorm.database=<name>.transactional { }route DSL (storm-ktor): each route runs in its own transaction with the same options astransaction { }, bound to the first template the handler touches.id("st.orm"), Gradle Plugin Portal, BOM-aligned): one application imports the BOM, adds the core dependencies, wires the metamodel processor (KSP or annotation processor), selects the Kotlin compiler-plugin variant matching the project's Kotlin version (2.0 through 2.4), and sets the Java preview flags; astorm { }extension covers opt-outs; configuration-cache compatible. Requires Gradle 8.5+. The plugin is released from the same tag as the artifacts it wires, so its version and the Storm coordinates it adds always match.Transactions.transaction(...)(storm-java21) with Kotlintransaction { }semantics — all seven propagation modes, isolation, timeout, read-only, rollback-only, and callbacks — blocking and virtual-thread friendly. Options via thest.orm.TransactionOptionsrecord.Transaction.onCompletion(committed)(all APIs): the completion callback for work that has to happen either way, such as releasing a lock, receiving whether the transaction committed. The three callback kinds share one registration order, skipping the ones the outcome does not apply to. A failed callback surfaces asTransactionCallbackExceptionwith anisCommitted()flag — the transaction completed and a side effect failed afterwards, which is the case a retry must not repeat — with the first failure as the cause and the rest suppressed; after a rollback caused by an exception, the wrapped failure attaches to that exception as suppressed rather than replacing it.TransactionPropagation,TransactionIsolation,TransactionTimedOutException,UnexpectedRollbackException, and the language-neutralTransactionhandle.Sql.dataType(): the statement's primary entity or projection type, now derived for SELECTs too and reported to query observers.SpringTransactionTemplateProviderrunsTransactions.transaction(...)blocks through Spring'sPlatformTransactionManager, joins active@Transactionaltransactions, and picks the manager per templateDataSource.SpringOrmTemplate.of(dataSource, transactionManagers)is the canonical Java composition.st.orm.spring.boot):StormTransactionAutoConfiguration,StormValidationAutoConfiguration, andStormProperties, used by both starters; keys unchanged.DataAccessExceptionhierarchy:SpringExceptionMappermaps on vendor code withSQLException-subclass and SQL-state fallback, auto-configured by both starters (storm.exception-translation.enabled=falseto disable). Failures without aSQLExceptioncause keep Storm's own exceptions.ObservationRegistrybean is present (Actuator provides one); override with anObservationConventionorQueryObserverbean, or disable viamanagement.observations.enable.storm.query=false.OtelDatabaseObservationConvention(storm-micrometer): opt-in OpenTelemetry database semantic conventions (db.system.name,db.operation.name,db.query.text) on query observations, viastorm.observations.semantic-conventions=otelor the Ktor container.SqlCommenterhook (all stacks) appends per-execution comments after processing, andTraceContextSqlCommenter(storm-micrometer) emits the current span as a W3Ctraceparentcomment. Opt-in (storm.tracing.sql-comments=true|sampled, or the KtorsqlCommenterslot) since per-execution comments defeat statement caching; a sampled-only mode limits the cost to sampled traces, and hostile content is rejected and padded.storm.transactionMicrometer Observation (duration, outcome, propagation, read-only), without double-counting joined blocks;QueryObservergains a default no-op transaction hook.@EnableStormRepositories(basePackages = ...)(storm-spring, both stacks): switches on repository scanning in plain Spring (mirroring@EnableJpaRepositories) and doubles as the explicit override in Spring Boot. Adapter constructors (base packages, template bean name, prefix) let multi-template applications define one bean per repository set.@DataStormTesttest slice in the new storm-spring-boot-test-autoconfigure module (the@DataJpaTestcounterpart): starts only theDataSource, Storm's auto-configuration, and SQL initialization, replaces the data source with an embedded database, and rolls back per test. One module serves both starters on Spring Boot 3 and 4 (spring.test.database.replace=noneopts out for Testcontainers).Changed
groupByandorderBymetamodel overloads resolve a path to the columns a predicate on that path uses: a foreign key contributes its foreign key column(s) on the referencing table, once and without joining the referenced table, and an inline record contributes its component columns. The core API previously expanded a foreign key to every column of the referenced table (joining it, and listing the key columns of a compound-key target twice), while the Kotlin and Java 21 APIs rejected compound foreign keys and inline records outright. All three APIs now share the same expansion, and descending order appliesDESCto every expanded column.Visit_.pet.idnamesvisit.pet_idin predicates, templates,groupByandorderByalike, without joining the referenced table. A single component of a compound key reached through a foreign key still resolves in the referenced table, which is joined on demand. Generated metamodels now flatten by the documented contract: an inline record expands into its component columns and every other node flattens to itself, so generated and reflection-built metamodels agree.getValuereturns null; the annotation processor declared it non-null while KSP already declared it nullable. Both now agree. Value extraction beyond a reference is unaffected, since a beyond-reference node is navigation-only and has no value to report.Stringfor@Json) so a predicate compares against the stored JSON, while value extraction returns the type the record declares. Previously the annotation processor substituted the stored type for both, generating a metamodel that did not compile, so@Jsoncould not be used on any entity that generates one; KSP ignored the annotation entirely, so the same model produced different metamodels per language. The runtime metamodel mistook a converted record field for an inline record, resolving paths to columns the table does not have; it now recognizes the converter as the model always has.@Nullableor inside a@NullUnmarkedscope, nearest marker wins. Nullable is load-bearing — a bare@FKis non-null and joins INNER,@Nullable @FKis optional and joins LEFT. JSpecify stays an optional, reflectively resolved dependency.Constructor.newInstance; models without the generators fall back to reflection. This removes the last reflective call from row mapping — no reflection config for native images, noopensclauses for model packages.transaction { }/transactionBlocking { }bind to the first template that executes inside the block (options recorded, transaction opened on first use); a block that touches no template is a no-op, and mixing transaction providers fails fast.TransactionTemplateSPI is reshaped fromexecute()toopen()/complete()handles for the lazy binding.ServiceLoaderresolution of connection or transaction template providers throws a descriptive error naming the candidates instead of silently picking one; enablement is re-evaluated per resolution.ConnectionProvider/TransactionTemplateProviderbeans (backing off to user beans) and consume optionalExceptionMapper/QueryObserverbeans.ktor.versionnow lives in the parent pom.StormConfiguration→StormPluginConfig(vs core'sStormConfig); application code is unaffected unless it named the type explicitly.TransactionRunner), and the Kotlin transaction enums and exceptions moved fromst.orm.templatetost.orm(imports change, behavior identical).AbstractRepositoryBeanFactoryPostProcessor); the Kotlin adapters moved tost.orm.spring.kotlinand override methods instead of properties (override fun getRepositoryBasePackages(), etc.). Java subclasses are unaffected.ORMTemplateand startup schema validation condition on a singleDataSource: applications with severalDataSourcebeans boot cleanly (the template backs off, or binds the@Primarypool).stormRepositoryProxyingPostProcessor.PlatformTransactionManagerno longer get a Spring-boundConnectionProvider; they fall back to Storm's own JDBC transactions.DataAccessExceptionsubtypes instead ofPersistenceException(auto-configured). Update catch blocks and rollback rules, or setstorm.exception-translation.enabled=falseto keep the previous behavior.EntityCallback.beforeDelete/afterDeleteare renamedbeforeRemove/afterRemove(all APIs), matching theremovemethods that fire them and the naming of the other three callback pairs.deletestays the statement-level vocabulary (delete(),deleteFrom(...)), and a query-builder delete carries no entity, so it does not fire these. Kotlin overrides and Java overrides marked@Overridefail to compile; a Java override without@Overridecompiles and stops being called, so search for the old names.*AndFetchId(s)methods report it carrying the primary key the database assigned, and the*AndFetchmethods report the row as read back — soafterInsertcan finally reference what was just inserted, andafterUpdateonupdateAndFetchsees version increments and trigger-applied changes. The method name at the call site is what determines this; a callback that needs the key has to be driven by a method that reports one. Write sets follow the same rule: keys retrieved only to bind foreign keys on dependent rows stay out of the callbacks, and on the fetch variants every written member is read back, so a callback observes the same thing whether its entity was passed or reached by the insertion closure. Breaking for callbacks that relied onafterInsertreceiving an unset key, or that compared the callback entity against the instance passed in.Fixed
transaction { }block inside a Spring@Transactionalmethod fired itsonCommitcallbacks as soon as the block ended: the callback ran while the transaction was still open, and reported a commit even when Spring went on to roll back. Two mechanisms close this. A block that joined the physical transaction through a query hands its callbacks to the manager that owns it, via the newTransactionHandle.joinedExistingTransaction()/deferCompletion(...)SPI methods. A block that never binds to a template — it only registers callbacks — settles through the new statelessExternalTransactionProviderSPI, which detects the externally managed transaction active on the thread; storm-spring implements it overTransactionSynchronizationManager, carrying no configuration, so itsServiceLoaderdiscovery cannot affect template behavior (per-context configuration stays on the composed providers). In both cases the callbacks register as Spring transaction synchronizations in registration order, and asetRollbackOnly()in an unbound block marks the Spring transaction, mirroring how a joined scope marks its parent (NESTEDstays bounded at its savepoint). AREQUIRES_NEWblock owns its transaction and keeps firing its own callbacks; a callback failure deferred to Spring follows Spring's completion rules and is logged rather than thrown. The result is one contract everywhere: an entity callback registeringonCommitthrough a joining block behaves identically under Storm-managed and Spring-managed transactions.beforeInsertfilling in audit fields was skipped and the row was written without them. The widest case was SQL Server's batchinsertAndFetchIds, which took the override for every generation strategy other thanNONE, identity keys included. MySQL, Oracle, H2 and SQLite were unaffected.rendersTupleComparison(operator, rowCount)and one implementation acts on it. A single row of equality never renders as a tuple — measured on MariaDB, MySQL, PostgreSQL and H2 the expansion ties or beats the tuple for that shape, and it is the shape behind every keyed update, delete and lookup. A multi-row list keeps the tuple, which is where it pays.RowIdentitynormalizes a primary key to the identity the SQL layer binds — an entity-typed id reduces recursively to its own primary key, a ref id to the key it wraps, a composite key component-wise — detected through the pluggable reflection support, so Java records and Kotlin data classes behave alike. Scalar keys are returned unchanged through a per-class cached decision.findAllById,whereId) on an entity whose primary key is itself an entity. The key values resolved by type, which is ambiguous when the key entity's table appears more than once in the root's join graph — a junction row whose second foreign key reaches the same table again — and the query failed withCannot uniquely identify object in expression. Object-expression resolution now falls back to the root's primary key path when the value type matches the primary key type, mirroring the existing fallback for scalar values.Ref<City>that also takes theCityit refers to — could decide the field's type, depending on which constructor the compiler enclosed first. Type resolution andisDataTypenow agree.ProtocolResolverregistered on the application context: a resolver bound to its own scheme, such as Spring Cloud AWS'ss3resolver, was consulted for these lookups and logged a warning per resource, because its backing client bean is not available yet while bean factory post-processing runs.upsertAndFetchIdson SQL Server with an auto-generated primary key. An auto-generated key routes the upsert to insert rather than toMERGE, but the insert branch bound a shared prepared statement and read its generated keys, bypassing the dialect override that exists because the driver cannot report generated keys for a JDBC batch; the call failed withThe statement must be executed before any results can be obtained. The newSqlDialect.supportsBatchGeneratedKeys()names the constraint (defaulttrue,falsefor SQL Server): where a prepared batch cannot report its keys, the branch goes through the publicinsertAndFetchIdsso the dialect emits the statement that carries them, and elsewhere the insert partitions keep sharing one prepared statement.Removed
@EnableTransactionIntegrationandSpringTransactionConfiguration(storm-kotlin-spring): the JVM-global transaction manager list is gone. UsespringOrmTemplate(...)or the starter; multiple contexts in one JVM no longer interfere.ServiceLoaderproviders, and the reflective Spring probes are removed from storm-core and storm-kotlin: a plainORMTemplate.of(dataSource)inside a Spring application runs independently of Spring transactions — compose withspringOrmTemplateor the builder. This also removes the mixed-transactions guard.Providers.getConnection/Providers.releaseConnection: connections are acquired through the template's own connection provider.SpringConnectionProviderImpl,SpringTransactionTemplateProviderImpl, andTransactionAwareConnectionProviderImplare replaced by the publicSpringConnectionProviderandSpringTransactionTemplateProvider.st.orm.spring.impl.ResolverRegistration: 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; the docs include the Koin recipe.@SqlLogand its repository-proxy processing: the annotation covered only repository proxies, disabled compiled plans and the template cache for annotated calls, and stopped capturing at the first suspension point. The name now denotes what the annotation wanted to be: the built-in SQL log (sqlLog { },SqlLog.open(...), and thest.orm.sqllogger tree), which covers every execution without those constraints.Security
DatabaseSchema) — defense in depth; no metadata query is assembled from concatenated values.Reftarget resolves to a StormDatatype before loading it, and does not run static initializers until that check passes.This discussion was created from the release 1.13.0.
All reactions