diff --git a/storm-core/src/main/java/st/orm/core/spi/Providers.java b/storm-core/src/main/java/st/orm/core/spi/Providers.java
index 578e6cb2c..7ef2e1b3e 100644
--- a/storm-core/src/main/java/st/orm/core/spi/Providers.java
+++ b/storm-core/src/main/java/st/orm/core/spi/Providers.java
@@ -235,11 +235,20 @@ public static SqlDialect getSqlDialect() {
return getSqlDialect(StormConfig.defaults());
}
+ /**
+ * Resolves the SQL dialect from the classpath, without a database in view.
+ *
+ *
Enablement is re-evaluated on every resolution, and an ambiguous resolution fails fast.
+ *
+ * @param config the Storm configuration to apply.
+ * @return the SQL dialect.
+ * @throws PersistenceException if no dialect provider is found or the resolution is ambiguous.
+ */
public static SqlDialect getSqlDialect(@Nonnull StormConfig config) {
- return enabled(SQL_DIALECT_PROVIDERS)
- .map(p -> p.getSqlDialect(config))
- .findFirst()
- .orElseThrow();
+ return selectUnique(SQL_DIALECT_PROVIDERS, "SQL dialect provider",
+ "SqlTemplate.withDialect(...), or by binding the template to a DataSource or Connection so the " +
+ "dialect is derived from the database")
+ .getSqlDialect(config);
}
public static SqlDialect getSqlDialect(@Nonnull Predicate super SqlDialectProvider> filter) {
diff --git a/storm-core/src/main/java/st/orm/core/template/SqlTemplate.java b/storm-core/src/main/java/st/orm/core/template/SqlTemplate.java
index 33956d94f..4c5055fb9 100644
--- a/storm-core/src/main/java/st/orm/core/template/SqlTemplate.java
+++ b/storm-core/src/main/java/st/orm/core/template/SqlTemplate.java
@@ -286,14 +286,19 @@ interface BatchListener {
/**
* Returns the SQL dialect used by this template.
*
+ *
When no dialect has been set via {@link #withDialect(SqlDialect)}, the dialect is resolved from the
+ * classpath on first use. That resolution fails with a descriptive exception when multiple dialect providers
+ * are eligible without a defined order.
+ *
* @return the SQL dialect used by this template.
* @since 1.2
*/
SqlDialect dialect();
/**
- * Returns a new SQL template with the specified Storm configuration. The SQL dialect is re-resolved from the
- * provided configuration.
+ * Returns a new SQL template with the specified Storm configuration. A dialect set via
+ * {@link #withDialect(SqlDialect)} is retained; a classpath-resolved dialect is re-resolved under the new
+ * configuration, as dialects capture their configuration at construction.
*
* @param config the Storm configuration to apply.
* @return a new SQL template.
diff --git a/storm-core/src/main/java/st/orm/core/template/impl/JpaTemplateImpl.java b/storm-core/src/main/java/st/orm/core/template/impl/JpaTemplateImpl.java
index 59c27dff8..0ddbbca74 100644
--- a/storm-core/src/main/java/st/orm/core/template/impl/JpaTemplateImpl.java
+++ b/storm-core/src/main/java/st/orm/core/template/impl/JpaTemplateImpl.java
@@ -19,6 +19,7 @@
import static jakarta.persistence.TemporalType.TIME;
import static jakarta.persistence.TemporalType.TIMESTAMP;
import static st.orm.core.template.SqlTemplate.JPA;
+import static st.orm.core.template.impl.LazySupplier.lazy;
import static st.orm.core.template.impl.RecordValidation.validate;
import jakarta.annotation.Nonnull;
@@ -28,6 +29,7 @@
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
+import java.util.function.Supplier;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import javax.sql.DataSource;
@@ -83,9 +85,21 @@ private interface TemplateProcessor {
private final TableAliasResolver tableAliasResolver;
private final Predicate providerFilter;
private final RefFactory refFactory;
- private final SqlTemplate sqlTemplate;
+
+ /**
+ * Built on first use: without a provider filter it consumes the resolved dialect, whose classpath fallback must
+ * stay untouched until the template is actually used.
+ */
+ private final Supplier sqlTemplate;
+
private final StormConfig config;
- private final SqlDialect dialect;
+
+ /**
+ * The dialect of the persistence unit's database, or a classpath fallback resolved on first use when the database
+ * is unknown. Deferring the fallback keeps an ambiguous classpath from failing template construction, so a
+ * provider filter can still be applied via {@link #withProviderFilter(Predicate)}.
+ */
+ private final Supplier dialect;
public JpaTemplateImpl(@Nonnull EntityManager entityManager) {
this(entityManager, StormConfig.defaults());
@@ -112,7 +126,7 @@ public JpaTemplateImpl(@Nonnull EntityManager entityManager, @Nonnull StormConfi
this.config = config;
this.dialect = resolveDialect(entityManager, config);
this.refFactory = new RefFactoryImpl(this, modelBuilder, providerFilter);
- this.sqlTemplate = createSqlTemplate();
+ this.sqlTemplate = lazy(this::createSqlTemplate);
}
/**
@@ -121,19 +135,19 @@ public JpaTemplateImpl(@Nonnull EntityManager entityManager, @Nonnull StormConfi
*
Asking the persistence unit for its data source is the portable way to reach the database: unwrapping an
* entity manager to a {@link java.sql.Connection} is not supported by every provider. A persistence unit
* configured with a connection URL rather than a data source, or one whose database cannot be reached while the
- * template is being built, leaves the database unknown, and the dialect then comes from the classpath as
- * before.
+ * template is being built, leaves the database unknown, and the dialect then comes from the classpath, resolved
+ * on first use.
*/
- private static SqlDialect resolveDialect(@Nonnull EntityManager entityManager, @Nonnull StormConfig config) {
+ private static Supplier resolveDialect(@Nonnull EntityManager entityManager, @Nonnull StormConfig config) {
DataSource dataSource = dataSourceOf(entityManager);
if (dataSource == null) {
- return Providers.getSqlDialect(config);
+ return lazy(() -> Providers.getSqlDialect(config));
}
try {
- return Providers.getSqlDialect(dataSource, config);
+ return new LazySupplier<>(Providers.getSqlDialect(dataSource, config));
} catch (RuntimeException e) {
LOGGER.debug("Failed to determine the database of the persistence unit.", e);
- return Providers.getSqlDialect(config);
+ return lazy(() -> Providers.getSqlDialect(config));
}
}
@@ -163,7 +177,7 @@ private JpaTemplateImpl(@Nonnull TemplateProcessor templateProcessor,
@Nonnull TableAliasResolver tableAliasResolver,
@Nullable Predicate providerFilter,
@Nonnull StormConfig config,
- @Nonnull SqlDialect dialect) {
+ @Nonnull Supplier dialect) {
this.dialect = dialect;
this.templateProcessor = templateProcessor;
this.modelBuilder = modelBuilder;
@@ -171,7 +185,7 @@ private JpaTemplateImpl(@Nonnull TemplateProcessor templateProcessor,
this.providerFilter = providerFilter;
this.config = config;
this.refFactory = new RefFactoryImpl(this, modelBuilder, providerFilter);
- this.sqlTemplate = createSqlTemplate();
+ this.sqlTemplate = lazy(this::createSqlTemplate);
}
private SqlTemplate createSqlTemplate() {
@@ -183,7 +197,7 @@ private SqlTemplate createSqlTemplate() {
// The shared template resolves a dialect without a database in view, so the dialect resolved for this
// persistence unit is applied on top. An explicit provider filter still wins.
return template.withDialect(
- providerFilter != null ? Providers.getSqlDialect(providerFilter, config) : dialect);
+ providerFilter != null ? Providers.getSqlDialect(providerFilter, config) : dialect.get());
}
private void setParameters(@Nonnull jakarta.persistence.Query query, @Nonnull List parameters) {
@@ -252,7 +266,7 @@ private jakarta.persistence.Query query(@Nonnull TemplateString template, @Nonnu
*/
@Override
public SqlTemplate sqlTemplate() {
- return SqlInterceptorManager.customize(sqlTemplate);
+ return SqlInterceptorManager.customize(sqlTemplate.get());
}
/**
diff --git a/storm-core/src/main/java/st/orm/core/template/impl/LazySupplier.java b/storm-core/src/main/java/st/orm/core/template/impl/LazySupplier.java
index ceae6d47a..ff9013fb7 100644
--- a/storm-core/src/main/java/st/orm/core/template/impl/LazySupplier.java
+++ b/storm-core/src/main/java/st/orm/core/template/impl/LazySupplier.java
@@ -68,13 +68,18 @@ public LazySupplier(@Nonnull T initialValue) {
/**
* Gets a result. On the first invocation, the supplier is called to produce the value. The supplier is then
- * released for garbage collection.
+ * released for garbage collection. A resolved value is returned with a single volatile read, so shared
+ * suppliers on hot paths stay free of write contention.
*
* @return a result.
*/
@Override
public T get() {
- T result = reference.updateAndGet(value -> requireNonNullElseGet(value, supplier));
+ T result = reference.get();
+ if (result != null) {
+ return result;
+ }
+ result = reference.updateAndGet(value -> requireNonNullElseGet(value, supplier));
supplier = null; // Release the supplier (and its captured context) for GC.
return result;
}
diff --git a/storm-core/src/main/java/st/orm/core/template/impl/SqlTemplateImpl.java b/storm-core/src/main/java/st/orm/core/template/impl/SqlTemplateImpl.java
index 1157f7653..fbd97bf73 100644
--- a/storm-core/src/main/java/st/orm/core/template/impl/SqlTemplateImpl.java
+++ b/storm-core/src/main/java/st/orm/core/template/impl/SqlTemplateImpl.java
@@ -24,12 +24,14 @@
import static st.orm.core.template.impl.SqlInterceptorManager.intercept;
import jakarta.annotation.Nonnull;
+import jakarta.annotation.Nullable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
+import java.util.function.Supplier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import st.orm.BindVars;
@@ -84,15 +86,33 @@ record Wrapped(@Nonnull List elements) implements Element {
private final boolean inlineParameters;
private final ModelBuilder modelBuilder;
private final TableAliasResolver tableAliasResolver;
- private final SqlDialect dialect;
- private final TemplatePreparation templatePreparation;
+
+ /**
+ * The dialect set via {@link #withDialect(SqlDialect)} or a dialect-taking constructor, or {@code null} when the
+ * dialect is resolved from the classpath. An explicit dialect is preserved across {@link #withConfig(StormConfig)},
+ * whereas a classpath-resolved dialect is re-resolved under the new configuration.
+ */
+ private final @Nullable SqlDialect explicitDialect;
+
+ /**
+ * The dialect in use, resolved on first use. Classpath resolution fails fast when multiple dialect providers are
+ * eligible without a defined order, so templates that receive an explicit dialect before processing, such as the
+ * shared {@link SqlTemplate#PS} and {@link SqlTemplate#JPA} instances customized by database-bound templates,
+ * must never trigger it. All dialect-dependent state is therefore initialized lazily.
+ */
+ private final LazySupplier dialect;
+
+ private final Supplier templatePreparation;
private final Function keyGenerator;
private final StormConfig config;
- private final SegmentedLruCache