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
17 changes: 13 additions & 4 deletions storm-core/src/main/java/st/orm/core/spi/Providers.java
Original file line number Diff line number Diff line change
Expand Up @@ -235,11 +235,20 @@ public static SqlDialect getSqlDialect() {
return getSqlDialect(StormConfig.defaults());
}

/**
* Resolves the SQL dialect from the classpath, without a database in view.
*
* <p>Enablement is re-evaluated on every resolution, and an ambiguous resolution fails fast.</p>
*
* @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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -286,14 +286,19 @@ interface BatchListener {
/**
* Returns the SQL dialect used by this template.
*
* <p>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.</p>
*
* @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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -83,9 +85,21 @@ private interface TemplateProcessor {
private final TableAliasResolver tableAliasResolver;
private final Predicate<Provider> 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> 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<SqlDialect> dialect;

public JpaTemplateImpl(@Nonnull EntityManager entityManager) {
this(entityManager, StormConfig.defaults());
Expand All @@ -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);
}

/**
Expand All @@ -121,19 +135,19 @@ public JpaTemplateImpl(@Nonnull EntityManager entityManager, @Nonnull StormConfi
* <p>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.</p>
* template is being built, leaves the database unknown, and the dialect then comes from the classpath, resolved
* on first use.</p>
*/
private static SqlDialect resolveDialect(@Nonnull EntityManager entityManager, @Nonnull StormConfig config) {
private static Supplier<SqlDialect> 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));
}
}

Expand Down Expand Up @@ -163,15 +177,15 @@ private JpaTemplateImpl(@Nonnull TemplateProcessor templateProcessor,
@Nonnull TableAliasResolver tableAliasResolver,
@Nullable Predicate<Provider> providerFilter,
@Nonnull StormConfig config,
@Nonnull SqlDialect dialect) {
@Nonnull Supplier<SqlDialect> dialect) {
this.dialect = dialect;
this.templateProcessor = templateProcessor;
this.modelBuilder = modelBuilder;
this.tableAliasResolver = tableAliasResolver;
this.providerFilter = providerFilter;
this.config = config;
this.refFactory = new RefFactoryImpl(this, modelBuilder, providerFilter);
this.sqlTemplate = createSqlTemplate();
this.sqlTemplate = lazy(this::createSqlTemplate);
}

private SqlTemplate createSqlTemplate() {
Expand All @@ -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<SqlTemplate.Parameter> parameters) {
Expand Down Expand Up @@ -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());
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Loading
Loading