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
41 changes: 23 additions & 18 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -45,29 +45,29 @@ Maven (Java)::
<dependency>
<groupId>st.orm</groupId>
<artifactId>storm-java21</artifactId>
<version>1.8.0</version>
<version>1.8.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>st.orm</groupId>
<artifactId>storm-core</artifactId>
<version>1.8.0</version>
<version>1.8.1</version>
<scope>runtime</scope>
</dependency>
----
Gradle (Java)::
+
[source,groovy]
----
implementation 'st.orm:storm-java21:1.8.0'
runtimeOnly 'st.orm:storm-core:1.8.0'
implementation 'st.orm:storm-java21:1.8.1'
runtimeOnly 'st.orm:storm-core:1.8.1'
----
Gradle (Kotlin)::
+
[source,groovy]
----
implementation 'st.orm:storm-kotlin:1.8.0'
runtimeOnly 'st.orm:storm-core:1.8.0'
implementation 'st.orm:storm-kotlin:1.8.1'
runtimeOnly 'st.orm:storm-core:1.8.1'
----
====

Expand Down Expand Up @@ -96,7 +96,7 @@ Java::
record City(@PK Integer id,
String name,
long population
) implements Entity<City, Integer> {}
) implements Entity<Integer> {}

record User(@PK Integer id,
String email,
Expand Down Expand Up @@ -1192,15 +1192,15 @@ Maven::
<dependency>
<groupId>st.orm</groupId>
<artifactId>storm-oracle</artifactId>
<version>1.8.0</version>
<version>1.8.1</version>
<scope>runtime</scope>
</dependency>
----
Gradle::
+
[source,groovy]
----
runtimeOnly 'st.orm:storm-oracle:1.8.0'
runtimeOnly 'st.orm:storm-oracle:1.8.1'
----
====

Expand All @@ -1222,15 +1222,15 @@ Maven::
<dependency>
<groupId>st.orm</groupId>
<artifactId>storm-metamodel-processor</artifactId>
<version>1.8.0</version>
<version>1.8.1</version>
<scope>provided</scope>
</dependency>
----
Gradle::
+
[source,groovy]
----
annotationProcessor 'st.orm:storm-metamodel-processor:1.8.0'
annotationProcessor 'st.orm:storm-metamodel-processor:1.8.1'
----
====

Expand Down Expand Up @@ -1294,21 +1294,21 @@ Maven (Jackson)::
<dependency>
<groupId>st.orm</groupId>
<artifactId>storm-jackson</artifactId>
<version>1.8.0</version>
<version>1.8.1</version>
<scope>compile</scope>
</dependency>
----
Gradle (Jackson)::
+
[source,groovy]
----
implementation 'st.orm:storm-jackson:1.8.0'
implementation 'st.orm:storm-jackson:1.8.1'
----
Gradle (Kotlinx Serialization)::
+
[source,groovy]
----
implementation 'st.orm:storm-kotlinx-serialization:1.8.0'
implementation 'st.orm:storm-kotlinx-serialization:1.8.1'
----
====

Expand Down Expand Up @@ -1431,6 +1431,11 @@ EntityRepository, a DSL query, or a SQL template. This observed state is used as
Dirty checking is only applied when updates are executed through an EntityRepository. Manual SQL updates, bulk
statements, or custom queries bypass dirty checking entirely and may leave in-memory entities stale.

Unless configured otherwise, entity observation is automatically disabled for `READ_UNCOMMITTED` transactions. At this
isolation level, the application expects to see uncommitted changes from other transactions. Caching observed state
would mask these changes, contradicting the requested isolation semantics. When observation is disabled, dirty checking
treats all entities as dirty, resulting in full-row updates.

Dirty checking affects both whether an UPDATE is issued and how that UPDATE is constructed. This behavior is
controlled by UpdateMode and by the dirty checking strategy.

Expand Down Expand Up @@ -1598,15 +1603,15 @@ Maven (Java)::
<dependency>
<groupId>st.orm</groupId>
<artifactId>storm-spring</artifactId>
<version>1.8.0</version>
<version>1.8.1</version>
<scope>compile</scope>
</dependency>
----
Gradle (Java)::
+
[source,groovy]
----
implementation 'st.orm:storm-spring:1.8.0'
implementation 'st.orm:storm-spring:1.8.1'
----
Maven (Kotlin)::
+
Expand All @@ -1615,15 +1620,15 @@ Maven (Kotlin)::
<dependency>
<groupId>st.orm</groupId>
<artifactId>storm-kotlin-spring</artifactId>
<version>1.8.0</version>
<version>1.8.1</version>
<scope>compile</scope>
</dependency>
----
Gradle (Kotlin)::
+
[source,groovy]
----
implementation 'st.orm:storm-kotlin-spring:1.8.0'
implementation 'st.orm:storm-kotlin-spring:1.8.1'
----
====

Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
</properties>
<groupId>st.orm</groupId>
<artifactId>storm-framework</artifactId>
<version>1.8.0</version>
<version>1.8.1</version>
<packaging>pom</packaging>
<name>Storm Framework</name>
<description>A SQL Template and ORM framework, focusing on modernizing and simplifying database programming.</description>
Expand Down
2 changes: 1 addition & 1 deletion storm-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<parent>
<groupId>st.orm</groupId>
<artifactId>storm-framework</artifactId>
<version>1.8.0</version>
<version>1.8.1</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>storm-core</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,15 +290,14 @@ public E insertAndFetch(@Nonnull E entity) {
}

/**
* Returns the entity cache for the current transaction, or null.
* Returns the entity cache for the current transaction, if available.
*
* @return the entity cache for the current transaction, or null.
* @return the entity cache for the current transaction, or empty if not available.
* @since 1.7
*/
protected Optional<EntityCache<E, ID>> entityCache() {
//noinspection unchecked
return TRANSACTION_TEMPLATE.currentContext()
.filter(ctx -> !ctx.isReadOnly())
.map(ctx -> (EntityCache<E, ID>) ctx.entityCache(model().type()));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
Expand All @@ -35,6 +36,42 @@ public class DefaultTransactionTemplateProviderImpl implements TransactionTempla
private static final Object SPRING_CTX_RESOURCE_KEY =
DefaultTransactionTemplateProviderImpl.class.getName() + ".SPRING_TX_CONTEXT";

/**
* Minimum transaction isolation level required for entity caching to be enabled.
*
* <p>Transactions with an isolation level below this threshold will not use entity caching, which means dirty
* checking will treat all entities as dirty (resulting in full-row updates). This prevents the entity cache from
* masking changes that the application expects to see at lower isolation levels.</p>
*
* <p>The default value is {@link Connection#TRANSACTION_READ_COMMITTED}, meaning entity caching is disabled only
* for {@code READ_UNCOMMITTED} transactions. This can be overridden using the system property
* {@code storm.entityCache.minIsolationLevel}.</p>
*/
private static final int MIN_ISOLATION_LEVEL_FOR_CACHE = parseMinIsolationLevel();

private static int parseMinIsolationLevel() {
String value = System.getProperty("storm.entityCache.minIsolationLevel");
if (value == null || value.isBlank()) {
return Connection.TRANSACTION_READ_COMMITTED;
}
value = value.trim().toUpperCase();
return switch (value) {
case "NONE", "0" -> Connection.TRANSACTION_NONE;
case "READ_UNCOMMITTED", "1" -> Connection.TRANSACTION_READ_UNCOMMITTED;
case "READ_COMMITTED", "2" -> Connection.TRANSACTION_READ_COMMITTED;
case "REPEATABLE_READ", "4" -> Connection.TRANSACTION_REPEATABLE_READ;
case "SERIALIZABLE", "8" -> Connection.TRANSACTION_SERIALIZABLE;
default -> {
try {
yield Integer.parseInt(value);
} catch (NumberFormatException e) {
throw new PersistenceException(
"Invalid value for storm.entityCache.minIsolationLevel: '%s'.".formatted(value));
}
}
};
}

@Override
public TransactionTemplate getTransactionTemplate() {
return new TransactionTemplate() {
Expand Down Expand Up @@ -126,6 +163,13 @@ public boolean isReadOnly() {

@Override
public EntityCache<? extends Entity<?>, ?> entityCache(@Nonnull Class<? extends Entity<?>> entityType) {
// Check if entity caching is disabled for this isolation level.
Integer isolationLevel = springReflection.getCurrentTransactionIsolationLevel();
// Spring returns null when no explicit isolation level is set (database default).
// In that case, we assume the database default (typically READ_COMMITTED or higher) and enable caching.
if (isolationLevel != null && isolationLevel < MIN_ISOLATION_LEVEL_FOR_CACHE) {
return null;
}
// We use computeIfAbsent so the "get or create" is a single operation.
//
// Why:
Expand Down Expand Up @@ -158,6 +202,7 @@ private static final class SpringReflection {

private final Method isActualTransactionActive;
private final Method isCurrentTransactionReadOnly;
private final Method getCurrentTransactionIsolationLevel;
private final Method getResource;
private final Method bindResource;
private final Method registerSynchronization;
Expand All @@ -167,6 +212,7 @@ private static final class SpringReflection {
private SpringReflection(
Method isActualTransactionActive,
Method isCurrentTransactionReadOnly,
Method getCurrentTransactionIsolationLevel,
Method getResource,
Method bindResource,
Method registerSynchronization,
Expand All @@ -175,6 +221,7 @@ private SpringReflection(
) {
this.isActualTransactionActive = isActualTransactionActive;
this.isCurrentTransactionReadOnly = isCurrentTransactionReadOnly;
this.getCurrentTransactionIsolationLevel = getCurrentTransactionIsolationLevel;
this.getResource = getResource;
this.bindResource = bindResource;
this.registerSynchronization = registerSynchronization;
Expand All @@ -188,6 +235,7 @@ static SpringReflection tryLoad() {
Class<?> tsm = Class.forName(TSM_FQCN, false, classLoader);
Method isActualTransactionActive = tsm.getMethod("isActualTransactionActive");
Method isCurrentTransactionReadOnly = tsm.getMethod("isCurrentTransactionReadOnly");
Method getCurrentTransactionIsolationLevel = tsm.getMethod("getCurrentTransactionIsolationLevel");
Method getResource = tsm.getMethod("getResource", Object.class);
Method bindResource = tsm.getMethod("bindResource", Object.class, Object.class);
// Cleanup hooks (may not exist in very old Spring).
Expand All @@ -204,6 +252,7 @@ static SpringReflection tryLoad() {
return new SpringReflection(
isActualTransactionActive,
isCurrentTransactionReadOnly,
getCurrentTransactionIsolationLevel,
getResource,
bindResource,
registerSynchronization,
Expand Down Expand Up @@ -231,6 +280,14 @@ boolean isCurrentTransactionReadOnly() {
}
}

Integer getCurrentTransactionIsolationLevel() {
try {
return (Integer) getCurrentTransactionIsolationLevel.invoke(null);
} catch (Throwable t) {
return null;
}
}

Object getResource(Object key) {
try {
return getResource.invoke(null, key);
Expand Down
Loading