Skip to content

Commit efade5e

Browse files
authored
Feature/storm 55 (#56)
Entity Cache * Enabled in read-only transactions. * Disabled for READ_UNCOMMITTED to respect isolation semantics. * Caching provides optimization without affecting correctness; versioning remains advised for concurrent updates. Performance * Cache / interner lookup extracts primary key before construction, avoiding full entity equality checks. * Interner uses primary key based lookups instead of equality checks for entities. * Interner is used when entity cache is not applicable and is scoped to the result set to prevent duplicate objects. * Queries returning the same entity within a result set or transaction benefit from caching / interning.
1 parent ec126f0 commit efade5e

34 files changed

Lines changed: 943 additions & 137 deletions

File tree

README.adoc

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -45,29 +45,29 @@ Maven (Java)::
4545
<dependency>
4646
<groupId>st.orm</groupId>
4747
<artifactId>storm-java21</artifactId>
48-
<version>1.8.0</version>
48+
<version>1.8.1</version>
4949
<scope>compile</scope>
5050
</dependency>
5151
<dependency>
5252
<groupId>st.orm</groupId>
5353
<artifactId>storm-core</artifactId>
54-
<version>1.8.0</version>
54+
<version>1.8.1</version>
5555
<scope>runtime</scope>
5656
</dependency>
5757
----
5858
Gradle (Java)::
5959
+
6060
[source,groovy]
6161
----
62-
implementation 'st.orm:storm-java21:1.8.0'
63-
runtimeOnly 'st.orm:storm-core:1.8.0'
62+
implementation 'st.orm:storm-java21:1.8.1'
63+
runtimeOnly 'st.orm:storm-core:1.8.1'
6464
----
6565
Gradle (Kotlin)::
6666
+
6767
[source,groovy]
6868
----
69-
implementation 'st.orm:storm-kotlin:1.8.0'
70-
runtimeOnly 'st.orm:storm-core:1.8.0'
69+
implementation 'st.orm:storm-kotlin:1.8.1'
70+
runtimeOnly 'st.orm:storm-core:1.8.1'
7171
----
7272
====
7373

@@ -96,7 +96,7 @@ Java::
9696
record City(@PK Integer id,
9797
String name,
9898
long population
99-
) implements Entity<City, Integer> {}
99+
) implements Entity<Integer> {}
100100
101101
record User(@PK Integer id,
102102
String email,
@@ -1192,15 +1192,15 @@ Maven::
11921192
<dependency>
11931193
<groupId>st.orm</groupId>
11941194
<artifactId>storm-oracle</artifactId>
1195-
<version>1.8.0</version>
1195+
<version>1.8.1</version>
11961196
<scope>runtime</scope>
11971197
</dependency>
11981198
----
11991199
Gradle::
12001200
+
12011201
[source,groovy]
12021202
----
1203-
runtimeOnly 'st.orm:storm-oracle:1.8.0'
1203+
runtimeOnly 'st.orm:storm-oracle:1.8.1'
12041204
----
12051205
====
12061206

@@ -1222,15 +1222,15 @@ Maven::
12221222
<dependency>
12231223
<groupId>st.orm</groupId>
12241224
<artifactId>storm-metamodel-processor</artifactId>
1225-
<version>1.8.0</version>
1225+
<version>1.8.1</version>
12261226
<scope>provided</scope>
12271227
</dependency>
12281228
----
12291229
Gradle::
12301230
+
12311231
[source,groovy]
12321232
----
1233-
annotationProcessor 'st.orm:storm-metamodel-processor:1.8.0'
1233+
annotationProcessor 'st.orm:storm-metamodel-processor:1.8.1'
12341234
----
12351235
====
12361236

@@ -1294,21 +1294,21 @@ Maven (Jackson)::
12941294
<dependency>
12951295
<groupId>st.orm</groupId>
12961296
<artifactId>storm-jackson</artifactId>
1297-
<version>1.8.0</version>
1297+
<version>1.8.1</version>
12981298
<scope>compile</scope>
12991299
</dependency>
13001300
----
13011301
Gradle (Jackson)::
13021302
+
13031303
[source,groovy]
13041304
----
1305-
implementation 'st.orm:storm-jackson:1.8.0'
1305+
implementation 'st.orm:storm-jackson:1.8.1'
13061306
----
13071307
Gradle (Kotlinx Serialization)::
13081308
+
13091309
[source,groovy]
13101310
----
1311-
implementation 'st.orm:storm-kotlinx-serialization:1.8.0'
1311+
implementation 'st.orm:storm-kotlinx-serialization:1.8.1'
13121312
----
13131313
====
13141314

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

1434+
Unless configured otherwise, entity observation is automatically disabled for `READ_UNCOMMITTED` transactions. At this
1435+
isolation level, the application expects to see uncommitted changes from other transactions. Caching observed state
1436+
would mask these changes, contradicting the requested isolation semantics. When observation is disabled, dirty checking
1437+
treats all entities as dirty, resulting in full-row updates.
1438+
14341439
Dirty checking affects both whether an UPDATE is issued and how that UPDATE is constructed. This behavior is
14351440
controlled by UpdateMode and by the dirty checking strategy.
14361441

@@ -1598,15 +1603,15 @@ Maven (Java)::
15981603
<dependency>
15991604
<groupId>st.orm</groupId>
16001605
<artifactId>storm-spring</artifactId>
1601-
<version>1.8.0</version>
1606+
<version>1.8.1</version>
16021607
<scope>compile</scope>
16031608
</dependency>
16041609
----
16051610
Gradle (Java)::
16061611
+
16071612
[source,groovy]
16081613
----
1609-
implementation 'st.orm:storm-spring:1.8.0'
1614+
implementation 'st.orm:storm-spring:1.8.1'
16101615
----
16111616
Maven (Kotlin)::
16121617
+
@@ -1615,15 +1620,15 @@ Maven (Kotlin)::
16151620
<dependency>
16161621
<groupId>st.orm</groupId>
16171622
<artifactId>storm-kotlin-spring</artifactId>
1618-
<version>1.8.0</version>
1623+
<version>1.8.1</version>
16191624
<scope>compile</scope>
16201625
</dependency>
16211626
----
16221627
Gradle (Kotlin)::
16231628
+
16241629
[source,groovy]
16251630
----
1626-
implementation 'st.orm:storm-kotlin-spring:1.8.0'
1631+
implementation 'st.orm:storm-kotlin-spring:1.8.1'
16271632
----
16281633
====
16291634

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
</properties>
1717
<groupId>st.orm</groupId>
1818
<artifactId>storm-framework</artifactId>
19-
<version>1.8.0</version>
19+
<version>1.8.1</version>
2020
<packaging>pom</packaging>
2121
<name>Storm Framework</name>
2222
<description>A SQL Template and ORM framework, focusing on modernizing and simplifying database programming.</description>

storm-core/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<parent>
77
<groupId>st.orm</groupId>
88
<artifactId>storm-framework</artifactId>
9-
<version>1.8.0</version>
9+
<version>1.8.1</version>
1010
<relativePath>../pom.xml</relativePath>
1111
</parent>
1212
<artifactId>storm-core</artifactId>

storm-core/src/main/java/st/orm/core/repository/impl/EntityRepositoryImpl.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -290,15 +290,14 @@ public E insertAndFetch(@Nonnull E entity) {
290290
}
291291

292292
/**
293-
* Returns the entity cache for the current transaction, or null.
293+
* Returns the entity cache for the current transaction, if available.
294294
*
295-
* @return the entity cache for the current transaction, or null.
295+
* @return the entity cache for the current transaction, or empty if not available.
296296
* @since 1.7
297297
*/
298298
protected Optional<EntityCache<E, ID>> entityCache() {
299299
//noinspection unchecked
300300
return TRANSACTION_TEMPLATE.currentContext()
301-
.filter(ctx -> !ctx.isReadOnly())
302301
.map(ctx -> (EntityCache<E, ID>) ctx.entityCache(model().type()));
303302
}
304303

storm-core/src/main/java/st/orm/core/spi/DefaultTransactionTemplateProviderImpl.java

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
import java.lang.reflect.Method;
2424
import java.lang.reflect.Proxy;
25+
import java.sql.Connection;
2526
import java.util.HashMap;
2627
import java.util.Map;
2728
import java.util.Optional;
@@ -35,6 +36,42 @@ public class DefaultTransactionTemplateProviderImpl implements TransactionTempla
3536
private static final Object SPRING_CTX_RESOURCE_KEY =
3637
DefaultTransactionTemplateProviderImpl.class.getName() + ".SPRING_TX_CONTEXT";
3738

39+
/**
40+
* Minimum transaction isolation level required for entity caching to be enabled.
41+
*
42+
* <p>Transactions with an isolation level below this threshold will not use entity caching, which means dirty
43+
* checking will treat all entities as dirty (resulting in full-row updates). This prevents the entity cache from
44+
* masking changes that the application expects to see at lower isolation levels.</p>
45+
*
46+
* <p>The default value is {@link Connection#TRANSACTION_READ_COMMITTED}, meaning entity caching is disabled only
47+
* for {@code READ_UNCOMMITTED} transactions. This can be overridden using the system property
48+
* {@code storm.entityCache.minIsolationLevel}.</p>
49+
*/
50+
private static final int MIN_ISOLATION_LEVEL_FOR_CACHE = parseMinIsolationLevel();
51+
52+
private static int parseMinIsolationLevel() {
53+
String value = System.getProperty("storm.entityCache.minIsolationLevel");
54+
if (value == null || value.isBlank()) {
55+
return Connection.TRANSACTION_READ_COMMITTED;
56+
}
57+
value = value.trim().toUpperCase();
58+
return switch (value) {
59+
case "NONE", "0" -> Connection.TRANSACTION_NONE;
60+
case "READ_UNCOMMITTED", "1" -> Connection.TRANSACTION_READ_UNCOMMITTED;
61+
case "READ_COMMITTED", "2" -> Connection.TRANSACTION_READ_COMMITTED;
62+
case "REPEATABLE_READ", "4" -> Connection.TRANSACTION_REPEATABLE_READ;
63+
case "SERIALIZABLE", "8" -> Connection.TRANSACTION_SERIALIZABLE;
64+
default -> {
65+
try {
66+
yield Integer.parseInt(value);
67+
} catch (NumberFormatException e) {
68+
throw new PersistenceException(
69+
"Invalid value for storm.entityCache.minIsolationLevel: '%s'.".formatted(value));
70+
}
71+
}
72+
};
73+
}
74+
3875
@Override
3976
public TransactionTemplate getTransactionTemplate() {
4077
return new TransactionTemplate() {
@@ -126,6 +163,13 @@ public boolean isReadOnly() {
126163

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

159203
private final Method isActualTransactionActive;
160204
private final Method isCurrentTransactionReadOnly;
205+
private final Method getCurrentTransactionIsolationLevel;
161206
private final Method getResource;
162207
private final Method bindResource;
163208
private final Method registerSynchronization;
@@ -167,6 +212,7 @@ private static final class SpringReflection {
167212
private SpringReflection(
168213
Method isActualTransactionActive,
169214
Method isCurrentTransactionReadOnly,
215+
Method getCurrentTransactionIsolationLevel,
170216
Method getResource,
171217
Method bindResource,
172218
Method registerSynchronization,
@@ -175,6 +221,7 @@ private SpringReflection(
175221
) {
176222
this.isActualTransactionActive = isActualTransactionActive;
177223
this.isCurrentTransactionReadOnly = isCurrentTransactionReadOnly;
224+
this.getCurrentTransactionIsolationLevel = getCurrentTransactionIsolationLevel;
178225
this.getResource = getResource;
179226
this.bindResource = bindResource;
180227
this.registerSynchronization = registerSynchronization;
@@ -188,6 +235,7 @@ static SpringReflection tryLoad() {
188235
Class<?> tsm = Class.forName(TSM_FQCN, false, classLoader);
189236
Method isActualTransactionActive = tsm.getMethod("isActualTransactionActive");
190237
Method isCurrentTransactionReadOnly = tsm.getMethod("isCurrentTransactionReadOnly");
238+
Method getCurrentTransactionIsolationLevel = tsm.getMethod("getCurrentTransactionIsolationLevel");
191239
Method getResource = tsm.getMethod("getResource", Object.class);
192240
Method bindResource = tsm.getMethod("bindResource", Object.class, Object.class);
193241
// Cleanup hooks (may not exist in very old Spring).
@@ -204,6 +252,7 @@ static SpringReflection tryLoad() {
204252
return new SpringReflection(
205253
isActualTransactionActive,
206254
isCurrentTransactionReadOnly,
255+
getCurrentTransactionIsolationLevel,
207256
getResource,
208257
bindResource,
209258
registerSynchronization,
@@ -231,6 +280,14 @@ boolean isCurrentTransactionReadOnly() {
231280
}
232281
}
233282

283+
Integer getCurrentTransactionIsolationLevel() {
284+
try {
285+
return (Integer) getCurrentTransactionIsolationLevel.invoke(null);
286+
} catch (Throwable t) {
287+
return null;
288+
}
289+
}
290+
234291
Object getResource(Object key) {
235292
try {
236293
return getResource.invoke(null, key);

0 commit comments

Comments
 (0)