You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@grails.apache.org by bo...@apache.org on 2026/06/26 21:08:06 UTC
(grails-core) 01/01: feat: GORM O(M+N) scaling — Hibernate 7 adapter wiring and child datastore init
This is an automated email from the ASF dual-hosted git repository.
borinquenkid pushed a commit to branch feat/gorm-registry-h7
in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit 14e6ca27efc20098e694924b9614c8d98327d267
Author: Walter Duque de Estrada <wb...@mac.com>
AuthorDate: Fri Jun 26 16:03:01 2026 -0500
feat: GORM O(M+N) scaling — Hibernate 7 adapter wiring and child datastore init
Wires Hibernate 7 to the new GormRegistry with correct child datastore
initialization order, fixing the gaps that PR #15771 intentionally deferred:
- HibernateDatastore (H7): registers child datastores with GormRegistry eagerly
during initialization, preserving schema/database/datasource routing
- HibernateGormInstanceApi: use getHibernateTemplate() lazy getter for
isAttached(), lock(), and attach() — factory constructor leaves the field null
- HibernateGormStaticApi: executeQuery(query, Map) passes params as both named
params and query settings so max/offset pagination is applied
- HibernateSession: coerceId() converts String IDs to the entity's declared
identifier type in retrieveAll(), fixing String→Long lookup mismatch
- HibernateGormEnhancerSpec: updated to use GormRegistry public API
- HibernateDirtyCheckingSpec / HibernateUpdateFromListenerSpec: local Person
renamed to DirtyCheckPerson to avoid DuplicateMappingException with TCK
- grails-test-examples/hibernate7: demo33 UniqueConstraintOnHasOneSpec
@NotYetImplemented removed (test now passes); GormAdvancedSpec pagination fixed
- grails-data-hibernate7-core GormApiAllocationSpec: verifies O(M+N) API count
across single-datasource, multi-datasource, and multi-tenant configurations
Co-Authored-By: Claude Sonnet 4.6 <no...@anthropic.com>
---
grails-data-hibernate7/ISSUES.md | 21 +
.../grails/gorm/hibernate/HibernateEntity.groovy | 55 +-
.../groovy/grails/orm/CriteriaMethodInvoker.java | 4 +-
.../orm/hibernate/ChildHibernateDatastore.java | 123 +++-
.../orm/hibernate/GrailsHibernateTemplate.java | 64 +-
.../GrailsHibernateTransactionManager.groovy | 68 +-
.../grails/orm/hibernate/HibernateDatastore.java | 198 +++++-
.../orm/hibernate/HibernateGormApiFactory.groovy | 74 +++
.../orm/hibernate/HibernateGormEnhancer.groovy | 73 +--
.../orm/hibernate/HibernateGormInstanceApi.groovy | 673 ++++++++++++--------
.../orm/hibernate/HibernateGormStaticApi.groovy | 694 +++++++++++++--------
.../hibernate/HibernateGormValidationApi.groovy | 50 +-
.../org/grails/orm/hibernate/HibernateSession.java | 145 ++++-
.../orm/hibernate/HibernateSessionResolver.groovy | 88 +++
.../orm/hibernate/SchemaTenantGormEnhancer.java | 6 +-
.../hibernate/TenantBoundHibernateTemplate.groovy | 170 +++++
.../orm/hibernate/cfg/GrailsHibernateUtil.java | 10 +-
.../orm/hibernate/cfg/HibernateMappingContext.java | 4 +-
.../cfg/HibernateMappingContextConfiguration.java | 13 +-
.../domainbinding/binder/GrailsDomainBinder.java | 2 +-
.../dirty/GrailsEntityDirtinessStrategy.groovy | 77 ++-
.../event/listener/HibernateEventListener.java | 2 +-
.../multitenancy/MultiTenantEventListener.java | 6 +-
.../orm/hibernate/query/HibernateHqlQuery.java | 97 +++
.../grails/orm/hibernate/query/HibernateQuery.java | 39 +-
.../orm/hibernate/query/HqlListQueryBuilder.java | 22 +
.../orm/hibernate/query/HqlQueryContext.java | 6 +-
.../hibernate/query/JpaCriteriaQueryCreator.java | 3 +-
.../orm/hibernate/query/PagedResultList.java | 148 +++++
.../grails/orm/hibernate/query/SelectHqlQuery.java | 93 ++-
.../hibernate/support/ClosureEventListener.java | 37 +-
.../support/ClosureEventTriggeringInterceptor.java | 62 +-
.../GormAutoTimestampFlushEntityEventListener.java | 125 ++++
.../hibernate/support/HibernateRuntimeUtils.groovy | 27 +
.../gorm/tests/HibernateGormDatastoreSpec.groovy | 5 +
.../gorm/tests/HibernatePagedResultListSpec.groovy | 16 +-
.../grails/gorm/tests/PagedResultListSpec.groovy | 7 +-
...WithNewSessionAndExistingTransactionSpec.groovy | 23 +-
.../HibernateDirtyCheckingSpec.groovy | 22 +-
.../HibernateUpdateFromListenerSpec.groovy | 12 +-
.../tests/hibernatequery/HibernateQuerySpec.groovy | 46 ++
.../gorm/tests/services/DataServiceSpec.groovy | 12 +-
.../tests/validation/UniqueWithinGroupSpec.groovy | 3 +-
.../core/GrailsDataHibernate7TckManager.groovy | 46 +-
.../tck/tests/PagedResultSpecHibernate.groovy | 8 +-
.../datastore/gorm/GormEnhancerCleanupSpec.groovy | 46 +-
.../ChildHibernateDatastoreUnitSpec.groovy | 73 +++
.../hibernate/GormRegistryScalabilitySpec.groovy | 240 +++++++
.../HibernateDatastoreIntegrationSpec.groovy | 1 +
.../hibernate/HibernateGormApiFactorySpec.groovy | 51 ++
.../orm/hibernate/HibernateGormEnhancerSpec.groovy | 12 +-
.../hibernate/HibernateGormInstanceApiSpec.groovy | 7 +-
.../hibernate/HibernateGormStaticApiSpec.groovy | 10 +-
.../HibernateGormValidationApiSpec.groovy | 4 +-
.../orm/hibernate/HibernateSessionSpec.groovy | 43 ++
.../HibernateTenantContextProfilingSpec.groovy | 111 ++++
.../hibernate/SchemaTenantGormEnhancerSpec.groovy | 1 -
.../DataServiceMultiDataSourceSpec.groovy | 17 +-
.../MultipleDataSourcesWithEventsSpec.groovy | 8 +-
.../connections/SchemaMultiTenantSpec.groovy | 33 +-
.../hibernate/connections/SingleTenantSpec.groovy | 37 +-
.../WhereQueryMultiDataSourceSpec.groovy | 4 +-
.../orm/hibernate/query/HqlQueryContextSpec.groovy | 2 +-
.../support/ClosureEventListenerSpec.groovy | 2 +-
.../HibernateTransactionManagerSpec.groovy | 192 ++++++
.../src/test/resources/simplelogger.properties | 1 +
grails-data-hibernate7/docs/build.gradle | 2 +-
.../GrailsOpenSessionInViewInterceptor.java | 4 +-
...bernatePersistenceContextInterceptorSpec.groovy | 23 +-
.../hibernate7/HibernateTransactionManager.java | 15 +-
.../demo/UniqueConstraintOnHasOneSpec.groovy | 4 +-
.../grails-app/conf/logback.xml | 6 +-
.../services/example/AnotherBookService.groovy | 4 +-
.../DatabasePerTenantIntegrationSpec.groovy | 4 +-
.../services/example/MetricService.groovy | 3 +-
75 files changed, 3514 insertions(+), 925 deletions(-)
diff --git a/grails-data-hibernate7/ISSUES.md b/grails-data-hibernate7/ISSUES.md
new file mode 100644
index 0000000000..d3ad0f64e0
--- /dev/null
+++ b/grails-data-hibernate7/ISSUES.md
@@ -0,0 +1,21 @@
+# Hibernate 7 O(M+N) Scaling and Performance
+
+## Context
+Hibernate 7 integration in GORM 8 introduces a modern persistence baseline. The O(M+N) scaling work ensures that multi-tenant applications remain efficient as the number of tenants grows.
+
+## Implemented and Validated
+
+### Datastore integration aligned to shared model
+- Primary target for the shared-registry architecture refactor.
+- Updated all API entry points to leverage centralized lookups and normalized keys.
+
+### Query and session behavior hardening
+- Comprehensive refactor of query paths to reduce allocation churn.
+- [DONE] Refactor `JpaCriteriaQueryCreator` to inject `PredicateGenerator` (eliminated redundant object churn).
+
+### Verification
+- Added `HibernateTenantContextProfilingSpec` to measure tenant wrapping overhead.
+- Integrated `GormRegistryScalabilitySpec` to ensure linear (or better) scaling with entity and tenant counts.
+
+## Potential Optimization Opportunities
+- Further tracing of JpaCriteria query construction to identify remaining minor allocation hotspots.
diff --git a/grails-data-hibernate7/core/src/main/groovy/grails/gorm/hibernate/HibernateEntity.groovy b/grails-data-hibernate7/core/src/main/groovy/grails/gorm/hibernate/HibernateEntity.groovy
index edcdca57b8..c85676f784 100644
--- a/grails-data-hibernate7/core/src/main/groovy/grails/gorm/hibernate/HibernateEntity.groovy
+++ b/grails-data-hibernate7/core/src/main/groovy/grails/gorm/hibernate/HibernateEntity.groovy
@@ -20,11 +20,10 @@ package grails.gorm.hibernate
import groovy.transform.CompileStatic
import groovy.transform.Generated
-
import org.codehaus.groovy.runtime.InvokerHelper
-import org.grails.datastore.gorm.GormEnhancer
import org.grails.datastore.gorm.GormEntity
+import org.grails.datastore.gorm.GormRegistry
import org.grails.datastore.mapping.model.PersistentEntity
import org.grails.datastore.mapping.model.types.Association
import org.grails.datastore.mapping.model.types.ToOne
@@ -40,6 +39,56 @@ import org.grails.orm.hibernate.HibernateGormStaticApi
@CompileStatic
trait HibernateEntity<D> extends GormEntity<D> {
+ /**
+ * Finds all objects for the given native SQL query. Pass a GString to have interpolated
+ * values safely bound as named parameters rather than interpolated into the query string.
+ *
+ * @param sql The native SQL query
+ * @return The matching objects
+ */
+ @Generated
+ static List<D> findAllWithNativeSql(CharSequence sql) {
+ currentHibernateStaticApi().findAllWithNativeSql(sql, Collections.emptyMap())
+ }
+
+ /**
+ * Finds an entity for the given native SQL query. The query must be a Groovy GString
+ * so that interpolated values are safely bound as named parameters.
+ *
+ * @param sql The native SQL query (must be a GString with {@code ${value}} interpolations)
+ * @return The entity
+ */
+ @Generated
+ static D findWithNativeSql(CharSequence sql) {
+ currentHibernateStaticApi().findWithNativeSql(sql, Collections.emptyMap())
+ }
+
+ /**
+ * Finds all objects for the given native SQL query. The query must be a Groovy GString
+ * so that interpolated values are safely bound as named parameters.
+ *
+ * @param sql The native SQL query (must be a GString with {@code ${value}} interpolations)
+ * @param args Pagination/query settings (max, offset, cache, etc.) — NOT SQL parameters
+ * @return The matching objects
+ */
+ @Generated
+ static List<D> findAllWithNativeSql(CharSequence sql, Map args) {
+ currentHibernateStaticApi().findAllWithNativeSql(sql, args)
+ }
+
+ /**
+ * Finds an entity for the given native SQL query. The query must be a Groovy GString
+ * so that interpolated values are safely bound as named parameters.
+ *
+ * @param sql The native SQL query (must be a GString with {@code ${value}} interpolations)
+ * @param args Pagination/query settings (max, offset, cache, etc.) — NOT SQL parameters
+ * @return The entity
+ */
+ @Generated
+ static D findWithNativeSql(CharSequence sql, Map args) {
+ currentHibernateStaticApi().findWithNativeSql(sql, args)
+ }
+
/**
* Finds all objects for the given SQL query. Pass a GString to have interpolated
* values safely bound as named parameters rather than interpolated into the query string.
@@ -92,7 +141,7 @@ trait HibernateEntity<D> extends GormEntity<D> {
@Generated
private static HibernateGormStaticApi currentHibernateStaticApi() {
- (HibernateGormStaticApi) GormEnhancer.findStaticApi(this)
+ (HibernateGormStaticApi) GormRegistry.findStaticApi(this)
}
/**
diff --git a/grails-data-hibernate7/core/src/main/groovy/grails/orm/CriteriaMethodInvoker.java b/grails-data-hibernate7/core/src/main/groovy/grails/orm/CriteriaMethodInvoker.java
index 97bf860cb2..c31fb028f3 100644
--- a/grails-data-hibernate7/core/src/main/groovy/grails/orm/CriteriaMethodInvoker.java
+++ b/grails-data-hibernate7/core/src/main/groovy/grails/orm/CriteriaMethodInvoker.java
@@ -38,9 +38,9 @@ import org.grails.datastore.mapping.model.PersistentProperty;
import org.grails.datastore.mapping.model.types.Association;
import org.grails.datastore.mapping.query.Query;
import org.grails.orm.hibernate.cfg.domainbinding.hibernate.GrailsHibernatePersistentEntity;
-import org.grails.orm.hibernate.query.HibernatePagedResultList;
import org.grails.orm.hibernate.query.HibernateQuery;
import org.grails.orm.hibernate.query.HibernateQueryArgument;
+import org.grails.orm.hibernate.query.PagedResultList;
/**
* If you want to extend functionality of the HibernateCriteriaBuilder
@@ -133,7 +133,7 @@ public class CriteriaMethodInvoker {
}
hibernateQuery.order(order);
}
- result = new HibernatePagedResultList(hibernateQuery);
+ result = new PagedResultList(hibernateQuery);
} else if (builder.isScroll()) {
result = hibernateQuery.scroll();
} else {
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/ChildHibernateDatastore.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/ChildHibernateDatastore.java
index c3be048d73..233eb85037 100644
--- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/ChildHibernateDatastore.java
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/ChildHibernateDatastore.java
@@ -18,20 +18,33 @@
*/
package org.grails.orm.hibernate;
+import java.util.Collections;
+import java.util.Map;
+
import org.hibernate.SessionFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.grails.datastore.gorm.events.ConfigurableApplicationEventPublisher;
+import org.grails.datastore.mapping.core.Session;
import org.grails.datastore.mapping.core.connections.ConnectionSource;
import org.grails.datastore.mapping.core.connections.ConnectionSources;
import org.grails.orm.hibernate.cfg.HibernateMappingContext;
import org.grails.orm.hibernate.cfg.Settings;
import org.grails.orm.hibernate.connections.HibernateConnectionSourceSettings;
+import org.grails.orm.hibernate.support.hibernate7.SessionHolder;
/**
* A datastore for a specific connection in a multiple data source setup.
*/
public class ChildHibernateDatastore extends HibernateDatastore {
+ private static final Logger log = LoggerFactory.getLogger(ChildHibernateDatastore.class);
+
+ private static final ThreadLocal<HibernateDatastore> PARENT_HOLDER = new ThreadLocal<>();
+
private final HibernateDatastore parent;
public ChildHibernateDatastore(
@@ -39,37 +52,117 @@ public class ChildHibernateDatastore extends HibernateDatastore {
ConnectionSources<SessionFactory, HibernateConnectionSourceSettings> connectionSources,
HibernateMappingContext mappingContext,
ConfigurableApplicationEventPublisher eventPublisher) {
- super(connectionSources, mappingContext, eventPublisher,
- connectionSources.getDefaultConnectionSource().getSource());
+ super(bindParent(parent, connectionSources), mappingContext, eventPublisher,
+ connectionSources.getDefaultConnectionSource().getSource());
this.parent = parent;
+ PARENT_HOLDER.remove();
+ }
+
+ private static ConnectionSources<SessionFactory, HibernateConnectionSourceSettings> bindParent(HibernateDatastore parent, ConnectionSources<SessionFactory, HibernateConnectionSourceSettings> connectionSources) {
+ PARENT_HOLDER.set(parent);
+ return connectionSources;
+ }
+
+ @Override
+ public HibernateDatastore getPrimaryDatastore() {
+ return parent != null ? parent : PARENT_HOLDER.get();
}
@Override
protected HibernateGormEnhancer initialize() {
- return null;
+ HibernateDatastore p = getPrimaryDatastore();
+ Map<String, HibernateDatastore> datastoresMap = p != null ? p.datastoresByConnectionSource : Collections.emptyMap();
+ return new HibernateGormEnhancer(this, getTransactionManager(), connectionSources.getDefaultConnectionSource().getSettings(), datastoresMap);
}
@Override
public void destroy() {
if (!this.destroyed) {
- // Only mark as destroyed, don't close shared resources
- this.destroyed = true;
+ super.destroy();
}
}
@Override
public HibernateDatastore getDatastoreForConnection(String connectionName) {
- if (Settings.SETTING_DATASOURCE.equals(connectionName) ||
- ConnectionSource.DEFAULT.equals(connectionName)) {
- return parent;
- } else {
- HibernateDatastore hibernateDatastore = parent.datastoresByConnectionSource.get(connectionName);
- if (hibernateDatastore == null) {
- throw new org.grails.datastore.mapping.core.exceptions.ConfigurationException(
- "DataSource not found for name [" + connectionName +
- "] in configuration. Please check your multiple data sources configuration and try again.");
+ String myName = getConnectionSources().getDefaultConnectionSource().getName();
+ if (connectionName.equals(myName)) {
+ return this;
+ }
+
+ HibernateDatastore p = getPrimaryDatastore();
+ if (Settings.SETTING_DATASOURCE.equals(connectionName) || ConnectionSource.DEFAULT.equals(connectionName)) {
+ return p;
+ }
+
+ if (p != null) {
+ HibernateDatastore hibernateDatastore = p.datastoresByConnectionSource.get(connectionName);
+ if (hibernateDatastore != null) {
+ return hibernateDatastore;
+ }
+ // During initialization this child may not yet be registered in the parent's runtime map,
+ // while sibling datastores being initialized in parallel may also be absent. Return null
+ // only when (a) this child is not yet registered (so we are in the initialization phase)
+ // AND (b) the requested connection name is a sibling that is configured in the parent's
+ // connection sources (i.e., it will exist once initialization completes). Truly unknown
+ // names always throw ConfigurationException regardless of initialization state.
+ if (!p.datastoresByConnectionSource.containsKey(myName) &&
+ p.connectionSources.getConnectionSource(connectionName) != null) {
+ return null;
+ }
+ }
+
+ throw new org.grails.datastore.mapping.core.exceptions.ConfigurationException(
+ "DataSource not found for name [" + connectionName +
+ "] in configuration. Please check your multiple data sources configuration and try again.");
+ }
+
+ private org.springframework.transaction.PlatformTransactionManager springTransactionManager;
+
+ @Override
+ public org.springframework.context.ConfigurableApplicationContext getApplicationContext() {
+ org.springframework.context.ConfigurableApplicationContext ctx = super.getApplicationContext();
+ if (ctx == null && parent != null) {
+ ctx = parent.getApplicationContext();
+ }
+ return ctx;
+ }
+
+ @Override
+ public org.springframework.transaction.PlatformTransactionManager getTransactionManager() {
+ if (springTransactionManager == null && getApplicationContext() != null) {
+ String name = getConnectionSources().getDefaultConnectionSource().getName();
+ String beanName = "transactionManager_" + name;
+ if (log.isDebugEnabled()) {
+ log.debug("ChildHibernateDatastore.getTransactionManager(): name=" + name + " beanName=" + beanName);
+ }
+ if (getApplicationContext() instanceof org.springframework.context.ConfigurableApplicationContext configurableApplicationContext) {
+ org.springframework.beans.factory.config.ConfigurableListableBeanFactory beanFactory = configurableApplicationContext.getBeanFactory();
+ boolean contains = beanFactory.containsBean(beanName);
+ boolean inCreation = beanFactory.isCurrentlyInCreation(beanName);
+ if (log.isDebugEnabled()) {
+ log.debug("ChildHibernateDatastore.getTransactionManager(): contains=" + contains + " inCreation=" + inCreation);
+ }
+ if (contains && !inCreation) {
+ springTransactionManager = beanFactory.getBean(beanName, org.springframework.transaction.PlatformTransactionManager.class);
+ if (log.isDebugEnabled()) {
+ log.debug("ChildHibernateDatastore.getTransactionManager(): springTransactionManager resolved: " + springTransactionManager);
+ }
+ }
+ }
+ }
+ return springTransactionManager != null ? springTransactionManager : super.getTransactionManager();
+ }
+
+ @Override
+ public Session connect() {
+ SessionFactory sf = getSessionFactory();
+ Object resource = TransactionSynchronizationManager.getResource(sf);
+ if (resource instanceof SessionHolder sfHolder) {
+ org.hibernate.Session nativeSession = sfHolder.getSession();
+ if (nativeSession != null && nativeSession.isOpen()) {
+ return new HibernateSession(this, sf, nativeSession);
}
- return hibernateDatastore;
}
+ return new HibernateSession(this, sf, sf.openSession());
}
}
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/GrailsHibernateTemplate.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/GrailsHibernateTemplate.java
index c23f23f6b9..12ade07d8a 100644
--- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/GrailsHibernateTemplate.java
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/GrailsHibernateTemplate.java
@@ -161,6 +161,7 @@ public class GrailsHibernateTemplate implements IHibernateTemplate {
.getServiceRegistry()
.getService(ConnectionProvider.class);
this.dataSource = connectionProvider != null ? connectionProvider.unwrap(DataSource.class) : null;
+
if (this.dataSource != null) {
if (this.dataSource instanceof TransactionAwareDataSourceProxy) {
DataSource target = ((TransactionAwareDataSourceProxy) this.dataSource).getTargetDataSource();
@@ -182,7 +183,7 @@ public class GrailsHibernateTemplate implements IHibernateTemplate {
this(sessionFactory);
if (datastore != null) {
cacheQueries = datastore.isCacheQueries();
- this.osivReadOnly = datastore.isOsivReadOnly();
+ this.osivReadOnly = datastore.isOsivReadOnly(sessionFactory);
this.passReadOnlyToHibernate = datastore.isPassReadOnlyToHibernate();
this.flushMode = hibernateFlushModeToConstant(datastore.getDefaultFlushMode());
}
@@ -192,7 +193,7 @@ public class GrailsHibernateTemplate implements IHibernateTemplate {
this(sessionFactory);
if (datastore != null) {
cacheQueries = datastore.isCacheQueries();
- this.osivReadOnly = datastore.isOsivReadOnly();
+ this.osivReadOnly = datastore.isOsivReadOnly(sessionFactory);
this.passReadOnlyToHibernate = datastore.isPassReadOnlyToHibernate();
}
this.flushMode = defaultFlushMode;
@@ -216,6 +217,46 @@ public class GrailsHibernateTemplate implements IHibernateTemplate {
return execute(hibernateCallback);
}
+ /**
+ * Executes the given closure in a brand-new Hibernate {@link Session}, fully isolated from any
+ * session or transaction that may already be bound to the current thread.
+ *
+ * <h3>Thread-local state management</h3>
+ * <p>Before opening the new session this method suspends the caller's transactional context:
+ * <ul>
+ * <li>Any existing {@link org.hibernate.engine.spi.SessionImplementor} bound to
+ * {@link #sessionFactory} is unbound and later restored.</li>
+ * <li>Any {@link org.springframework.jdbc.datasource.ConnectionHolder} bound to
+ * {@link #dataSource} is unbound and later restored. This handles the case where the
+ * datasource is <em>shared</em> across multiple session factories — most notably in
+ * {@code DATABASE} multi-tenancy mode, where every tenant's session factory references
+ * the same {@code LazyConnectionDataSourceProxy}. Without this unbinding,
+ * {@link org.springframework.orm.hibernate5.HibernateTransactionManager#doBegin}
+ * would throw {@code IllegalStateException: Already value [...] bound to thread}
+ * when it tried to register the new session's connection.</li>
+ * <li>Active {@link org.springframework.transaction.support.TransactionSynchronization}s
+ * are cleared and restored so that existing listeners are not notified of lifecycle
+ * events belonging to the inner session.</li>
+ * </ul>
+ *
+ * <h3>Resource restoration</h3>
+ * <p>The {@code finally} block always:
+ * <ol>
+ * <li>Clears the new session's synchronizations.</li>
+ * <li>Unbinds and releases the new session's JDBC connection (unless the datasource is a
+ * {@link org.grails.datastore.gorm.jdbc.MultiTenantDataSource}).</li>
+ * <li>Closes the new session.</li>
+ * <li>Re-registers the caller's synchronizations.</li>
+ * <li>Rebinds the caller's session holder (if one existed).</li>
+ * <li>Rebinds the caller's connection holder (if one existed), independently of whether
+ * a session holder was present — necessary for the shared-datasource case above.</li>
+ * </ol>
+ *
+ * @param <T> the return type of the closure
+ * @param callable a {@link groovy.lang.Closure} that receives the new {@link Session} as its
+ * single argument and returns a result
+ * @return the value returned by {@code callable}
+ */
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
@Override
public <T> T executeWithNewSession(final Closure<T> callable) {
@@ -240,9 +281,13 @@ public class GrailsHibernateTemplate implements IHibernateTemplate {
// if there are already bound holders, unbind them so they can be restored later
if (sessionHolder != null) {
txResources.unbindResource(sessionFactory);
- if (previousConnectionHolder != null) {
- txResources.unbindResource(dataSource);
- }
+ }
+ // The datasource may be shared across session factories (e.g. in DATABASE multi-tenancy
+ // mode). If a connection was bound by an outer transaction (e.g. from HibernateSpec.setup),
+ // we must unbind it now so that HibernateTransactionManager.doBegin() can bind its own
+ // connection for the new session, regardless of whether a session holder already exists.
+ if (previousConnectionHolder != null) {
+ txResources.unbindResource(dataSource);
}
// create and bind a new session holder for the new session
@@ -297,9 +342,12 @@ public class GrailsHibernateTemplate implements IHibernateTemplate {
// now restore any previous state
if (previousHolder != null) {
txResources.bindResource(sessionFactory, previousHolder);
- if (previousConnectionHolder != null) {
- txResources.bindResource(dataSource, previousConnectionHolder);
- }
+ }
+ // Restore the previously-bound datasource connection, even when there was no
+ // prior session holder — the outer transaction's connection must be re-bound so
+ // it can continue after this new-session block returns.
+ if (previousConnectionHolder != null) {
+ txResources.bindResource(dataSource, previousConnectionHolder);
}
}
}
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/GrailsHibernateTransactionManager.groovy b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/GrailsHibernateTransactionManager.groovy
index 5655fbecd4..242e5bfc82 100644
--- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/GrailsHibernateTransactionManager.groovy
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/GrailsHibernateTransactionManager.groovy
@@ -18,17 +18,21 @@
*/
package org.grails.orm.hibernate
+import javax.sql.DataSource
+
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
-import javax.sql.DataSource
+
import org.hibernate.FlushMode
import org.hibernate.SessionFactory
-import org.grails.orm.hibernate.support.hibernate7.HibernateTransactionManager
-import org.grails.orm.hibernate.support.hibernate7.SessionHolder
import org.springframework.transaction.TransactionDefinition
import org.springframework.transaction.support.TransactionSynchronizationManager
+import org.grails.datastore.mapping.core.Datastore
+import org.grails.orm.hibernate.support.hibernate7.HibernateTransactionManager
+import org.grails.orm.hibernate.support.hibernate7.SessionHolder
+
/**
* Extends the standard class to always set the flush mode to manual when in a read-only transaction.
*
@@ -39,6 +43,11 @@ import org.springframework.transaction.support.TransactionSynchronizationManager
class GrailsHibernateTransactionManager extends HibernateTransactionManager {
final FlushMode defaultFlushMode
+ private Datastore datastore
+
+ void setDatastore(Datastore datastore) {
+ this.datastore = datastore
+ }
GrailsHibernateTransactionManager(SessionFactory sessionFactory, DataSource dataSource, FlushMode defaultFlushMode = FlushMode.AUTO) {
super(sessionFactory)
@@ -47,24 +56,53 @@ class GrailsHibernateTransactionManager extends HibernateTransactionManager {
}
this.defaultFlushMode = defaultFlushMode
}
-
@Override
protected void doBegin(Object transaction, TransactionDefinition definition) {
super.doBegin transaction, definition
-
- if (definition.isReadOnly()) {
- // transaction is HibernateTransactionManager.HibernateTransactionObject private class instance
- // always set to manual; the base class doesn't because the OSIV has already registered a session
-
- SessionHolder holder = (SessionHolder) TransactionSynchronizationManager.getResource(sessionFactory)
- if (holder != null) {
- holder.session.setHibernateFlushMode(FlushMode.MANUAL)
+
+ SessionHolder holder = (SessionHolder) TransactionSynchronizationManager.getResource(sessionFactory)
+ if (holder != null) {
+ if (definition.readOnly) {
+ holder.session.setHibernateFlushMode FlushMode.MANUAL
}
- } else if (defaultFlushMode != FlushMode.AUTO) {
- SessionHolder holder = (SessionHolder) TransactionSynchronizationManager.getResource(sessionFactory)
- if (holder != null) {
+ else {
holder.session.setHibernateFlushMode(defaultFlushMode)
}
+ if (this.datastore != null) {
+ if (!TransactionSynchronizationManager.hasResource(this.datastore)) {
+ org.grails.datastore.mapping.core.Session session = new HibernateSession((HibernateDatastore) this.datastore, sessionFactory as SessionFactory, null)
+ TransactionSynchronizationManager.bindResource(this.datastore, new org.grails.datastore.mapping.transactions.SessionHolder(session))
+ }
+ org.grails.datastore.gorm.GormEnhancerRegistry.getInstance().setPreferredDatastore(this.datastore)
+ }
+ }
+ }
+
+ @Override
+ protected void doCleanupAfterCompletion(Object transaction) {
+ super.doCleanupAfterCompletion(transaction)
+ if (this.datastore != null) {
+ org.grails.datastore.gorm.GormEnhancerRegistry.getInstance().clearPreferredDatastore()
+ HibernateTransactionManager.HibernateTransactionObject txObject = (HibernateTransactionManager.HibernateTransactionObject) transaction
+ if (txObject.isNewSessionHolder()) {
+ TransactionSynchronizationManager.unbindResourceIfPossible(this.datastore)
+ }
+ }
+ }
+
+ @Override
+ protected void doRollback(org.springframework.transaction.support.DefaultTransactionStatus status) {
+ if (log.isDebugEnabled()) {
+ log.debug('GrailsHibernateTransactionManager(' + this.hashCode() + ').doRollback called. status=' + status)
+ }
+ super.doRollback(status)
+ }
+
+ @Override
+ protected void doCommit(org.springframework.transaction.support.DefaultTransactionStatus status) {
+ if (log.isDebugEnabled()) {
+ log.debug('GrailsHibernateTransactionManager(' + this.hashCode() + ').doCommit called. status=' + status)
}
+ super.doCommit(status)
}
}
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateDatastore.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateDatastore.java
index fcc6cebaff..d5854986d2 100644
--- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateDatastore.java
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateDatastore.java
@@ -47,6 +47,8 @@ import org.hibernate.SessionFactory;
import org.hibernate.boot.Metadata;
import org.hibernate.cfg.Environment;
import org.hibernate.engine.spi.SessionFactoryImplementor;
+import org.hibernate.event.service.spi.EventListenerRegistry;
+import org.hibernate.event.spi.EventType;
import org.hibernate.integrator.spi.Integrator;
import org.hibernate.integrator.spi.IntegratorService;
import org.hibernate.service.ServiceRegistry;
@@ -120,6 +122,7 @@ import org.grails.orm.hibernate.event.listener.HibernateEventListener;
import org.grails.orm.hibernate.multitenancy.MultiTenantEventListener;
import org.grails.orm.hibernate.query.HibernateQueryArgument;
import org.grails.orm.hibernate.support.ClosureEventTriggeringInterceptor;
+import org.grails.orm.hibernate.support.GormAutoTimestampFlushEntityEventListener;
/**
* Datastore implementation that uses a Hibernate SessionFactory underneath.
@@ -246,10 +249,11 @@ public class HibernateDatastore extends AbstractDatastore
} else {
this.bytecodeProvider = new org.grails.orm.hibernate.proxy.GrailsBytecodeProvider();
}
-
- this.dataSourceName = ConnectionSource.DEFAULT;
+
+ this.dataSourceName = defaultConnectionSource.getName();
this.sessionFactory = sessionFactory != null ? sessionFactory : defaultConnectionSource.getSource();
-
+ setSessionResolver(new HibernateSessionResolver(this, this.sessionFactory));
+
HibernateConnectionSourceSettings settings = defaultConnectionSource.getSettings();
HibernateConnectionSourceSettings.HibernateSettings hibernateSettings = settings.getHibernate();
this.osivReadOnly = hibernateSettings.getOsiv().isReadonly();
@@ -259,11 +263,11 @@ public class HibernateDatastore extends AbstractDatastore
Boolean markDirty = settings.getMarkDirty();
this.markDirty = markDirty != null && markDirty;
this.defaultFlushMode = FlushMode.valueOf(hibernateSettings.getFlush().getMode().name());
-
+
MultiTenancySettings multiTenancySettings = settings.getMultiTenancy();
final TenantResolver multiTenantResolver = multiTenancySettings.getTenantResolver();
this.multiTenantMode = multiTenancySettings.getMode();
-
+
Class<? extends SchemaHandler> schemaHandlerClass = settings.getDataSource().getSchemaHandler();
this.schemaHandler = BeanUtils.instantiateClass(schemaHandlerClass);
this.tenantResolver = multiTenantResolver;
@@ -275,7 +279,9 @@ public class HibernateDatastore extends AbstractDatastore
this.transactionManager = new GrailsHibernateTransactionManager(
defaultConnectionSource.getSource(), defaultConnectionSource.getDataSource(), defaultFlushMode);
+ this.transactionManager.setDatastore(this);
this.eventPublisher = eventPublisher;
+ setApplicationEventPublisher(eventPublisher);
this.eventTriggeringInterceptor = new HibernateEventListener(this);
this.autoTimestampEventListener = new AutoTimestampEventListener(this);
@@ -283,6 +289,7 @@ public class HibernateDatastore extends AbstractDatastore
interceptor.setDatastore(this);
interceptor.setEventPublisher(eventPublisher);
registerEventListeners(this.eventPublisher);
+ registerAutoTimestampFlushEntityEventListener();
configureValidatorRegistry(mappingContext);
this.mappingContext.addMappingContextListener(new MappingContext.Listener() {
@Override
@@ -306,7 +313,22 @@ public class HibernateDatastore extends AbstractDatastore
if (ConnectionSource.DEFAULT.equals(connectionSource.getName())) {
childDatastore = this;
} else {
+ HibernateConnectionSourceSettings hcss = connectionSource.getSettings();
+ String schema = hcss.getHibernate().get("default_schema");
+ if (schema != null) {
+ try (Connection connection = defaultConnectionSource.getDataSource().getConnection()) {
+ try {
+ schemaHandler.useSchema(connection, schema);
+ } catch (Exception e) {
+ schemaHandler.createSchema(connection, schema);
+ }
+ schemaHandler.useDefaultSchema(connection);
+ } catch (SQLException e) {
+ LOG.warn("Could not create schema [" + schema + "]: " + e.getMessage());
+ }
+ }
childDatastore = createChildDatastore(mappingContext, eventPublisher, parent, singletonConnectionSources);
+ org.grails.datastore.gorm.GormRegistry.getInstance().registerDatastoreByQualifier(connectionSource.getName(), childDatastore);
}
datastoresByConnectionSource.put(connectionSource.getName(), childDatastore);
}
@@ -316,6 +338,7 @@ public class HibernateDatastore extends AbstractDatastore
singletonConnectionSources = new SingletonConnectionSources<>(
connectionSource, connectionSources.getBaseConfiguration());
HibernateDatastore childDatastore = createChildDatastore(mappingContext, eventPublisher, parent, singletonConnectionSources);
+ org.grails.datastore.gorm.GormRegistry.getInstance().registerDatastoreByQualifier(connectionSource.getName(), childDatastore);
datastoresByConnectionSource.put(connectionSource.getName(), childDatastore);
registerAllEntitiesWithEnhancer();
});
@@ -336,6 +359,9 @@ public class HibernateDatastore extends AbstractDatastore
}
this.gormEnhancer = initialize();
+ if (this.gormEnhancer != null) {
+ registerAllEntitiesWithEnhancer();
+ }
}
private HibernateDatastore createChildDatastore(
@@ -532,6 +558,25 @@ public class HibernateDatastore extends AbstractDatastore
}
}
+ /**
+ * Registers {@link GormAutoTimestampFlushEntityEventListener} as a prepended
+ * {@code FLUSH_ENTITY} listener so it runs before Hibernate's
+ * {@code DefaultFlushEntityEventListener} and can update {@code lastUpdated} on the
+ * entity before the dirty-property set is computed. This ensures {@code lastUpdated}
+ * is included in dynamic-update SQL even when {@code dynamicUpdate = true}.
+ */
+ protected void registerAutoTimestampFlushEntityEventListener() {
+ if (this.sessionFactory instanceof SessionFactoryImplementor sfi) {
+ EventListenerRegistry elr =
+ sfi.getServiceRegistry().getService(EventListenerRegistry.class);
+ if (elr != null) {
+ elr.getEventListenerGroup(EventType.FLUSH_ENTITY)
+ .prependListener(new GormAutoTimestampFlushEntityEventListener(
+ autoTimestampEventListener, mappingContext));
+ }
+ }
+ }
+
protected void configureValidatorRegistry(HibernateMappingContext mappingContext) {
StaticMessageSource messageSource = new StaticMessageSource();
ValidatorRegistry defaultValidatorRegistry = createValidatorRegistry(messageSource);
@@ -561,18 +606,28 @@ public class HibernateDatastore extends AbstractDatastore
datastoresByConnectionSource
);
} else {
- return new HibernateGormEnhancer(this, transactionManager, defaultConnectionSource.getSettings());
+ return new HibernateGormEnhancer(this, transactionManager, defaultConnectionSource.getSettings(), datastoresByConnectionSource);
}
}
@Override
public boolean hasCurrentSession() {
- return TransactionSynchronizationManager.getResource(sessionFactory) != null;
+ SessionFactory sf = getSessionFactory();
+ return super.hasCurrentSession() || (sf != null && TransactionSynchronizationManager.getResource(sf) != null);
+ }
+
+ public boolean hasCurrentTransaction() {
+ SessionFactory sf = getSessionFactory();
+ Object resource = sf != null ? TransactionSynchronizationManager.getResource(sf) : null;
+ if (resource instanceof org.grails.orm.hibernate.support.hibernate7.SessionHolder) {
+ return ((org.grails.orm.hibernate.support.hibernate7.SessionHolder) resource).getTransaction() != null;
+ }
+ return false;
}
@Override
protected Session createSession(PropertyResolver connectionDetails) {
- return new HibernateSession(this, sessionFactory);
+ return new HibernateSession(this, getSessionFactory());
}
@Override
@@ -634,17 +689,62 @@ public class HibernateDatastore extends AbstractDatastore
return session;
}
+ /**
+ * Returns the current GORM session for this datastore, using a priority-based lookup.
+ *
+ * <p>Priority order:
+ * <ol>
+ * <li>Custom session resolver (e.g. {@code StaticSingletonPersistenceContextInterceptor})</li>
+ * <li>GORM session holder in TSM (key = this datastore)</li>
+ * <li>Spring TX {@link SessionFactory} holder in TSM (key = SessionFactory) — used when
+ * {@code withTransaction{}} is active and the TX manager has bound the session</li>
+ * </ol>
+ *
+ * <p>Without priority 3, {@code DatastoreUtils.execute()} would call {@link #connect()} and
+ * open a brand-new standalone session even inside a {@code withTransaction{}} block,
+ * causing {@code TransactionRequiredException} on flush for SCHEMA multi-tenancy child
+ * datastores (each of which has its own {@link SessionFactory}).</p>
+ */
@Override
public Session getCurrentSession() throws ConnectionNotFoundException {
- return new HibernateSession(this, sessionFactory);
+ // Priority 1: custom session resolver
+ Session resolved = getSessionResolver().resolve();
+ if (resolved != null) {
+ return resolved;
+ }
+ // Priority 2: GORM session holder (key = this datastore)
+ org.grails.datastore.mapping.transactions.SessionHolder gormHolder =
+ (org.grails.datastore.mapping.transactions.SessionHolder)
+ TransactionSynchronizationManager.getResource(this);
+ if (gormHolder != null) {
+ Session s = gormHolder.getValidatedSession();
+ if (s != null) {
+ return s;
+ }
+ }
+ // Priority 3: Spring TX SessionFactory holder (key = SessionFactory).
+ // When withTransaction{} is active, the TX manager binds the Hibernate session here.
+ SessionFactory sf = getSessionFactory();
+ if (sf != null) {
+ Object resource = TransactionSynchronizationManager.getResource(sf);
+ if (resource instanceof org.grails.orm.hibernate.support.hibernate7.SessionHolder sfHolder) {
+ org.hibernate.Session nativeSession = sfHolder.getSession();
+ if (nativeSession != null && nativeSession.isOpen()) {
+ return new HibernateSession(this, sf, nativeSession);
+ }
+ }
+ }
+ throw new ConnectionNotFoundException(
+ "No Datastore Session bound to thread, and configuration does not allow creation of non-transactional one here");
}
@Override
public void destroy() {
if (!this.destroyed) {
+ org.grails.datastore.gorm.GormRegistry.getInstance().removeDatastore(this);
try {
for (HibernateDatastore childDatastore : datastoresByConnectionSource.values()) {
- if (childDatastore != this && childDatastore.getMappingContext() != getMappingContext()) {
+ if (childDatastore != this) {
childDatastore.destroy();
}
}
@@ -760,6 +860,7 @@ public class HibernateDatastore extends AbstractDatastore
ConnectionSource<SessionFactory, HibernateConnectionSourceSettings> connectionSource =
factory.create(schemaName, dataSourceConnectionSource, tenantSettings);
HibernateDatastore childDatastore = getChildDatastore(connectionSource);
+ org.grails.datastore.gorm.GormRegistry.getInstance().registerDatastoreByQualifier(schemaName, childDatastore);
datastoresByConnectionSource.put(connectionSource.getName(), childDatastore);
} finally {
TransactionSynchronizationManager.unbindResourceIfPossible(dataSource);
@@ -815,16 +916,21 @@ public class HibernateDatastore extends AbstractDatastore
messageSource);
}
+ /**
+ * @return The primary datastore
+ */
+ public HibernateDatastore getPrimaryDatastore() {
+ return this;
+ }
+
@Override
public MultiTenancySettings.MultiTenancyMode getMultiTenancyMode() {
- return this.multiTenantMode == MultiTenancySettings.MultiTenancyMode.SCHEMA ?
- MultiTenancySettings.MultiTenancyMode.DATABASE :
- this.multiTenantMode;
+ return this.multiTenantMode;
}
@Override
public Datastore getDatastoreForTenantId(Serializable tenantId) {
- if (getMultiTenancyMode() == MultiTenancySettings.MultiTenancyMode.DATABASE) {
+ if (getMultiTenancyMode() == MultiTenancySettings.MultiTenancyMode.DATABASE || getMultiTenancyMode() == MultiTenancySettings.MultiTenancyMode.SCHEMA) {
return getDatastoreForConnection(tenantId.toString());
} else {
return this;
@@ -877,7 +983,7 @@ public class HibernateDatastore extends AbstractDatastore
return failOnError;
}
- public boolean isOsivReadOnly() {
+ public boolean isOsivReadOnly(SessionFactory sessionFactory) {
return osivReadOnly;
}
@@ -941,33 +1047,67 @@ public class HibernateDatastore extends AbstractDatastore
@Override
public <T> T withSession(final Closure<T> callable) {
- Closure<T> multiTenantCallable = prepareMultiTenantClosure(callable);
- return getHibernateTemplate().execute(multiTenantCallable);
+ if (multiTenantMode == MultiTenancySettings.MultiTenancyMode.SCHEMA && !(this instanceof ChildHibernateDatastore)) {
+ Serializable tenantId = Tenants.currentId(this);
+ if (tenantId != null && !ConnectionSource.DEFAULT.equals(tenantId.toString())) {
+ return getDatastoreForTenantId(tenantId).withSession(callable);
+ }
+ }
+ final HibernateDatastore self = this;
+ return DatastoreUtils.execute(this, session -> {
+ org.hibernate.Session nativeSession = ((HibernateSession) session).getNativeSession();
+ SessionFactory sessionFactory = getSessionFactory();
+ boolean alreadyBound = TransactionSynchronizationManager.hasResource(sessionFactory);
+ if (!alreadyBound) {
+ org.grails.orm.hibernate.support.hibernate7.SessionHolder sessionHolder = new org.grails.orm.hibernate.support.hibernate7.SessionHolder(nativeSession);
+ TransactionSynchronizationManager.bindResource(sessionFactory, sessionHolder);
+ }
+ try {
+ Closure<T> multiTenantCallable = prepareMultiTenantClosure(callable);
+ return multiTenantCallable.call(nativeSession);
+ } finally {
+ if (!alreadyBound) {
+ TransactionSynchronizationManager.unbindResource(sessionFactory);
+ }
+ }
+ });
}
public <T> T withSession(String connectionName, final Closure<T> callable) {
- HibernateDatastore datastore = getDatastoreForConnection(connectionName);
- Closure<T> multiTenantCallable = datastore.prepareMultiTenantClosure(callable);
- return datastore.getHibernateTemplate().execute(multiTenantCallable);
+ return getDatastoreForConnection(connectionName).withSession(callable);
}
public <T> T withNewSession(String connectionName, final Closure<T> callable) {
- HibernateDatastore datastore = getDatastoreForConnection(connectionName);
- Closure<T> multiTenantCallable = datastore.prepareMultiTenantClosure(callable);
- return datastore.getHibernateTemplate().executeWithNewSession(multiTenantCallable);
+ return getDatastoreForConnection(connectionName).withNewSession(callable);
}
public <T> T withNewSession(final Closure<T> callable) {
- Closure<T> multiTenantCallable = prepareMultiTenantClosure(callable);
- return getHibernateTemplate().executeWithNewSession(multiTenantCallable);
+ // Delegate to GrailsHibernateTemplate.executeWithNewSession which correctly saves and
+ // restores both the Hibernate SessionHolder and the JDBC ConnectionHolder so that a
+ // nested transaction inside the closure starts clean (no pre-bound connection conflict).
+ final HibernateDatastore self = this;
+ final Closure<T> multiTenantCallable = prepareMultiTenantClosure(callable);
+ return getHibernateTemplate().executeWithNewSession(new Closure<T>(this) {
+ @Override
+ public T call(Object... args) {
+ org.hibernate.Session nativeSession = (org.hibernate.Session) args[0];
+ HibernateSession gormSession = new HibernateSession(self, self.getSessionFactory(), nativeSession);
+ DatastoreUtils.bindNewSession(gormSession);
+ try {
+ return multiTenantCallable.call(nativeSession);
+ } finally {
+ DatastoreUtils.unbindSession(gormSession);
+ // Native session is closed by executeWithNewSession's finally block,
+ // but we still want to clean up any GORM-specific state if needed.
+ }
+ }
+ });
}
@Override
public <T1> T1 withNewSession(Serializable tenantId, Closure<T1> callable) {
- if (getMultiTenancyMode() == MultiTenancySettings.MultiTenancyMode.DATABASE) {
- HibernateDatastore datastore = getDatastoreForConnection(tenantId.toString());
- SessionFactory sf = datastore.getSessionFactory();
- return datastore.getHibernateTemplate().executeWithExistingOrCreateNewSession(sf, callable);
+ if (getMultiTenancyMode() == MultiTenancySettings.MultiTenancyMode.DATABASE || getMultiTenancyMode() == MultiTenancySettings.MultiTenancyMode.SCHEMA) {
+ return ((HibernateDatastore) getDatastoreForTenantId(tenantId)).withNewSession(callable);
} else {
return withNewSession(callable);
}
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormApiFactory.groovy b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormApiFactory.groovy
new file mode 100644
index 0000000000..b75fd28d66
--- /dev/null
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormApiFactory.groovy
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.grails.orm.hibernate
+
+import groovy.transform.CompileStatic
+
+import org.grails.datastore.gorm.DefaultGormApiFactory
+import org.grails.datastore.gorm.DatastoreResolver
+import org.grails.datastore.gorm.GormInstanceApi
+import org.grails.datastore.gorm.GormRegistry
+import org.grails.datastore.gorm.GormStaticApi
+import org.grails.datastore.gorm.GormValidationApi
+import org.grails.datastore.mapping.model.MappingContext
+
+/**
+ * Hibernate-specific factory for creating GORM API objects.
+ * Creates Hibernate-specific API implementations (HibernateGormStaticApi, etc.)
+ * instead of generic GORM APIs.
+ *
+ * @since 8.0.0
+ */
+@CompileStatic
+class HibernateGormApiFactory extends DefaultGormApiFactory {
+
+ @Override
+ <D> GormStaticApi<D> createStaticApi(Class<D> persistentClass,
+ MappingContext mappingContext,
+ DatastoreResolver resolver,
+ String qualifier,
+ GormRegistry registry) {
+ def finders = createDynamicFinders(resolver, mappingContext)
+ ClassLoader classLoader = mappingContext.getMappingFactory().getClass().getClassLoader()
+ return new HibernateGormStaticApi<D>(persistentClass, mappingContext, finders, resolver, qualifier, classLoader)
+ }
+
+ @Override
+ <D> GormInstanceApi<D> createInstanceApi(Class<D> persistentClass,
+ MappingContext mappingContext,
+ DatastoreResolver resolver,
+ GormRegistry registry,
+ boolean failOnError,
+ boolean markDirty) {
+ ClassLoader classLoader = mappingContext.getMappingFactory().getClass().getClassLoader()
+ GormInstanceApi<D> instanceApi = new HibernateGormInstanceApi<D>(persistentClass, mappingContext, resolver, classLoader)
+ instanceApi.failOnError = failOnError
+ instanceApi.markDirty = markDirty
+ return instanceApi
+ }
+
+ @Override
+ <D> GormValidationApi<D> createValidationApi(Class<D> persistentClass,
+ MappingContext mappingContext,
+ DatastoreResolver resolver,
+ GormRegistry registry) {
+ ClassLoader classLoader = mappingContext.getMappingFactory().getClass().getClassLoader()
+ return new HibernateGormValidationApi<D>(persistentClass, mappingContext, resolver, classLoader)
+ }
+}
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormEnhancer.groovy b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormEnhancer.groovy
index 1349ae24f6..75ef7ab69e 100644
--- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormEnhancer.groovy
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormEnhancer.groovy
@@ -16,20 +16,6 @@
* specific language governing permissions and limitations
* under the License.
*/
-/* Copyright (C) 2011 SpringSource
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
package org.grails.orm.hibernate
import groovy.transform.CompileStatic
@@ -37,15 +23,14 @@ import groovy.transform.CompileStatic
import org.springframework.transaction.PlatformTransactionManager
import org.grails.datastore.gorm.GormEnhancer
-import org.grails.datastore.gorm.GormInstanceApi
-import org.grails.datastore.gorm.GormStaticApi
-import org.grails.datastore.gorm.GormValidationApi
+import org.grails.datastore.gorm.GormRegistry
import org.grails.datastore.mapping.core.Datastore
+import org.grails.datastore.mapping.core.connections.ConnectionSource
import org.grails.datastore.mapping.core.connections.ConnectionSourceSettings
+import org.grails.datastore.mapping.model.PersistentEntity
/**
- * Extended GORM Enhancer that fills out the remaining GORM for Hibernate methods
- * and implements string-based query support via HQL.
+ * A {@link GormEnhancer} for Hibernate.
*
* @author Graeme Rocher
* @since 1.0
@@ -53,47 +38,37 @@ import org.grails.datastore.mapping.core.connections.ConnectionSourceSettings
@CompileStatic
class HibernateGormEnhancer extends GormEnhancer {
+ private static final HibernateGormApiFactory API_FACTORY = new HibernateGormApiFactory()
+ protected final Map<String, HibernateDatastore> datastoresByConnectionSource
+
@Deprecated
HibernateGormEnhancer(HibernateDatastore datastore, PlatformTransactionManager transactionManager) {
- super(datastore, transactionManager)
+ this(datastore, transactionManager, datastore.connectionSources.defaultConnectionSource.settings)
}
- HibernateGormEnhancer(Datastore datastore, PlatformTransactionManager transactionManager, ConnectionSourceSettings settings) {
- super(datastore, transactionManager, settings)
+ HibernateGormEnhancer(HibernateDatastore datastore, PlatformTransactionManager transactionManager, ConnectionSourceSettings settings) {
+ this(datastore, transactionManager, settings, Collections.<String, HibernateDatastore>emptyMap())
}
- @Override
- protected <D> GormStaticApi<D> getStaticApi(Class<D> cls, String qualifier) {
- HibernateDatastore hibernateDatastore = (HibernateDatastore) datastore
- HibernateDatastore datastoreForConnection = hibernateDatastore.getDatastoreForConnection(qualifier)
- new HibernateGormStaticApi<D>(
- cls,
- datastoreForConnection,
- createDynamicFinders(datastoreForConnection),
- Thread.currentThread().contextClassLoader,
- datastoreForConnection.getTransactionManager(),
- qualifier
- )
+ HibernateGormEnhancer(HibernateDatastore datastore, PlatformTransactionManager transactionManager, ConnectionSourceSettings settings, Map<String, HibernateDatastore> datastoresByConnectionSource) {
+ super(datastore, transactionManager, settings, prepareRegistry())
+ this.datastoresByConnectionSource = datastoresByConnectionSource
}
- @Override
- protected <D> GormInstanceApi<D> getInstanceApi(Class<D> cls, String qualifier) {
- HibernateDatastore hibernateDatastore = (HibernateDatastore) datastore
- new HibernateGormInstanceApi<D>(cls, hibernateDatastore.getDatastoreForConnection(qualifier), Thread.currentThread().contextClassLoader)
+ private static GormRegistry prepareRegistry() {
+ GormRegistry registry = GormRegistry.instance
+ registry.registerApiFactory(HibernateDatastore, API_FACTORY)
+ return registry
}
@Override
- protected <D> GormValidationApi<D> getValidationApi(Class<D> cls, String qualifier) {
- HibernateDatastore hibernateDatastore = (HibernateDatastore) datastore
- new HibernateGormValidationApi<D>(cls, hibernateDatastore.getDatastoreForConnection(qualifier), Thread.currentThread().contextClassLoader)
+ List<String> allQualifiers(Datastore datastore, PersistentEntity entity) {
+ List<String> qualifiers = new ArrayList<>(super.allQualifiers(datastore, entity))
+ if (qualifiers.contains(ConnectionSource.ALL)) {
+ qualifiers.remove(ConnectionSource.ALL)
+ qualifiers.addAll(datastoresByConnectionSource.keySet())
+ }
+ return qualifiers.unique()
}
- @Override
- protected void registerConstraints(Datastore datastore) {
- // no-op
- }
-
- public static <D> GormStaticApi<D> findStaticApi(Class<D> cls, String qualifier) {
- GormEnhancer.findStaticApi(cls, qualifier)
- }
}
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormInstanceApi.groovy b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormInstanceApi.groovy
index 4034b5c1d9..14944a5fa3 100644
--- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormInstanceApi.groovy
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormInstanceApi.groovy
@@ -16,115 +16,251 @@
* specific language governing permissions and limitations
* under the License.
*/
-/*
- * Copyright 2013-2026 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
package org.grails.orm.hibernate
+import groovy.transform.CompileDynamic
import groovy.transform.CompileStatic
import org.codehaus.groovy.runtime.InvokerHelper
-import jakarta.persistence.FlushModeType
import jakarta.persistence.LockModeType
-import org.hibernate.HibernateException
import org.hibernate.LockMode
+
+import org.hibernate.Hibernate
import org.hibernate.Session
-import org.hibernate.SessionFactory
import org.hibernate.collection.spi.PersistentCollection
import org.hibernate.engine.spi.EntityEntry
import org.hibernate.engine.spi.SessionImplementor
import org.hibernate.persister.entity.EntityPersister
-import org.springframework.beans.BeanWrapperImpl
-import org.springframework.beans.InvalidPropertyException
-import org.springframework.dao.DataAccessException
import org.springframework.validation.Errors
import org.springframework.validation.Validator
import grails.gorm.validation.CascadingValidator
+import org.grails.datastore.gorm.DatastoreResolver
import org.grails.datastore.gorm.GormInstanceApi
import org.grails.datastore.gorm.GormValidateable
+import org.grails.datastore.gorm.finders.DynamicFinder
import org.grails.datastore.mapping.core.Datastore
+import org.grails.datastore.mapping.dirty.checking.DirtyCheckable
import org.grails.datastore.mapping.engine.event.ValidationEvent
+import org.grails.datastore.mapping.model.MappingContext
import org.grails.datastore.mapping.model.PersistentEntity
import org.grails.datastore.mapping.model.PersistentProperty
import org.grails.datastore.mapping.model.config.GormProperties
import org.grails.datastore.mapping.model.types.Association
-import org.grails.datastore.mapping.model.types.Embedded
import org.grails.datastore.mapping.model.types.ManyToMany
import org.grails.datastore.mapping.model.types.OneToMany
-import org.grails.datastore.mapping.model.types.ToOne
import org.grails.datastore.mapping.reflect.ClassUtils
import org.grails.datastore.mapping.reflect.EntityReflector
-import org.grails.orm.hibernate.cfg.GrailsHibernateUtil
+import org.grails.orm.hibernate.proxy.GroovyProxyInterceptorLogic
+import org.grails.orm.hibernate.support.ClosureEventListener
import org.grails.orm.hibernate.support.HibernateRuntimeUtils
/**
- * The implementation of the GORM instance API contract for Hibernate 7.
+ * Hibernate GORM instance API.
+ *
+ * @author Graeme Rocher
+ * @since 1.0
*/
@CompileStatic
class HibernateGormInstanceApi<D> extends GormInstanceApi<D> {
- private static final String ARGUMENT_VALIDATE = 'validate'
- private static final String ARGUMENT_DEEP_VALIDATE = 'deepValidate'
- private static final String ARGUMENT_FLUSH = 'flush'
- private static final String ARGUMENT_INSERT = 'insert'
- private static final String ARGUMENT_MERGE = 'merge'
- private static final String ARGUMENT_FAIL_ON_ERROR = 'failOnError'
- private static final Class DEFERRED_BINDING
+ protected Class<? extends Exception> validationException
+ protected final ClassLoader classLoader
+ protected IHibernateTemplate hibernateTemplate
- static {
- try {
- DEFERRED_BINDING = Class.forName('grails.validation.DeferredBindingActions')
- } catch (Throwable ignored) {
- DEFERRED_BINDING = null
+ HibernateGormInstanceApi(Class<D> persistentClass, HibernateDatastore datastore, ClassLoader classLoader) {
+ super(persistentClass, datastore)
+ this.classLoader = classLoader ?: persistentClass.classLoader
+ this.hibernateTemplate = (IHibernateTemplate) datastore.getHibernateTemplate()
+ initializeValidationException(this.classLoader)
+ }
+
+ HibernateGormInstanceApi(Class<D> persistentClass, MappingContext mappingContext, DatastoreResolver datastoreResolver, ClassLoader classLoader) {
+ super(persistentClass, mappingContext, datastoreResolver)
+ this.classLoader = classLoader ?: persistentClass.classLoader
+ initializeValidationException(this.classLoader)
+ }
+
+ protected void initializeValidationException(ClassLoader classLoader) {
+ for (cl in [classLoader, Thread.currentThread().getContextClassLoader(), HibernateGormInstanceApi.classLoader]) {
+ if (cl == null) continue
+ try {
+ this.validationException = (Class<? extends Exception>) cl.loadClass('grails.validation.ValidationException')
+ return
+ } catch (Throwable e) {
+ // ignore
+ }
}
+ this.validationException = org.grails.datastore.mapping.validation.ValidationException
}
- static final ThreadLocal<Boolean> insertActiveThreadLocal = new ThreadLocal<Boolean>()
+ protected HibernateDatastore getHibernateDatastore() {
+ return (HibernateDatastore) getDatastore()
+ }
- protected SessionFactory sessionFactory
- protected ClassLoader classLoader
- protected IHibernateTemplate hibernateTemplate
- boolean autoFlush
- protected InstanceApiHelper instanceApiHelper
+ InstanceApiHelper getInstanceApiHelper() {
+ return getHibernateDatastore().getInstanceApiHelper()
+ }
- HibernateGormInstanceApi(Class<D> persistentClass, HibernateDatastore datastore, ClassLoader classLoader) {
- super(persistentClass, datastore as Datastore)
- this.classLoader = classLoader
- this.sessionFactory = datastore.getSessionFactory()
- this.hibernateTemplate = (GrailsHibernateTemplate) datastore.getHibernateTemplate()
- this.autoFlush = datastore.autoFlush
- this.failOnError = datastore.failOnError
- this.markDirty = datastore.markDirty
- this.instanceApiHelper = datastore.getInstanceApiHelper()
+ /**
+ * Handles proxy-related method calls on Hibernate or Groovy proxies (e.g. isInitialized()).
+ */
+ @CompileDynamic
+ Object methodMissing(Object target, String name, Object[] args) {
+ if ('isInitialized' == name) {
+ Boolean groovyResult = GroovyProxyInterceptorLogic.isInitialized(target)
+ return groovyResult != null ? groovyResult : Hibernate.isInitialized(target)
+ }
+ if ('initialize' == name || 'getTarget' == name) {
+ Hibernate.initialize(target)
+ return target
+ }
+ throw new MissingMethodException(name, target?.class ?: persistentClass, args, false)
+ }
+
+ @Override
+ HibernateGormInstanceApi<D> forQualifier(String qualifier) {
+ Datastore ds = getDatastore()
+ if (ds == null) return this
+
+ org.grails.datastore.gorm.DatastoreResolver resolver = new org.grails.datastore.gorm.DatastoreResolver() {
+ @Override Datastore resolve() { org.grails.datastore.gorm.GormRegistry.instance.apiResolver.findDatastore(persistentClass, qualifier) }
+ }
+ HibernateGormInstanceApi<D> newApi = new HibernateGormInstanceApi<D>(persistentClass, ds.mappingContext, resolver, classLoader)
+ newApi.failOnError = failOnError
+ newApi.markDirty = markDirty
+ return newApi
+ }
+
+ protected IHibernateTemplate getHibernateTemplate() {
+ if (this.hibernateTemplate == null) {
+ HibernateDatastore datastore = getHibernateDatastore()
+ IHibernateTemplate template = (IHibernateTemplate) datastore.getHibernateTemplate()
+ if (qualifier != null && !org.grails.datastore.mapping.core.connections.ConnectionSource.DEFAULT.equals(qualifier) && datastore.getMultiTenancyMode() == org.grails.datastore.mapping.multitenancy.MultiTenancySettings.MultiTenancyMode.DISCRIMINATOR) {
+ String connectionName = datastore.connectionSources.defaultConnectionSource.name
+ if (!connectionName.equals(qualifier)) {
+ this.hibernateTemplate = new TenantBoundHibernateTemplate(template, (Serializable)qualifier, datastore)
+ } else {
+ this.hibernateTemplate = template
+ }
+ } else {
+ // For DEFAULT qualifier or non-discriminator mode, the datastore resolver may return
+ // different datastores in different transaction contexts (e.g., preferred datastore switching
+ // between a multi-datasource parent and a secondary child). Do not cache here — resolve
+ // the template dynamically on every call to avoid using a stale template from a prior context.
+ return template
+ }
+ }
+ return hibernateTemplate
+ }
+
+ /**
+ * Checks whether a field is dirty
+ * Gets the original persisted value of a field.
+ *
+ * @param fieldName The field name
+ * @return The original persisted value
+ */
+ Object getPersistentValue(D instance, String fieldName) {
+ SessionImplementor session = (SessionImplementor) getHibernateDatastore().getSessionFactory().getCurrentSession()
+ EntityEntry entry = findEntityEntry(instance, session)
+ if (entry == null || entry.getLoadedState() == null) {
+ if (instance instanceof DirtyCheckable) {
+ return ((DirtyCheckable) instance).getOriginalValue(fieldName)
+ }
+ return null
+ }
+
+ EntityPersister persister = entry.getPersister()
+ int fieldIndex = Arrays.asList(persister.getPropertyNames()).indexOf(fieldName)
+ return fieldIndex == -1 ? null : entry.getLoadedState()[fieldIndex]
+ }
+
+ protected EntityEntry findEntityEntry(D instance, SessionImplementor session) {
+ return session.getPersistenceContext().getEntry(instance)
+ }
+
+ @Override
+ List<String> getDirtyPropertyNames(D instance) {
+ if (instance instanceof DirtyCheckable) {
+ return ((DirtyCheckable) instance).listDirtyPropertyNames()
+ }
+ SessionImplementor session = (SessionImplementor) getHibernateDatastore().getSessionFactory().getCurrentSession()
+ EntityEntry entry = findEntityEntry(instance, session)
+ if (entry == null) {
+ return Collections.emptyList()
+ }
+
+ Object[] loadedState = entry.getLoadedState()
+ if (loadedState == null) {
+ return Collections.emptyList()
+ }
+
+ EntityPersister persister = entry.getPersister()
+ Object[] values = persister.getPropertyValues(instance)
+ int[] dirtyPropertyIndexes = persister.findDirty(values, loadedState, instance, session)
+ if (dirtyPropertyIndexes == null) {
+ return Collections.emptyList()
+ }
+
+ List<String> names = new ArrayList<>()
+ String[] propertyNames = persister.getPropertyNames()
+ for (int index : dirtyPropertyIndexes) {
+ names.add(propertyNames[index])
+ }
+ return names
+ }
+
+ @Override
+ boolean isDirty(D instance) {
+ if (!isAttached(instance)) {
+ return false
+ }
+ if (instance instanceof DirtyCheckable) {
+ return ((DirtyCheckable) instance).hasChanged()
+ }
+ SessionImplementor session = (SessionImplementor) getHibernateDatastore().getSessionFactory().getCurrentSession()
+ EntityEntry entry = findEntityEntry(instance, session)
+ if (entry == null) {
+ return false
+ }
+
+ Object[] loadedState = entry.getLoadedState()
+ if (loadedState == null) {
+ return true // brand new
+ }
+
+ EntityPersister persister = entry.getPersister()
+ Object[] values = persister.getPropertyValues(instance)
+ int[] dirtyPropertyIndexes = persister.findDirty(values, loadedState, instance, session)
+ return dirtyPropertyIndexes != null && dirtyPropertyIndexes.length > 0
+ }
+
+ @Override
+ boolean isDirty(D instance, String fieldName) {
+ if (!isAttached(instance)) {
+ return false
+ }
+ if (instance instanceof DirtyCheckable) {
+ return ((DirtyCheckable) instance).hasChanged(fieldName)
+ }
+ return false
}
@Override
D save(D target, Map arguments) {
- PersistentEntity domainClass = persistentEntity
+ PersistentEntity domainClass = getGormPersistentEntity()
runDeferredBinding()
boolean shouldFlush = shouldFlush(arguments)
- boolean shouldValidate = shouldValidate(arguments, persistentEntity)
+ boolean shouldValidate = shouldValidate(arguments, domainClass)
HibernateRuntimeUtils.autoAssociateBidirectionalOneToOnes(domainClass, target)
boolean deepValidate = true
- if (arguments?.containsKey(ARGUMENT_DEEP_VALIDATE)) {
- deepValidate = ClassUtils.getBooleanFromMap(ARGUMENT_DEEP_VALIDATE, arguments)
+ if (arguments?.containsKey(HibernateGormValidationApi.ARGUMENT_DEEP_VALIDATE)) {
+ deepValidate = ClassUtils.getBooleanFromMap(HibernateGormValidationApi.ARGUMENT_DEEP_VALIDATE, arguments)
}
if (shouldValidate) {
@@ -132,7 +268,7 @@ class HibernateGormInstanceApi<D> extends GormInstanceApi<D> {
Errors errors = HibernateRuntimeUtils.setupErrorsProperty(target)
if (validator) {
- datastore.applicationEventPublisher?.publishEvent new ValidationEvent(datastore, target)
+ getHibernateDatastore().applicationEventPublisher?.publishEvent new ValidationEvent(getHibernateDatastore(), target)
if (validator instanceof CascadingValidator) {
((CascadingValidator) validator).validate target, errors, deepValidate
@@ -145,7 +281,7 @@ class HibernateGormInstanceApi<D> extends GormInstanceApi<D> {
if (errors.hasErrors()) {
handleValidationError(domainClass, target, errors)
if (shouldFail(arguments)) {
- throw validationException.newInstance('Validation Error(s) occurred during save()', errors)
+ throw org.grails.datastore.mapping.validation.ValidationException.newInstance('Validation Error(s) occurred during save()', errors)
}
return null
}
@@ -153,15 +289,31 @@ class HibernateGormInstanceApi<D> extends GormInstanceApi<D> {
}
}
- autoRetrieveAssociations datastore, domainClass, target
+ autoRetrieveAssociations getHibernateDatastore(), domainClass, target
GormValidateable validateable = (GormValidateable) target
validateable.skipValidation(true)
+ if (!deepValidate) {
+ ClosureEventListener.SKIP_DEEP_VALIDATION.set(Boolean.TRUE)
+ }
try {
return performUpsert(target, shouldFlush)
} finally {
validateable.skipValidation(false)
+ if (!deepValidate) {
+ ClosureEventListener.SKIP_DEEP_VALIDATION.remove()
+ }
+ }
+ }
+
+ private static final Class DEFERRED_BINDING
+
+ static {
+ try {
+ DEFERRED_BINDING = HibernateGormInstanceApi.classLoader.loadClass('org.grails.datastore.mapping.core.DeferredBindingActions')
+ } catch (Throwable e) {
+ DEFERRED_BINDING = null
}
}
@@ -171,89 +323,203 @@ class HibernateGormInstanceApi<D> extends GormInstanceApi<D> {
}
}
+ protected void autoRetrieveAssociations(Datastore datastore, PersistentEntity domainClass, D target) {
+ // no-op, handled by Hibernate
+ }
+
+ protected boolean isAutoFlush() {
+ return getHibernateDatastore().isAutoFlush()
+ }
+
@Override
- D merge(D instance, Map params) {
- Map args = new HashMap(params)
- args[ARGUMENT_MERGE] = true
- return save(instance, args)
+ boolean isFailOnError() {
+ return getHibernateDatastore().isFailOnError()
}
@Override
- D insert(D instance, Map params) {
- Map args = new HashMap(params)
- args[ARGUMENT_INSERT] = true
- return save(instance, args)
+ boolean isMarkDirty() {
+ return getHibernateDatastore().markDirty
+ }
+
+ protected boolean shouldFlush(Map arguments) {
+ if (arguments?.containsKey('flush')) {
+ return ClassUtils.getBooleanFromMap('flush', arguments)
+ }
+ if (arguments?.containsKey(DynamicFinder.ARGUMENT_FLUSH_MODE)) {
+ return ClassUtils.getBooleanFromMap(DynamicFinder.ARGUMENT_FLUSH_MODE, arguments)
+ }
+ return isAutoFlush()
+ }
+
+ protected boolean shouldValidate(Map arguments, PersistentEntity domainClass) {
+ if (arguments?.containsKey('validate')) {
+ return ClassUtils.getBooleanFromMap('validate', arguments)
+ }
+ if (arguments?.containsKey(org.grails.datastore.gorm.GormValidationApi.ARGUMENT_DEEP_VALIDATE)) {
+ return ClassUtils.getBooleanFromMap(org.grails.datastore.gorm.GormValidationApi.ARGUMENT_DEEP_VALIDATE, arguments)
+ }
+ return true
+ }
+
+ protected boolean shouldFail(Map arguments) {
+ if (arguments?.containsKey('failOnError')) {
+ return ClassUtils.getBooleanFromMap('failOnError', arguments)
+ }
+ return isFailOnError()
}
@Override
- void discard(D instance) {
- hibernateTemplate.evict instance
+ D merge(D target, Map arguments) {
+ return performMerge(target, shouldFlush(arguments))
}
@Override
- void delete(D instance, Map params = Collections.emptyMap()) {
- boolean flush = shouldFlush(params)
- try {
- hibernateTemplate.execute { Session session ->
- session.remove instance
- if (flush) {
- session.flush()
+ D insert(D target, Map arguments) {
+ PersistentEntity domainClass = getGormPersistentEntity()
+ runDeferredBinding()
+ boolean shouldFlush = shouldFlush(arguments)
+ boolean shouldValidate = shouldValidate(arguments, domainClass)
+
+ if (shouldValidate) {
+ Validator validator = datastore.mappingContext.getEntityValidator(domainClass)
+ Errors errors = HibernateRuntimeUtils.setupErrorsProperty(target)
+
+ if (validator) {
+ getHibernateDatastore().applicationEventPublisher?.publishEvent new ValidationEvent(getHibernateDatastore(), target)
+ validator.validate target, errors
+
+ if (errors.hasErrors()) {
+ handleValidationError(domainClass, target, errors)
+ if (shouldFail(arguments)) {
+ throw org.grails.datastore.mapping.validation.ValidationException.newInstance('Validation Error(s) occurred during insert()', errors)
+ }
+ return null
}
}
}
- catch (DataAccessException e) {
- try {
- hibernateTemplate.execute { Session session ->
- session.setFlushMode(FlushModeType.COMMIT)
+
+ GormValidateable validateable = (GormValidateable) target
+ validateable.skipValidation(true)
+
+ try {
+ return (D) execute({ org.grails.datastore.mapping.core.Session session ->
+ session.insert(target)
+ if (shouldFlush) {
+ session.flush()
}
- }
- finally {
- throw e
+ return target
+ } as org.grails.datastore.mapping.core.SessionCallback)
+ } finally {
+ validateable.skipValidation(false)
+ }
+ }
+
+ @Override
+ void delete(D target, Map arguments) {
+ getHibernateTemplate().execute { Session session ->
+ session.remove target
+ if (shouldFlush(arguments)) {
+ session.flush()
}
}
}
@Override
boolean isAttached(D instance) {
- hibernateTemplate.contains instance
+ getHibernateTemplate().contains instance
}
@Override
D lock(D instance) {
- hibernateTemplate.lock(instance, LockMode.PESSIMISTIC_WRITE)
+ getHibernateTemplate().lock(instance, LockMode.PESSIMISTIC_WRITE)
instance
}
@Override
D attach(D instance) {
- hibernateTemplate.execute { Session session ->
+ getHibernateTemplate().execute { Session session ->
HibernateAttachSupport.attach(instance, session)
}
return instance
}
@Override
- D refresh(D instance) {
- hibernateTemplate.refresh(instance)
- return instance
+ void discard(D target) {
+ getHibernateTemplate().execute { Session session ->
+ if (sessionContains(session, target)) {
+ session.detach target
+ }
+ }
+ }
+
+ @Override
+ D refresh(D target) {
+ getHibernateTemplate().execute { Session session ->
+ session.refresh target
+ }
+ return target
+ }
+
+ D read(Serializable id) {
+ (D) getHibernateTemplate().execute { Session session ->
+ D instance = (D) session.get(persistentClass, id)
+ if (instance != null) {
+ session.setReadOnly(instance, true)
+ }
+ return instance
+ }
}
protected D performUpsert(D target, boolean shouldFlush) {
- PersistentEntity entity = persistentEntity
- String idPropertyName = entity.identity?.name ?: 'id'
- Object idVal = InvokerHelper.getProperty(target, idPropertyName)
- if (idVal == null) {
- return performPersist(target, shouldFlush)
- } else {
- return performMerge(target, shouldFlush)
+ getHibernateTemplate().execute { Session session ->
+ if (sessionContains(session, target)) {
+ if (shouldFlush) {
+ flushSession session
+ }
+ return target
+ } else {
+ PersistentProperty identityProperty = getGormPersistentEntity().identity
+ if (identityProperty == null) {
+ // Composite ID entity — the user always supplies all key properties.
+ // Hibernate merge() handles both the first-save (INSERT) and update (UPDATE) paths.
+ return performMerge(target, shouldFlush)
+ }
+ Serializable id = (Serializable) InvokerHelper.getProperty(target, identityProperty.name)
+ if (id == null) {
+ return performPersist(target, shouldFlush)
+ } else {
+ return performMerge(target, shouldFlush)
+ }
+ }
+ }
+ }
+
+ protected void flushSession(Session session) {
+ HibernateDatastore datastore = getHibernateDatastore()
+ if (datastore.isOsivReadOnly(datastore.sessionFactory)) {
+ return
+ }
+ session.flush()
+ }
+
+ /**
+ * Hibernate 7 changed {@code Session.contains()} to throw {@link IllegalArgumentException}
+ * when the supplied object's class is not a mapped entity in this session factory, instead of
+ * returning {@code false} as Hibernate 5 did. All call sites in this class must go through
+ * this helper so they safely degrade to {@code false} for cross-datasource entities.
+ */
+ private static boolean sessionContains(Session session, Object target) {
+ try {
+ return session.contains(target)
+ } catch (IllegalArgumentException ignored) {
+ return false
}
}
protected D performMerge(final D target, final boolean flush) {
- hibernateTemplate.execute { Session session ->
+ getHibernateTemplate().execute { Session session ->
D merged
- if (session.contains(target)) {
- // Entity is already managed in this session — merging would cause H7 to create
+ if (sessionContains(session, target)) {
// a second PersistentCollection for the same role+key ("two representations").
// Just use the entity as-is; dirty-checking + cascade will handle children.
merged = target
@@ -262,23 +528,28 @@ class HibernateGormInstanceApi<D> extends GormInstanceApi<D> {
merged = (D) session.merge(target)
session.lock(merged, LockModeType.NONE)
// Sync id back immediately so target has an identity
- String idProp = persistentEntity.identity?.name ?: 'id'
+ PersistentEntity entity = getGormPersistentEntity()
+ String idProp = entity.identity?.name ?: 'id'
InvokerHelper.setProperty(target, idProp, InvokerHelper.getProperty(merged, idProp))
}
if (flush) {
flushSession session
}
// Sync version after flush so the incremented value is captured
- PersistentProperty versionProperty = persistentEntity.version
+ PersistentEntity entity = getGormPersistentEntity()
+ PersistentProperty versionProperty = entity.version
if (versionProperty != null) {
InvokerHelper.setProperty(target, versionProperty.name, InvokerHelper.getProperty(merged, versionProperty.name))
}
- return target
+ // Return the session-managed instance so callers can use it in subsequent session
+ // operations without triggering NonUniqueObjectException when the same entity
+ // is referenced again (e.g. as a cascade target or query parameter).
+ return merged
}
}
protected D performPersist(final D target, final boolean shouldFlush) {
- hibernateTemplate.execute { Session session ->
+ getHibernateTemplate().execute { Session session ->
try {
markInsertActive()
session.persist target
@@ -308,12 +579,13 @@ class HibernateGormInstanceApi<D> extends GormInstanceApi<D> {
*/
@SuppressWarnings('unchecked')
private void reconcileCollections(Session session, D target) {
- EntityReflector reflector = datastore.mappingContext.getEntityReflector(persistentEntity)
+ PersistentEntity entity = getGormPersistentEntity()
+ EntityReflector reflector = datastore.mappingContext.getEntityReflector(entity)
if (reflector == null) return
SessionImplementor si = (SessionImplementor) session
- for (Association assoc in persistentEntity.associations) {
+ for (Association assoc in entity.associations) {
if (!(assoc instanceof OneToMany) && !(assoc instanceof ManyToMany)) continue
String propName = assoc.name
@@ -338,186 +610,39 @@ class HibernateGormInstanceApi<D> extends GormInstanceApi<D> {
}
}
- protected static void flushSession(Session session) throws HibernateException {
- try {
- session.flush()
- } catch (HibernateException e) {
- session.setFlushMode(FlushModeType.COMMIT)
- throw e
- }
+ @CompileDynamic
+ protected void handleValidationError(PersistentEntity domainClass, D target, Errors errors) {
+ InvokerHelper.setProperty(target, GormProperties.ERRORS, errors)
}
- @SuppressWarnings('unchecked')
- private void autoRetrieveAssociations(Datastore datastore, PersistentEntity entity, Object target) {
- EntityReflector reflector = datastore.mappingContext.getEntityReflector(entity)
- IHibernateTemplate t = this.hibernateTemplate
- for (PersistentProperty prop in entity.associations) {
- if (prop instanceof ToOne && !(prop instanceof Embedded)) {
- ToOne toOne = (ToOne) prop
- def propertyName = prop.name
- def propValue = reflector.getProperty(target, propertyName)
- if (propValue == null || t.contains(propValue)) {
- continue
- }
-
- PersistentEntity otherSide = toOne.associatedEntity
- if (otherSide == null) continue
-
- def identity = otherSide.identity
- if (identity == null) continue
-
- def otherSideReflector = datastore.mappingContext.getEntityReflector(otherSide)
- try {
- def id = (Serializable) otherSideReflector.getProperty(propValue, identity.name)
- if (id) {
- final Object associatedInstance = t.get(prop.type, id)
- if (associatedInstance) {
- reflector.setProperty(target, propertyName, associatedInstance)
- }
- }
- }
- catch (InvalidPropertyException ignored) {
- }
- }
- }
- }
-
- private static boolean shouldValidate(Map arguments, PersistentEntity entity) {
- if (!entity) return false
- if (arguments?.containsKey(ARGUMENT_VALIDATE)) {
- return ClassUtils.getBooleanFromMap(ARGUMENT_VALIDATE, arguments)
- }
- return true
- }
-
- protected boolean shouldFlush(Map map) {
- if (map?.containsKey(ARGUMENT_FLUSH)) {
- return ClassUtils.getBooleanFromMap(ARGUMENT_FLUSH, map)
- }
- return autoFlush
- }
-
- protected boolean shouldFail(Map map) {
- if (map?.containsKey(ARGUMENT_FAIL_ON_ERROR)) {
- return ClassUtils.getBooleanFromMap(ARGUMENT_FAIL_ON_ERROR, map)
- }
- return failOnError
- }
-
- protected Object handleValidationError(PersistentEntity entity, final Object target, Errors errors) {
- setObjectToReadOnly target
- if (entity) {
- for (Association association in entity.associations) {
- if (association instanceof ToOne && !association instanceof Embedded) {
- def bean = new BeanWrapperImpl(target)
- def propertyValue = bean.getPropertyValue(association.name)
- if (propertyValue != null) {
- setObjectToReadOnly propertyValue
- }
- }
- }
- }
- setErrorsOnInstance target, errors
- return null
+ @CompileDynamic
+ protected void markInsertActive() {
+ HibernateRuntimeUtils.markInsertActive()
}
- protected static void setErrorsOnInstance(Object target, Errors errors) {
- if (target instanceof GormValidateable) {
- ((GormValidateable) target).setErrors(errors)
- } else {
- ((GroovyObject) target).setProperty(GormProperties.ERRORS, errors)
- }
- }
-
- static void markInsertActive() {
- insertActiveThreadLocal.set(Boolean.TRUE)
+ @CompileDynamic
+ protected static void resetInsertActive() {
+ HibernateRuntimeUtils.resetInsertActive()
}
- static void resetInsertActive() {
- insertActiveThreadLocal.remove()
- }
-
- // --- Dirty Checking Logic ---
-
- boolean isDirty(D instance, String fieldName) {
- SessionImplementor session = (SessionImplementor) sessionFactory.currentSession
- EntityEntry entry = findEntityEntry(instance, session)
- if (!entry || !entry.loadedState) return false
-
- EntityPersister persister = entry.persister
- Object[] values = persister.getValues(instance)
- int[] dirtyProperties = findDirty(persister, values, entry, instance, session)
- if (dirtyProperties == null) return false
-
- String[] propertyNames = persister.getPropertyNames()
- int fieldIndex = -1
- for (int i = 0; i < propertyNames.length; i++) {
- if (propertyNames[i] == fieldName) {
- fieldIndex = i; break
- }
- }
- return fieldIndex in dirtyProperties
+ @CompileDynamic
+ void setObjectToReadWrite(Object target) {
+ HibernateRuntimeUtils.setObjectToReadWrite(target, getHibernateDatastore().sessionFactory)
}
- boolean isDirty(D instance) {
- SessionImplementor session = (SessionImplementor) sessionFactory.currentSession
- EntityEntry entry = findEntityEntry(instance, session)
- if (!entry || !entry.loadedState) return false
-
- EntityPersister persister = entry.persister
- Object[] currentState = persister.getValues(instance)
- int[] dirtyPropertyIndexes = findDirty(persister, currentState, entry, instance, session)
- return dirtyPropertyIndexes != null
+ @CompileDynamic
+ void setObjectToReadOnly(Object target) {
+ HibernateRuntimeUtils.setObjectToReadOnly(target, getHibernateDatastore().sessionFactory)
}
- List<String> getDirtyPropertyNames(D instance) {
- SessionImplementor session = (SessionImplementor) sessionFactory.currentSession
- EntityEntry entry = findEntityEntry(instance, session)
- if (!entry || !entry.loadedState) return []
-
- EntityPersister persister = entry.persister
- Object[] currentState = persister.getValues(instance)
- int[] dirtyPropertyIndexes = findDirty(persister, currentState, entry, instance, session)
-
- List<String> names = []
- String[] propertyNames = persister.getPropertyNames()
- if (dirtyPropertyIndexes != null) {
- for (int index : dirtyPropertyIndexes) {
- names.add(propertyNames[index])
+ @CompileDynamic
+ protected void incrementVersion(Object target) {
+ PersistentEntity persistentEntity = getGormPersistentEntity()
+ if (persistentEntity.isVersioned() && target.hasProperty(GormProperties.VERSION)) {
+ Object version = target."${GormProperties.VERSION}"
+ if (version instanceof Long) {
+ target."${GormProperties.VERSION}" = ++((Long) version)
}
}
- return names
- }
-
- Object getPersistentValue(D instance, String fieldName) {
- SessionImplementor session = (SessionImplementor) sessionFactory.currentSession
- def entry = findEntityEntry(instance, session, false)
- if (!entry || !entry.loadedState) return null
-
- EntityPersister persister = entry.persister
- String[] propertyNames = persister.getPropertyNames()
- int fieldIndex = propertyNames.findIndexOf { it == fieldName }
- return fieldIndex == -1 ? null : entry.loadedState[fieldIndex]
- }
-
- // --- Helper Methods using proper Generic definitions to satisfy stubs ---
-
- private static <T> int[] findDirty(EntityPersister persister, Object[] values, EntityEntry entry, T instance, SessionImplementor session) {
- persister.findDirty(values, entry.loadedState, instance, session)
- }
-
- protected static <T> EntityEntry findEntityEntry(T instance, SessionImplementor session, boolean forDirtyCheck = true) {
- def entry = session.persistenceContext.getEntry(instance)
- if (!entry) return null
- if (forDirtyCheck && !entry.requiresDirtyCheck(instance) && entry.loadedState) return null
- return entry
- }
-
- void setObjectToReadWrite(Object target) {
- GrailsHibernateUtil.setObjectToReadWrite(target, sessionFactory)
- }
-
- void setObjectToReadOnly(Object target) {
- GrailsHibernateUtil.setObjectToReadyOnly(target, sessionFactory)
}
}
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormStaticApi.groovy b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormStaticApi.groovy
index b77b7e39c4..b58454d727 100644
--- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormStaticApi.groovy
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormStaticApi.groovy
@@ -16,258 +16,215 @@
* specific language governing permissions and limitations
* under the License.
*/
-/*
- * Copyright 2013 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
package org.grails.orm.hibernate
+import groovy.transform.CompileDynamic
import groovy.transform.CompileStatic
-import groovy.util.logging.Slf4j
import org.grails.datastore.mapping.query.Query as GormQuery
-import org.hibernate.Session
-import org.hibernate.SessionFactory
-import org.hibernate.jpa.AvailableHints
+import org.hibernate.FlushMode
import org.springframework.core.convert.ConversionService
import org.springframework.transaction.PlatformTransactionManager
import grails.orm.HibernateCriteriaBuilder
-import grails.gorm.DetachedCriteria
+import org.grails.datastore.gorm.DatastoreResolver
import org.grails.datastore.gorm.GormStaticApi
+import org.grails.datastore.gorm.finders.DynamicFinder
import org.grails.datastore.gorm.finders.FinderMethod
+import org.grails.datastore.gorm.proxy.GroovyProxyFactory
+import org.grails.datastore.mapping.core.Datastore
+import org.grails.datastore.mapping.core.Session
+import org.grails.datastore.mapping.core.SessionCallback
import org.grails.datastore.mapping.core.connections.ConnectionSource
-import org.grails.datastore.mapping.core.connections.ConnectionSourcesProvider
-import org.grails.datastore.mapping.proxy.ProxyHandler
+import org.grails.datastore.mapping.model.MappingContext
+import org.grails.datastore.mapping.model.PersistentEntity
import org.grails.datastore.mapping.model.PersistentProperty
-import org.grails.datastore.mapping.query.api.BuildableCriteria as GrailsCriteria
+import org.grails.datastore.mapping.model.config.GormProperties
+import org.grails.datastore.mapping.model.types.Basic
+import org.grails.datastore.mapping.model.types.Simple
+import org.grails.datastore.mapping.query.Restrictions
+import org.grails.datastore.mapping.query.api.BuildableCriteria
import org.grails.datastore.mapping.query.event.PostQueryEvent
import org.grails.datastore.mapping.query.event.PreQueryEvent
+import org.grails.datastore.mapping.reflect.ClassUtils
import org.grails.orm.hibernate.cfg.domainbinding.hibernate.GrailsHibernatePersistentEntity
import org.grails.orm.hibernate.query.HibernateHqlQueryCreator
-import org.grails.orm.hibernate.query.HibernatePagedResultList
-import org.grails.orm.hibernate.query.MutationHqlQuery
import org.grails.orm.hibernate.query.HibernateQuery
import org.grails.orm.hibernate.query.HibernateQueryArgument
import org.grails.orm.hibernate.query.HqlListQueryBuilder
import org.grails.orm.hibernate.query.HqlQueryContext
+import org.grails.orm.hibernate.query.MutationHqlQuery
+import org.grails.orm.hibernate.query.PagedResultList
import org.grails.orm.hibernate.support.HibernateRuntimeUtils
/**
- * The implementation of the GORM static method contract for Hibernate
+ * Hibernate GORM static API.
*
* @author Graeme Rocher
* @since 1.0
*/
-@Slf4j
@CompileStatic
-//TODO Duplication!!
class HibernateGormStaticApi<D> extends GormStaticApi<D> {
protected GrailsHibernateTemplate hibernateTemplate
- protected ConversionService conversionService
- protected final HibernateSession hibernateSession
- protected ProxyHandler proxyHandler
- protected SessionFactory sessionFactory
- protected Class identityType
- protected ClassLoader classLoader
- protected String qualifier
- private HibernateGormInstanceApi<D> instanceApi
-
- HibernateGormStaticApi(Class<D> persistentClass, HibernateDatastore datastore, List<FinderMethod> finders,
- ClassLoader classLoader, PlatformTransactionManager transactionManager, String qualifier = null) {
- super(persistentClass, datastore, finders, transactionManager)
- this.datastore = datastore
+ protected final ClassLoader classLoader
+
+ private static final Set<String> PAGINATION_ARGS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
+ DynamicFinder.ARGUMENT_MAX,
+ DynamicFinder.ARGUMENT_OFFSET,
+ DynamicFinder.ARGUMENT_SORT,
+ DynamicFinder.ARGUMENT_ORDER,
+ DynamicFinder.ARGUMENT_FETCH,
+ DynamicFinder.ARGUMENT_IGNORE_CASE,
+ DynamicFinder.ARGUMENT_FETCH_SIZE,
+ DynamicFinder.ARGUMENT_TIMEOUT,
+ DynamicFinder.ARGUMENT_READ_ONLY,
+ DynamicFinder.ARGUMENT_FLUSH_MODE,
+ 'cache'
+ )))
+
+ HibernateGormStaticApi(Class<D> persistentClass, HibernateDatastore datastore, List<FinderMethod> finders, DatastoreResolver datastoreResolver, String qualifier, ClassLoader classLoader) {
+ super(persistentClass, datastore.mappingContext, finders, datastoreResolver, qualifier)
this.hibernateTemplate = (GrailsHibernateTemplate) datastore.getHibernateTemplate()
- this.conversionService = datastore.mappingContext.conversionService
- this.proxyHandler = datastore.mappingContext.proxyHandler
- this.hibernateSession = new HibernateSession(
- (HibernateDatastore) datastore,
- hibernateTemplate.getSessionFactory()
- )
this.classLoader = classLoader
- this.sessionFactory = datastore.getSessionFactory()
- this.identityType = persistentEntity.identity?.type
- this.instanceApi = new HibernateGormInstanceApi<>(persistentClass, datastore, classLoader)
- this.qualifier = qualifier
}
- GrailsHibernateTemplate getHibernateTemplate() {
- return hibernateTemplate as GrailsHibernateTemplate
+ HibernateGormStaticApi(Class<D> persistentClass, HibernateDatastore datastore, List<FinderMethod> finders, ClassLoader classLoader, DatastoreResolver datastoreResolver, String qualifier) {
+ this(persistentClass, datastore, finders, datastoreResolver, qualifier, classLoader)
}
- String getQualifier() {
- if (qualifier != null) return qualifier
- def dsNames = persistentEntity.mapping.mappedForm.datasources
- if (dsNames) {
- String first = dsNames[0]
- if (first != ConnectionSource.DEFAULT && first != 'ALL') {
- return first
- }
- }
- null
+ HibernateGormStaticApi(Class<D> persistentClass, HibernateDatastore datastore, List<FinderMethod> finders, ClassLoader classLoader, PlatformTransactionManager transactionManager) {
+ this(persistentClass, datastore, finders, new DatastoreResolver() {
+ @Override Datastore resolve() { datastore }
+ }, ConnectionSource.DEFAULT, classLoader)
}
- GormStaticApi<D> getApi(String qualifier) {
- (GormStaticApi<D>) HibernateGormEnhancer.findStaticApi(persistentClass, qualifier)
- }
-
- @Override
- DetachedCriteria<D> where(Closure callable) {
- new HibernateDetachedCriteria<D>(persistentClass).build(callable)
+ HibernateGormStaticApi(Class<D> persistentClass, MappingContext mappingContext, List<FinderMethod> finders, DatastoreResolver datastoreResolver, String qualifier, ClassLoader classLoader) {
+ super(persistentClass, mappingContext, finders, datastoreResolver, qualifier)
+ this.classLoader = classLoader
}
- @Override
- DetachedCriteria<D> whereLazy(Closure callable) {
- new HibernateDetachedCriteria<D>(persistentClass).buildLazy(callable)
+ protected HibernateDatastore getHibernateDatastore() {
+ (HibernateDatastore) getDatastore()
}
- @Override
- DetachedCriteria<D> whereAny(Closure callable) {
- (DetachedCriteria<D>) new HibernateDetachedCriteria<D>(persistentClass).or(callable)
- }
-
- @Override
- D merge(D d) {
- instanceApi.merge(d)
+ String getQualifier() {
+ return this.@qualifier
}
- @Override
- <T> T withNewSession(Closure<T> callable) {
- if (persistentEntity.isMultiTenant()) {
- return ((HibernateDatastore) datastore).withNewSession(callable)
- }
- String q = getQualifier()
- if (q != null && q != ConnectionSource.DEFAULT) {
- return ((HibernateDatastore) datastore).withNewSession(q, callable)
+ protected IHibernateTemplate getHibernateTemplate() {
+ IHibernateTemplate template = (IHibernateTemplate) getHibernateDatastore().getHibernateTemplate()
+ String connectionName = getHibernateDatastore().connectionSources.defaultConnectionSource.name
+ if (qualifier != null && !connectionName.equals(qualifier) && !ConnectionSource.DEFAULT.equals(qualifier) && getHibernateDatastore().getMultiTenancyMode() == org.grails.datastore.mapping.multitenancy.MultiTenancySettings.MultiTenancyMode.DISCRIMINATOR) {
+ return new TenantBoundHibernateTemplate(template, (Serializable)qualifier, getHibernateDatastore())
}
- ((HibernateDatastore) datastore).withNewSession(callable)
+ return template
}
@Override
- <T> T withSession(Closure<T> callable) {
- if (persistentEntity.isMultiTenant()) {
- return ((HibernateDatastore) datastore).withSession(callable)
- }
- String q = getQualifier()
- if (q != null && q != ConnectionSource.DEFAULT) {
- return ((HibernateDatastore) datastore).withSession(q, callable)
- }
- ((HibernateDatastore) datastore).withSession(callable)
+ BuildableCriteria createCriteria() {
+ HibernateDatastore ds = getHibernateDatastore()
+ new HibernateCriteriaBuilder(persistentClass, ds.sessionFactory, ds)
}
- D get(Serializable id) {
- if (id == null) {
- return null
- }
-
+ @Override
+ boolean exists(Serializable id) {
+ if (id == null) return false
id = convertIdentifier(id)
-
- if (id == null) {
- return null
- }
-
- if (persistentEntity.isMultiTenant()) {
- // for multi-tenant entities we process get(..) via a query
- (D) hibernateTemplate.execute { Session session ->
- new HibernateQuery(hibernateSession, (GrailsHibernatePersistentEntity) persistentEntity).idEq(id).singleResult()
+ if (id == null) return false
+ PersistentEntity pe = getGormPersistentEntity()
+
+ return (Boolean) getHibernateTemplate().execute { org.hibernate.Session session ->
+ StringBuilder hql = new StringBuilder('select count(e) from ').append(pe.name).append(' e where ')
+ Map<String, Object> params = [:]
+
+ PersistentProperty identity = pe.getIdentity()
+ if (identity != null) {
+ hql.append('e.').append(identity.name).append(' = :id')
+ params.id = id
+ } else {
+ PersistentProperty[] compositeId = pe.getCompositeIdentity()
+ if (compositeId != null && compositeId.length > 0) {
+ List<String> conditions = []
+ for (prop in compositeId) {
+ conditions << ("e.${prop.name} = :${prop.name}".toString())
+ params[prop.name] = id[prop.name]
+ }
+ hql.append(conditions.join(' and '))
+ } else {
+ return false
+ }
}
- } else {
- // for non multi-tenant entities we process get(..) via the second level cache
- (D) hibernateTemplate.execute { Session session -> session.find(persistentEntity.javaClass, id) }
- }
- }
- D read(Serializable id) {
- if (id == null) {
- return null
+ org.hibernate.query.Query<Long> q = session.createQuery(hql.toString(), Long)
+ params.each { k, v -> q.setParameter(k, v) }
+ return q.uniqueResult() > 0L
}
- id = convertIdentifier(id)
-
- if (id == null) {
- return null
- }
-
- String hql = "from ${persistentEntity.name} where ${persistentEntity.identity.name} = :id"
- Map<String, Object> args = [(AvailableHints.HINT_READ_ONLY): (Object) true]
- proxyHandler.unwrap(doSingleInternal(hql, [id: id], [], args, false)) as D
}
@Override
- D load(Serializable id) {
- id = convertIdentifier(id)
- if (id != null) {
- return (D) hibernateTemplate.load((Class) persistentClass, id)
- } else {
- return null
- }
- }
+ HibernateGormStaticApi<D> forQualifier(String qualifier) {
+ Datastore ds = getDatastore()
+ if (ds == null) return this
- @Override
- D proxy(Serializable id) {
- id = convertIdentifier(id)
- if (id != null) {
- // Use the configured MappingContext proxyFactory (e.g. GroovyProxyFactory) so proxies are created correctly
- def proxyFactory = datastore.getMappingContext().getProxyFactory()
- return (D) proxyFactory.createProxy(datastore.currentSession, (Class) persistentClass, id)
- } else {
- return null
+ org.grails.datastore.gorm.DatastoreResolver resolver = new org.grails.datastore.gorm.DatastoreResolver() {
+ @Override Datastore resolve() { org.grails.datastore.gorm.GormRegistry.instance.apiResolver.findDatastore(persistentClass, qualifier) }
}
+ // Create new finders with the qualifier-specific resolver so dynamic finders (e.g. findByName)
+ // execute against the correct (non-DEFAULT) datasource session factory.
+ List<org.grails.datastore.gorm.finders.FinderMethod> qualifiedFinders =
+ registry.createDynamicFinders(resolver, ds.mappingContext)
+ HibernateGormStaticApi<D> newApi = new HibernateGormStaticApi<D>(persistentClass, ds.mappingContext, qualifiedFinders, resolver, qualifier, classLoader)
+ return newApi
}
@Override
- List<D> getAll() {
- doListInternal("from ${persistentEntity.name}".toString(), [:], [], [:], false)
- }
-
- @Override
- Integer count() {
- String entity = persistentEntity.name
- doSingleInternal("select count(*) from $entity" as String, [:], [], [:], false) as Integer
+ def <T> T withSession(Closure<T> callable) {
+ getHibernateDatastore().withSession { session ->
+ callable.call(session)
+ }
}
@Override
- boolean exists(Serializable id) {
- def converted = convertIdentifier(id)
- if (converted == null) return false
- String entity = persistentEntity.name
- String idName = persistentEntity.identity.name
- (doSingleInternal("select count(*) from $entity where $idName = :id" as String, [id: converted], [], [:], false) as Long) > 0
+ def <T> T withNewSession(Closure<T> callable) {
+ getHibernateDatastore().withNewSession { session ->
+ callable.call(session)
+ }
}
@Override
- D first(Map m) {
- def list = list(m)
- list.isEmpty() ? null : list.first()
+ def <T1> T1 withDatastoreSession(Closure<T1> callable) {
+ getHibernateDatastore().withSession { session ->
+ callable.call(new HibernateSession(getHibernateDatastore(), getHibernateDatastore().getSessionFactory(), (org.hibernate.Session)session))
+ }
}
@Override
- D last(Map m) {
- def list = list(m)
- list.isEmpty() ? null : list.last()
+ def <T> T withNewSession(Serializable tenantId, Closure<T> callable) {
+ HibernateDatastore primaryDatastore = getHibernateDatastore().getPrimaryDatastore()
+ return (T) grails.gorm.multitenancy.Tenants.withId((org.grails.datastore.mapping.multitenancy.MultiTenantCapableDatastore)primaryDatastore, tenantId) { id, session ->
+ callable.call(session)
+ }
}
@Override
- D find(CharSequence query, Map namedParams, Map args) {
- doSingleInternal(query, namedParams, [], args, false)
+ List<D> list(Map params) {
+ PersistentEntity entity = getGormPersistentEntity()
+ HqlQueryContext ctx = HqlQueryContext.prepare(entity, null, null, null, params, new HashMap<>(), false, false)
+ GormQuery q = HibernateHqlQueryCreator.createHqlQuery(getHibernateDatastore(), getHibernateDatastore().getSessionFactory(), entity, ctx)
+ if (HqlListQueryBuilder.isPaged(params)) {
+ return (List<D>) new PagedResultList(q)
+ }
+ return (List<D>) q.list()
}
@Override
- D find(CharSequence query, Collection positionalParams, Map args) {
- doSingleInternal(query, [:], positionalParams, args, false)
+ List executeQuery(CharSequence query, Map params, Map args) {
+ PersistentEntity entity = getGormPersistentEntity()
+ HqlQueryContext ctx = HqlQueryContext.prepare(entity, query, params, null, args, new HashMap<>(), false, false)
+ return (List) HibernateHqlQueryCreator.createHqlQuery(getHibernateDatastore(), getHibernateDatastore().getSessionFactory(), entity, ctx).list()
}
@Override
@@ -303,6 +260,68 @@ class HibernateGormStaticApi<D> extends GormStaticApi<D> {
doInternalExecuteUpdate(query, [:], [], [:])
}
+ @Override
+ List executeQuery(CharSequence query, Map params) {
+ return executeQuery(query, params, params)
+ }
+
+ @Override
+ List executeQuery(CharSequence query, Collection params) {
+ return executeQuery(query, params, Collections.emptyMap())
+ }
+
+ @Override
+ List executeQuery(CharSequence query, Collection params, Map args) {
+ PersistentEntity entity = getGormPersistentEntity()
+ HqlQueryContext ctx = HqlQueryContext.prepare(entity, query, null, params, args, new HashMap<>(), false, false)
+ return (List) HibernateHqlQueryCreator.createHqlQuery(getHibernateDatastore(), getHibernateDatastore().getSessionFactory(), entity, ctx).list()
+ }
+
+ @Override
+ List executeQuery(CharSequence query, Object... params) {
+ return executeQuery(query, Arrays.asList(params))
+ }
+
+ @Override
+ grails.gorm.api.GormAllOperations<D> eachTenant(Closure callable) {
+ grails.gorm.multitenancy.Tenants.eachTenant((Class<? extends Datastore>)getDatastore().getClass()) { Serializable tenantId ->
+ withTenant(tenantId).withSession {
+ callable.call(tenantId)
+ }
+ }
+ return this
+ }
+
+ @Override
+ grails.gorm.api.GormAllOperations<D> withTenant(Serializable tenantId) {
+ HibernateDatastore hibernateDatastore = (HibernateDatastore) ((org.grails.datastore.mapping.multitenancy.MultiTenantCapableDatastore)getDatastore()).getDatastoreForTenantId(tenantId)
+ final org.grails.datastore.gorm.DatastoreResolver resolver = new org.grails.datastore.gorm.DatastoreResolver() {
+ @Override Datastore resolve() { hibernateDatastore }
+ }
+ return (grails.gorm.api.GormAllOperations<D>) new HibernateGormStaticApi<D>(persistentClass, hibernateDatastore, finders, resolver, tenantId.toString(), classLoader)
+ }
+
+ @Override
+ def <T1> T1 withTenant(Serializable tenantId, Closure<T1> callable) {
+ HibernateDatastore primaryDatastore = getHibernateDatastore().getPrimaryDatastore()
+ return (T1) grails.gorm.multitenancy.Tenants.withId((org.grails.datastore.mapping.multitenancy.MultiTenantCapableDatastore)primaryDatastore, tenantId) { id, session ->
+ if (callable.maximumNumberOfParameters == 2) {
+ callable.call(tenantId, session)
+ } else {
+ callable.call(session)
+ }
+ }
+ }
+
+ @Override
+ D find(CharSequence query, Map params, Map args) {
+ PersistentEntity entity = getGormPersistentEntity()
+ HqlQueryContext ctx = HqlQueryContext.prepare(entity, query, params, null, args, new HashMap<>(), false, false)
+ ctx.querySettings().put(DynamicFinder.ARGUMENT_MAX, 1)
+ List results = HibernateHqlQueryCreator.createHqlQuery(getHibernateDatastore(), getHibernateDatastore().getSessionFactory(), entity, ctx).list()
+ return results ? (D) results[0] : null
+ }
+
@Override
D find(CharSequence query) {
doSingleInternal(query, [:], [], [:], false)
@@ -310,22 +329,36 @@ class HibernateGormStaticApi<D> extends GormStaticApi<D> {
@Override
D find(CharSequence query, Map params) {
- doSingleInternal(query, params, [], params, false)
+ return find(query, params, Collections.emptyMap())
+ }
+
+ @Override
+ D find(CharSequence query, Collection params) {
+ return find(query, params, Collections.emptyMap())
+ }
+
+ @Override
+ D find(CharSequence query, Collection params, Map args) {
+ PersistentEntity entity = getGormPersistentEntity()
+ HqlQueryContext ctx = HqlQueryContext.prepare(entity, query, null, params, args, new HashMap<>(), false, false)
+ ctx.querySettings().put(DynamicFinder.ARGUMENT_MAX, 1)
+ List results = HibernateHqlQueryCreator.createHqlQuery(getHibernateDatastore(), getHibernateDatastore().getSessionFactory(), entity, ctx).list()
+ return results ? (D) results[0] : null
}
@Override
List<D> findAll(CharSequence query, Map params) {
- doListInternal(query, params, [], params, false)
+ return findAll(query, params, Collections.emptyMap())
}
@Override
- List executeQuery(CharSequence query, Map args) {
- doListInternal(query, args, [], args, false)
+ List<D> findAll(CharSequence query, Collection params) {
+ return findAll(query, params, Collections.emptyMap())
}
@Override
- Integer executeUpdate(CharSequence query, Map args) {
- doInternalExecuteUpdate(query, args, [], args)
+ List<D> findAll(CharSequence query, Object[] params) {
+ return findAll(query, Arrays.asList(params))
}
@Override
@@ -335,6 +368,58 @@ class HibernateGormStaticApi<D> extends GormStaticApi<D> {
}
@Override
+ @CompileDynamic
+ D read(Serializable id) {
+ if (id == null) return null
+ id = convertIdentifier(id)
+ if (id == null) return null
+ def template = getHibernateTemplate()
+ return (D) template.execute { org.hibernate.Session session ->
+ D entity = (D) session.get(persistentClass, id)
+ if (entity != null) {
+ session.setReadOnly(entity, true)
+ }
+ entity
+ }
+ }
+
+ @Override
+ @CompileDynamic
+ D proxy(Serializable id) {
+ if (id == null) return null
+ id = convertIdentifier(id)
+ if (id == null) return null
+ def proxyFactory = getHibernateDatastore().mappingContext.getProxyFactory()
+ if (proxyFactory instanceof GroovyProxyFactory) {
+ return execute({ org.grails.datastore.mapping.core.Session session ->
+ session.proxy(persistentClass, id)
+ } as SessionCallback<D>)
+ }
+ return (D) getHibernateTemplate().load(persistentClass, id)
+ }
+
+ @Override
+ D load(Serializable id) {
+ if (id == null) return null
+ id = convertIdentifier(id)
+ if (id == null) return null
+ return (D) getHibernateTemplate().load(persistentClass, id)
+ }
+
+ @Override
+ @CompileDynamic
+ D last(Map params) {
+ Map p = new LinkedHashMap(params ?: [:])
+ if (!p.containsKey(DynamicFinder.ARGUMENT_ORDER)) {
+ p.put(DynamicFinder.ARGUMENT_ORDER, 'desc')
+ }
+ p.put(DynamicFinder.ARGUMENT_MAX, 1)
+ List<D> results = list(p)
+ results ? results.get(0) : null
+ }
+
+ @Override
+ @CompileDynamic
List<D> findAllWhere(Map queryMap, Map args) {
if (!queryMap) return null
executeListHqlQuery(prepareWhereHqlQuery(queryMap, buildQuerySettings(args)))
@@ -353,17 +438,19 @@ class HibernateGormStaticApi<D> extends GormStaticApi<D> {
}
private String buildWhereHql(Map<String, Object> queryMap) {
+ PersistentEntity pe = gormPersistentEntity
String whereClause = queryMap.collect { String key, Object value ->
String propertyName = validateWherePropertyName(key)
value == null ? "$propertyName is null" : "$propertyName = :$propertyName"
}.join(' and ')
- return "from ${persistentEntity.name} where $whereClause"
+ return "from ${pe.name} where $whereClause"
}
private String validateWherePropertyName(String propertyName) {
- PersistentProperty property = persistentEntity.getPropertyByName(propertyName)
+ PersistentEntity pe = gormPersistentEntity
+ PersistentProperty property = pe.getPropertyByName(propertyName)
if (property == null || property.name != propertyName) {
- throw new IllegalArgumentException("Property [$propertyName] is not a valid property of ${persistentEntity.name}")
+ throw new IllegalArgumentException("Property [$propertyName] is not a valid property of ${pe.name}")
}
return propertyName
}
@@ -384,14 +471,34 @@ class HibernateGormStaticApi<D> extends GormStaticApi<D> {
return queryArgs
}
+ @CompileDynamic
+ protected Serializable convertIdentifier(Serializable id) {
+ try {
+ PersistentEntity pe = getGormPersistentEntity()
+ PersistentProperty identity = pe.getIdentity()
+ Class identityType = identity != null ? identity.type : id.getClass()
+ if (!identityType.isInstance(id)) {
+ ConversionService conversionService = pe.mappingContext.conversionService
+ if (conversionService.canConvert(id.class, identityType)) {
+ return (Serializable) conversionService.convert(id, identityType)
+ }
+ return null
+ }
+ return id
+ }
+ catch (Throwable e) {
+ return null
+ }
+ }
+
@Override
- List executeQuery(CharSequence query, Map namedParams, Map args) {
- doListInternal(query, namedParams, [], args, false)
+ Integer executeUpdate(CharSequence query, Map params) {
+ return executeUpdate(query, params, Collections.emptyMap())
}
@Override
- List executeQuery(CharSequence query, Collection positionalParams, Map args) {
- return doListInternal(query, [:], positionalParams, args, false)
+ Integer executeUpdate(CharSequence query, Collection params) {
+ return executeUpdate(query, params, Collections.emptyMap())
}
@Override
@@ -401,9 +508,11 @@ class HibernateGormStaticApi<D> extends GormStaticApi<D> {
private List<D> getAllInternal(List ids) {
if (!ids) return []
+ PersistentEntity persistentEntity = gormPersistentEntity
String idName = persistentEntity.identity.name
String entity = persistentEntity.name
Class<?> idType = persistentEntity.identity.type
+ ConversionService conversionService = persistentEntity.mappingContext.conversionService
List convertedIds = ids.collect { HibernateRuntimeUtils.convertValueToType(it, idType, conversionService) }
List<D> results = doListInternal("from $entity where $idName in (:ids)" as String, [ids: convertedIds], [], [:], false)
Map<Object, D> byId = results.collectEntries { [(it[idName]): it] }
@@ -416,10 +525,10 @@ class HibernateGormStaticApi<D> extends GormStaticApi<D> {
}
protected List<D> doListInternal(CharSequence hql,
- Map namedParams,
- Collection positionalParams,
- Map args
- , boolean isNative) {
+ Map namedParams,
+ Collection positionalParams,
+ Map args,
+ boolean isNative) {
GormQuery hqlQuery = prepareHqlQuery(hql, isNative, false, namedParams, positionalParams, args)
executeListHqlQuery(hqlQuery)
}
@@ -435,8 +544,8 @@ class HibernateGormStaticApi<D> extends GormStaticApi<D> {
private D doSingleInternal(CharSequence hql,
Map namedParams,
Collection positionalParams,
- Map args, Map hints = [:], boolean isNative
- ) {
+ Map args,
+ boolean isNative) {
GormQuery hqlQuery = prepareHqlQuery(hql, isNative, false, namedParams, positionalParams, args)
executeSingleHqlQuery(hqlQuery)
}
@@ -470,94 +579,163 @@ class HibernateGormStaticApi<D> extends GormStaticApi<D> {
return (Integer) execute
}
- @SuppressWarnings('GroovyAssignabilityCheck')
- protected GormQuery prepareHqlQuery(CharSequence hql
- , boolean isNative
- , boolean isUpdate
- , Map<String, Object> namedParams
- , Collection<Object> positionalParams
- , Map<String, Object> querySettings
- , Map<String, Object> hints = [:]) {
- if (hints.isEmpty() && querySettings != null) {
- hints = querySettings.findAll { AvailableHints.getDefinedHints().contains(it.key) }
- }
- Map<String, Object> coercedParams = namedParams?.collectEntries { k, v -> [k.toString(), v] } ?: [:]
- def ctx = HqlQueryContext.prepare(persistentEntity, hql, coercedParams, positionalParams, querySettings, hints, isNative, isUpdate)
- return HibernateHqlQueryCreator.createHqlQuery(
- (HibernateDatastore) datastore,
- sessionFactory,
- persistentEntity,
- ctx
- )
+ @Override
+ @CompileDynamic
+ List<D> findAll(D example, Map args) {
+ execute({ Session session ->
+ def query = session.createQuery(persistentClass)
+ populateQueryByExample(session, query, example)
+ if (query.allCriteria.isEmpty()) {
+ return null
+ }
+ Integer max = ClassUtils.getIntegerFromMap(DynamicFinder.ARGUMENT_MAX, args)
+ Integer offset = ClassUtils.getIntegerFromMap(DynamicFinder.ARGUMENT_OFFSET, args)
+ if (max != null) {
+ query.max(max.intValue())
+ }
+ if (offset != null) {
+ query.offset(offset.intValue())
+ }
+ query.list()
+ } as SessionCallback<List<D>>)
}
- protected Serializable convertIdentifier(Serializable id) {
- def identity = persistentEntity.identity
- if (identity != null) {
- ConversionService conversionService = persistentEntity.mappingContext.conversionService
- if (id != null) {
- Class identityType = identity.type
- Class idInstanceType = id.getClass()
- if (identityType.isAssignableFrom(idInstanceType)) {
- return id
- } else if (conversionService.canConvert(idInstanceType, identityType)) {
- try {
- return (Serializable) conversionService.convert(id, identityType)
- }
- catch (Throwable ignored) {
- return null
+ @Override
+ @CompileDynamic
+ D find(D example, Map args) {
+ execute({ Session session ->
+ def query = session.createQuery(persistentClass)
+ populateQueryByExample(session, query, example)
+ if (query.allCriteria.isEmpty()) {
+ return null
+ }
+ query.singleResult()
+ } as SessionCallback<D>)
+ }
+
+ protected void populateQueryByExample(Session session, GormQuery query, D example) {
+ PersistentEntity pe = getGormPersistentEntity()
+ MappingContext mappingContext = pe.mappingContext
+ def ea = mappingContext.createEntityAccess(pe, example)
+ def id = ea.getIdentifier()
+ if (id != null) {
+ query.add(Restrictions.eq(pe.identity.name, id))
+ }
+ else {
+ for (prop in pe.persistentProperties) {
+ if (prop.name == GormProperties.VERSION) {
+ continue
+ }
+ if (prop instanceof Simple || prop instanceof Basic) {
+ def val = ea.getProperty(prop.name)
+ if (val != null) {
+ query.add(Restrictions.eq(prop.name, val))
}
- } else {
- return null
}
}
}
- return id
}
- @Override
- List<D> list(Map params = Collections.emptyMap()) {
- firePreQueryEvent()
- HqlListQueryBuilder builder = new HqlListQueryBuilder((GrailsHibernatePersistentEntity) persistentEntity, params)
- String hql = builder.buildListHql()
- HqlQueryContext ctx = HqlQueryContext.prepare(persistentEntity, hql, Collections.emptyMap(), Collections.emptyList(), params, new HashMap<String, Object>(), false, false)
- GormQuery hqlQuery = HibernateHqlQueryCreator.createHqlQuery(
- (HibernateDatastore) datastore,
- sessionFactory,
- persistentEntity,
- ctx
- )
- if (params.containsKey('max')) {
- return new HibernatePagedResultList(getHibernateTemplate(), persistentEntity, hqlQuery)
+ protected void populateQueryArguments(org.hibernate.query.Query q, Map args) {
+ if (args == null || args.isEmpty()) return
+
+ Map argsToUse = new HashMap(args)
+ Integer max = intValue(argsToUse, DynamicFinder.ARGUMENT_MAX)
+ if (max != null) {
+ q.setMaxResults(max)
+ }
+ Integer offset = intValue(argsToUse, DynamicFinder.ARGUMENT_OFFSET)
+ if (offset != null) {
+ q.setFirstResult(offset)
+ }
+
+ if (argsToUse.containsKey(DynamicFinder.ARGUMENT_CACHE)) {
+ q.setCacheable(org.grails.datastore.mapping.reflect.ClassUtils.getBooleanFromMap(DynamicFinder.ARGUMENT_CACHE, argsToUse))
+ }
+ if (argsToUse.containsKey(DynamicFinder.ARGUMENT_FETCH_SIZE)) {
+ Object fetchSize = argsToUse.remove(DynamicFinder.ARGUMENT_FETCH_SIZE)
+ if (fetchSize instanceof Number) {
+ q.setFetchSize(((Number) fetchSize).intValue())
+ }
+ }
+ if (argsToUse.containsKey(DynamicFinder.ARGUMENT_TIMEOUT)) {
+ Object timeout = argsToUse.remove(DynamicFinder.ARGUMENT_TIMEOUT)
+ if (timeout instanceof Number) {
+ q.setTimeout(((Number) timeout).intValue())
+ }
+ }
+ if (argsToUse.containsKey(DynamicFinder.ARGUMENT_READ_ONLY)) {
+ q.setReadOnly((Boolean) argsToUse.remove(DynamicFinder.ARGUMENT_READ_ONLY))
+ }
+ if (argsToUse.containsKey(DynamicFinder.ARGUMENT_FLUSH_MODE)) {
+ Object flushMode = argsToUse.remove(DynamicFinder.ARGUMENT_FLUSH_MODE)
+ if (flushMode instanceof FlushMode) {
+ q.setHibernateFlushMode((FlushMode) flushMode)
+ } else if (flushMode instanceof String) {
+ q.setHibernateFlushMode(FlushMode.valueOf(flushMode.toString().toUpperCase()))
+ }
}
- List<D> result = (List<D>) hqlQuery.list()
- firePostQueryEvent(result)
- result
}
- @Override
- def propertyMissing(String name) {
- if (datastore instanceof ConnectionSourcesProvider) {
- return HibernateGormEnhancer.findStaticApi(persistentClass, name)
- } else {
- throw new MissingPropertyException(name, persistentClass)
+ protected Integer intValue(Map args, String name) {
+ Object val = args.get(name)
+ if (val instanceof Number) {
+ return ((Number) val).intValue()
+ } else if (val != null) {
+ try {
+ return Integer.valueOf(val.toString())
+ } catch (NumberFormatException e) {
+ return null
+ }
+ }
+ return null
+ }
+
+ protected String buildOrdinalParameterQueryFromGString(GString query, List params) {
+ StringBuilder sqlString = new StringBuilder()
+ int i = 0
+ Object[] values = query.values
+ String[] strings = query.getStrings()
+ for (String str in strings) {
+ sqlString.append(str)
+ if (i < values.length) {
+ sqlString.append('?')
+ params.add(values[i++])
+ }
}
+ return sqlString.toString()
}
- @Override
- GrailsCriteria createCriteria() {
- return new HibernateCriteriaBuilder(persistentClass, sessionFactory, (HibernateDatastore) datastore)
+ @SuppressWarnings('GroovyAssignabilityCheck')
+ protected GormQuery prepareHqlQuery(CharSequence hql,
+ boolean isNative,
+ boolean isUpdate,
+ Map namedParams,
+ Collection positionalParams,
+ Map args) {
+ PersistentEntity persistentEntity = gormPersistentEntity
+ Map<String, Object> coercedParams = namedParams?.collectEntries { k, v -> [k.toString(), v] } ?: [:]
+ Map<String, Object> coercedArgs = args?.collectEntries { k, v -> [k.toString(), v] } ?: [:]
+ def ctx = HqlQueryContext.prepare(persistentEntity, hql, coercedParams, positionalParams, coercedArgs, new HashMap<>(), isNative, isUpdate)
+ return HibernateHqlQueryCreator.createHqlQuery(
+ getHibernateDatastore(),
+ getHibernateDatastore().getSessionFactory(),
+ persistentEntity,
+ ctx
+ )
}
protected void firePostQueryEvent(Object result) {
- def hibernateQuery = new HibernateQuery(new HibernateSession((HibernateDatastore) datastore, sessionFactory), (GrailsHibernatePersistentEntity) persistentEntity)
+ def hibernateSession = new HibernateSession(getHibernateDatastore(), getHibernateDatastore().getSessionFactory())
+ def hibernateQuery = new HibernateQuery(hibernateSession, (GrailsHibernatePersistentEntity) gormPersistentEntity)
def list = result instanceof List ? (List) result : Collections.singletonList(result)
- datastore.applicationEventPublisher.publishEvent(new PostQueryEvent(datastore, hibernateQuery, list))
+ getHibernateDatastore().applicationEventPublisher.publishEvent(new PostQueryEvent(getHibernateDatastore(), hibernateQuery, list))
}
protected void firePreQueryEvent() {
- def hibernateSession = new HibernateSession((HibernateDatastore) datastore, sessionFactory)
- def hibernateQuery = new HibernateQuery(hibernateSession, (GrailsHibernatePersistentEntity) persistentEntity)
- datastore.applicationEventPublisher.publishEvent(new PreQueryEvent(datastore, hibernateQuery))
+ def hibernateSession = new HibernateSession(getHibernateDatastore(), getHibernateDatastore().getSessionFactory())
+ def hibernateQuery = new HibernateQuery(hibernateSession, (GrailsHibernatePersistentEntity) gormPersistentEntity)
+ getHibernateDatastore().applicationEventPublisher.publishEvent(new PreQueryEvent(getHibernateDatastore(), hibernateQuery))
}
+
}
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormValidationApi.groovy b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormValidationApi.groovy
index b34cf0c9b7..302911616c 100644
--- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormValidationApi.groovy
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormValidationApi.groovy
@@ -49,6 +49,9 @@ import org.grails.datastore.mapping.engine.event.ValidationEvent
import org.grails.datastore.mapping.reflect.ClassUtils
import org.grails.datastore.mapping.validation.ValidationErrors
import org.grails.orm.hibernate.support.HibernateRuntimeUtils
+import org.grails.datastore.mapping.core.Datastore
+import org.grails.datastore.mapping.model.MappingContext
+import org.grails.datastore.gorm.DatastoreResolver
@CompileStatic
class HibernateGormValidationApi<D> extends GormValidationApi<D> {
@@ -56,15 +59,40 @@ class HibernateGormValidationApi<D> extends GormValidationApi<D> {
public static final String ARGUMENT_DEEP_VALIDATE = 'deepValidate'
private static final String ARGUMENT_EVICT = 'evict'
- protected ClassLoader classLoader
- protected HibernateDatastore datastore
+ protected final ClassLoader classLoader
protected IHibernateTemplate hibernateTemplate
HibernateGormValidationApi(Class<D> persistentClass, HibernateDatastore datastore, ClassLoader classLoader) {
super(persistentClass, datastore)
this.classLoader = classLoader
- this.datastore = datastore
- hibernateTemplate = (IHibernateTemplate) datastore.getHibernateTemplate()
+ this.hibernateTemplate = (IHibernateTemplate) datastore.getHibernateTemplate()
+ }
+
+ HibernateGormValidationApi(Class<D> persistentClass, MappingContext mappingContext, DatastoreResolver datastoreResolver, ClassLoader classLoader) {
+ super(persistentClass, mappingContext, datastoreResolver)
+ this.classLoader = classLoader
+ }
+
+ @Override
+ GormValidationApi<D> forQualifier(String qualifier) {
+ Datastore ds = getDatastore()
+ if (ds == null) return this
+
+ org.grails.datastore.gorm.DatastoreResolver resolver = new org.grails.datastore.gorm.DatastoreResolver() {
+ @Override Datastore resolve() { org.grails.datastore.gorm.GormRegistry.instance.apiResolver.findDatastore(persistentClass, qualifier) }
+ }
+ return new HibernateGormValidationApi<D>(persistentClass, ds.mappingContext, resolver, classLoader)
+ }
+
+ protected HibernateDatastore getHibernateDatastore() {
+ (HibernateDatastore) getDatastore()
+ }
+
+ protected IHibernateTemplate getHibernateTemplate() {
+ if (this.hibernateTemplate == null) {
+ return (IHibernateTemplate) getHibernateDatastore().getHibernateTemplate()
+ }
+ return hibernateTemplate
}
@Override
@@ -96,14 +124,14 @@ class HibernateGormValidationApi<D> extends GormValidationApi<D> {
fireEvent(instance, validatedFieldsList)
- hibernateTemplate.execute { Session session ->
+ getHibernateTemplate().execute { Session session ->
FlushMode previous = session.getHibernateFlushMode()
session.setHibernateFlushMode(FlushMode.MANUAL)
try {
if (validator instanceof CascadingValidator) {
((CascadingValidator) validator).validate instance, errors, deepValidate
- } else if (validator instanceof grails.gorm.validation.CascadingValidator) {
- ((grails.gorm.validation.CascadingValidator) validator).validate instance, errors, deepValidate
+ } else if (validator instanceof org.grails.datastore.gorm.validation.CascadingValidator) {
+ ((org.grails.datastore.gorm.validation.CascadingValidator) validator).validate instance, errors, deepValidate
} else {
validator.validate instance, errors
}
@@ -120,8 +148,8 @@ class HibernateGormValidationApi<D> extends GormValidationApi<D> {
if (errors.hasErrors()) {
valid = false
if (evict) {
- if (hibernateTemplate.contains(instance)) {
- hibernateTemplate.evict(instance)
+ if (getHibernateTemplate().contains(instance)) {
+ getHibernateTemplate().evict(instance)
}
}
}
@@ -134,9 +162,9 @@ class HibernateGormValidationApi<D> extends GormValidationApi<D> {
}
private void fireEvent(Object target, List<?> validatedFieldsList) {
- ValidationEvent event = new ValidationEvent(datastore, target)
+ ValidationEvent event = new ValidationEvent(getHibernateDatastore(), target)
event.setValidatedFields(validatedFieldsList)
- datastore.getApplicationEventPublisher().publishEvent(event)
+ getHibernateDatastore().getApplicationEventPublisher().publishEvent(event)
}
@SuppressWarnings('rawtypes')
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateSession.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateSession.java
index 08b45e0836..99258d9b51 100644
--- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateSession.java
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateSession.java
@@ -24,6 +24,7 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -37,14 +38,18 @@ import org.hibernate.proxy.HibernateProxy;
import org.hibernate.query.MutationQuery;
import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.core.convert.ConversionService;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.support.TransactionSynchronizationManager;
+import org.grails.datastore.gorm.proxy.GroovyProxyFactory;
import org.grails.datastore.gorm.timestamp.DefaultTimestampProvider;
import org.grails.datastore.mapping.core.AbstractAttributeStoringSession;
import org.grails.datastore.mapping.core.Datastore;
+import org.grails.datastore.mapping.core.connections.ConnectionSource;
import org.grails.datastore.mapping.engine.Persister;
import org.grails.datastore.mapping.model.MappingContext;
+import org.grails.datastore.mapping.model.PersistentEntity;
import org.grails.datastore.mapping.model.PersistentProperty;
import org.grails.datastore.mapping.model.config.GormProperties;
import org.grails.datastore.mapping.proxy.ProxyHandler;
@@ -60,6 +65,7 @@ import org.grails.datastore.mapping.transactions.Transaction;
import org.grails.orm.hibernate.cfg.domainbinding.hibernate.GrailsHibernatePersistentEntity;
import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernatePersistentEntity;
import org.grails.orm.hibernate.proxy.HibernateProxyHandler;
+import org.grails.orm.hibernate.query.HibernateHqlQuery;
import org.grails.orm.hibernate.query.HibernateHqlQueryCreator;
import org.grails.orm.hibernate.query.HibernateQuery;
import org.grails.orm.hibernate.query.HqlQueryContext;
@@ -84,12 +90,19 @@ public class HibernateSession extends AbstractAttributeStoringSession implements
/** The hibernate template. */
protected IHibernateTemplate hibernateTemplate;
+ protected Session nativeSession;
+
ProxyHandler proxyHandler = new HibernateProxyHandler();
DefaultTimestampProvider timestampProvider;
public HibernateSession(HibernateDatastore hibernateDatastore, SessionFactory sessionFactory) {
+ this(hibernateDatastore, sessionFactory, null);
+ }
+
+ public HibernateSession(HibernateDatastore hibernateDatastore, SessionFactory sessionFactory, Session nativeSession) {
datastore = hibernateDatastore;
hibernateTemplate = (IHibernateTemplate) hibernateDatastore.getHibernateTemplate();
+ this.nativeSession = nativeSession;
}
@Override
@@ -103,13 +116,16 @@ public class HibernateSession extends AbstractAttributeStoringSession implements
}
@Override
- public boolean isConnected() {
- return connected;
+ public void disconnect() {
+ connected = false;
+ if (nativeSession != null && nativeSession.isOpen()) {
+ nativeSession.close();
+ }
}
@Override
- public void disconnect() {
- connected = false; // don't actually do any disconnection here. This will be handled by OSVI
+ public boolean isConnected() {
+ return connected;
}
@Override
@@ -209,11 +225,42 @@ public class HibernateSession extends AbstractAttributeStoringSession implements
@Override
public <T> T retrieve(Class<T> type, Serializable key) {
- return getHibernateTemplate().execute(session -> session.find(type, key));
+ if (key == null) {
+ return null;
+ }
+ PersistentEntity entity = getMappingContext().getPersistentEntity(type.getName());
+ if (entity != null) {
+ PersistentProperty identity = entity.getIdentity();
+ if (identity != null && !identity.getType().isAssignableFrom(key.getClass())) {
+ ConversionService conversionService = getMappingContext().getConversionService();
+ if (conversionService.canConvert(key.getClass(), identity.getType())) {
+ try {
+ key = (Serializable) conversionService.convert(key, identity.getType());
+ } catch (Exception ignored) {
+ return null;
+ }
+ }
+ }
+ }
+ final Serializable finalKey = key;
+ return getHibernateTemplate().execute(session -> {
+ try {
+ return session.find(type, finalKey);
+ } catch (IllegalArgumentException e) {
+ return null;
+ }
+ });
}
@Override
public <T> T proxy(Class<T> type, Serializable key) {
+ if (key == null) {
+ return null;
+ }
+ var proxyFactory = getMappingContext().getProxyFactory();
+ if (proxyFactory instanceof GroovyProxyFactory groovyProxyFactory) {
+ return groovyProxyFactory.createProxy(this, type, key);
+ }
return hibernateTemplate.load(type, key);
}
@@ -281,6 +328,13 @@ public class HibernateSession extends AbstractAttributeStoringSession implements
return hibernateTemplate;
}
+ public Session getNativeSession() {
+ if (nativeSession != null) {
+ return nativeSession;
+ }
+ return hibernateTemplate.getSessionFactory().getCurrentSession();
+ }
+
@Override
public void setSynchronizedWithTransaction(boolean synchronizedWithTransaction) {
// no-op
@@ -386,21 +440,43 @@ public class HibernateSession extends AbstractAttributeStoringSession implements
});
}
- @Override
- @SuppressWarnings({"PMD.DataflowAnomalyAnalysis"})
- //TODO Cleanup
+ private static Object coerceId(Object key, Class<?> idType) {
+ if (key == null || idType == null || idType.isInstance(key)) {
+ return key;
+ }
+ if (key instanceof String s) {
+ if (idType == Long.class) return Long.parseLong(s);
+ if (idType == Integer.class) return Integer.parseInt(s);
+ }
+ return key;
+ }
+
public List retrieveAll(final Class type, final Iterable keys) {
final GrailsHibernatePersistentEntity persistentEntity = (GrailsHibernatePersistentEntity) getMappingContext().getPersistentEntity(type.getName());
final String entityName = persistentEntity.getName();
final String idName = persistentEntity.getIdentity().getName();
+ final Class<?> idType = persistentEntity.getIdentity().getType();
+
+ // Collect input keys preserving order, coercing to the entity's ID type
+ List<Object> inputKeys = new ArrayList<>();
+ for (Object k : keys) {
+ inputKeys.add(coerceId(k, idType));
+ }
+ if (inputKeys.isEmpty()) {
+ return Collections.emptyList();
+ }
+ // Determine the unique set of keys for the HQL IN query
+ Collection<Object> uniqueKeys = new LinkedHashMap<Object, Object>() {{
+ for (Object k : inputKeys) { put(k, k); }
+ }}.keySet();
+
final String hql = "from " + entityName + " as e where e." + idName + " in (:keys)";
- return getHibernateTemplate().execute(session -> {
- // Prepare the HqlQueryContext using our manual HQL string and type override
+ Map<Object, Object> entityById = getHibernateTemplate().execute(session -> {
HqlQueryContext queryContext = HqlQueryContext.prepare(
persistentEntity,
hql,
- Map.of("keys", getIterableAsCollection(keys)),
+ Map.of("keys", uniqueKeys),
null,
null,
new HashMap<>(),
@@ -409,13 +485,45 @@ public class HibernateSession extends AbstractAttributeStoringSession implements
type
);
- return HibernateHqlQueryCreator.createHqlQuery(
+ List fetched = HibernateHqlQueryCreator.createHqlQuery(
(HibernateDatastore) getDatastore(),
getHibernateTemplate().getSessionFactory(),
persistentEntity,
queryContext
).list();
+
+ Map<Object, Object> byId = new LinkedHashMap<>();
+ org.hibernate.Session nativeSession = session;
+ for (Object entity : fetched) {
+ Object id = nativeSession.getIdentifier(entity);
+ byId.put(id, entity);
+ }
+ return byId;
});
+
+ // Build result list in input order, with null for missing IDs
+ List<Object> result = new ArrayList<>(inputKeys.size());
+ for (Object k : inputKeys) {
+ result.add(entityById.get(k));
+ }
+ return result;
+ }
+
+ public Query createQuery(String queryString) {
+ return createQuery(queryString, null);
+ }
+
+ public Query createQuery(String queryString, Class resultType) {
+ String trimmed = queryString.trim().toLowerCase(java.util.Locale.ENGLISH);
+ if (trimmed.startsWith("delete") || trimmed.startsWith("update")) {
+ org.hibernate.query.MutationQuery q = getNativeSession().createMutationQuery(queryString);
+ return new HibernateHqlQuery(this, null, q);
+ } else {
+ org.hibernate.query.Query q = resultType != null ?
+ getNativeSession().createQuery(queryString, resultType) :
+ getNativeSession().createQuery(queryString);
+ return new HibernateHqlQuery(this, null, q);
+ }
}
@Override
@@ -455,13 +563,16 @@ public class HibernateSession extends AbstractAttributeStoringSession implements
//TODO could be used
protected <D> HibernateGormStaticApi<D> getStaticApi(Class<D> type) {
- return new HibernateGormStaticApi<>(
+ HibernateDatastore datastore = (HibernateDatastore) getDatastore();
+ return new HibernateGormStaticApi<D>(
type,
- (HibernateDatastore) getDatastore(),
+ datastore,
Collections.emptyList(),
- Thread.currentThread().getContextClassLoader(),
- ((HibernateDatastore) getDatastore()).getTransactionManager(),
- null
+ new org.grails.datastore.gorm.DatastoreResolver() {
+ @Override public Datastore resolve() { return getDatastore(); }
+ },
+ ConnectionSource.DEFAULT,
+ ((HibernateDatastore) getDatastore()).getMappingContext().getMappingFactory().getClass().getClassLoader()
);
}
}
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateSessionResolver.groovy b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateSessionResolver.groovy
new file mode 100644
index 0000000000..9fe61bf3b2
--- /dev/null
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateSessionResolver.groovy
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.grails.orm.hibernate
+
+import groovy.transform.CompileStatic
+
+import org.hibernate.SessionFactory
+
+import org.springframework.transaction.support.TransactionSynchronizationManager
+
+import org.grails.datastore.mapping.core.Session
+import org.grails.datastore.mapping.core.SessionResolver
+import org.grails.orm.hibernate.support.hibernate7.SessionHolder
+
+/**
+ * Hibernate 7 specific SessionResolver
+ *
+ * @author borinquenkid
+ * @since 8.0
+ */
+@CompileStatic
+class HibernateSessionResolver implements SessionResolver<Session> {
+
+ private final SessionFactory sessionFactory
+ private final HibernateDatastore datastore
+
+ HibernateSessionResolver(HibernateDatastore datastore, SessionFactory sessionFactory) {
+ this.datastore = datastore
+ this.sessionFactory = sessionFactory
+ }
+
+ @Override
+ Session resolve() {
+ // 1. Try to find a GORM session bound to the datastore
+ Object resource = TransactionSynchronizationManager.getResource(datastore)
+ if (resource instanceof org.grails.datastore.mapping.transactions.SessionHolder) {
+ return ((org.grails.datastore.mapping.transactions.SessionHolder) resource).getSession()
+ }
+
+ // 2. Fallback to native Hibernate session bound to the datastore (legacy Grails binding)
+ if (resource instanceof SessionHolder) {
+ return new HibernateSession(datastore, sessionFactory)
+ }
+
+ // 3. Fallback to native Hibernate session bound to the session factory
+ resource = TransactionSynchronizationManager.getResource(sessionFactory)
+ if (resource instanceof SessionHolder) {
+ return new HibernateSession(datastore, sessionFactory)
+ }
+
+ return null
+ }
+
+ @Override
+ Session resolve(String qualifier) {
+ // Implementation for multi-datasource routing
+ return datastore.getDatastoreForConnection(qualifier).getSessionResolver().resolve()
+ }
+
+ @Override
+ void bind(Session session) {
+ if (session instanceof HibernateSession) {
+ TransactionSynchronizationManager.bindResource(sessionFactory, new SessionHolder(((HibernateSession) session).getNativeSession()))
+ }
+ }
+
+ @Override
+ void unbind() {
+ TransactionSynchronizationManager.unbindResource(sessionFactory)
+ }
+}
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/SchemaTenantGormEnhancer.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/SchemaTenantGormEnhancer.java
index c5e11ba047..8a2913ad2e 100644
--- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/SchemaTenantGormEnhancer.java
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/SchemaTenantGormEnhancer.java
@@ -42,20 +42,18 @@ public class SchemaTenantGormEnhancer extends HibernateGormEnhancer {
private final HibernateConnectionSource defaultConnectionSource;
private final TenantResolver tenantResolver;
private final SchemaHandler schemaHandler;
- private final Map<String, HibernateDatastore> datastoresByConnectionSource;
public SchemaTenantGormEnhancer(
- Datastore datastore,
+ HibernateDatastore datastore,
PlatformTransactionManager transactionManager,
HibernateConnectionSource defaultConnectionSource,
TenantResolver tenantResolver,
SchemaHandler schemaHandler,
Map<String, HibernateDatastore> datastoresByConnectionSource) {
- super(datastore, transactionManager, defaultConnectionSource.getSettings());
+ super(datastore, transactionManager, defaultConnectionSource.getSettings(), datastoresByConnectionSource);
this.defaultConnectionSource = defaultConnectionSource;
this.tenantResolver = tenantResolver;
this.schemaHandler = schemaHandler;
- this.datastoresByConnectionSource = datastoresByConnectionSource;
// super() calls registerEntity → allQualifiers before our fields are set.
// Re-register now that all fields are initialized so schema qualifiers are wired correctly.
for (PersistentEntity entity : datastore.getMappingContext().getPersistentEntities()) {
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/TenantBoundHibernateTemplate.groovy b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/TenantBoundHibernateTemplate.groovy
new file mode 100644
index 0000000000..7512d2c43a
--- /dev/null
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/TenantBoundHibernateTemplate.groovy
@@ -0,0 +1,170 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * 'License'); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * 'AS IS' BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.grails.orm.hibernate
+
+import groovy.transform.CompileStatic
+import org.hibernate.LockMode
+import org.hibernate.SessionFactory
+import org.hibernate.query.Query
+import org.grails.datastore.mapping.multitenancy.MultiTenantCapableDatastore
+import grails.gorm.multitenancy.Tenants
+
+/**
+ * A {@link IHibernateTemplate} implementation that binds a tenant id for the duration of the execution
+ *
+ * @author Graeme Rocher
+ * @since 6.0
+ */
+@CompileStatic
+class TenantBoundHibernateTemplate implements IHibernateTemplate {
+
+ private final IHibernateTemplate delegate
+ private final Serializable tenantId
+ private final MultiTenantCapableDatastore datastore
+
+ TenantBoundHibernateTemplate(IHibernateTemplate delegate, Serializable tenantId, MultiTenantCapableDatastore datastore) {
+ this.delegate = delegate
+ this.tenantId = tenantId
+ this.datastore = datastore
+ }
+
+ @Override
+ void persist(Object o) {
+ Tenants.withId(datastore, tenantId) {
+ delegate.persist(o)
+ }
+ }
+
+ @Override
+ Object merge(Object o) {
+ return Tenants.withId(datastore, tenantId) {
+ delegate.merge(o)
+ }
+ }
+
+ @Override
+ void refresh(Object o) {
+ Tenants.withId(datastore, tenantId) {
+ delegate.refresh(o)
+ }
+ }
+
+ @Override
+ void lock(Object o, LockMode lockMode) {
+ Tenants.withId(datastore, tenantId) {
+ delegate.lock(o, lockMode)
+ }
+ }
+
+ @Override
+ void flush() {
+ delegate.flush()
+ }
+
+ @Override
+ void clear() {
+ delegate.clear()
+ }
+
+ @Override
+ void evict(Object o) {
+ delegate.evict(o)
+ }
+
+ @Override
+ boolean contains(Object o) {
+ delegate.contains(o)
+ }
+
+ @Override
+ int getFlushMode() {
+ delegate.getFlushMode()
+ }
+
+ @Override
+ void setFlushMode(int mode) {
+ delegate.setFlushMode(mode)
+ }
+
+ @Override
+ void deleteAll(Collection<?> list) {
+ Tenants.withId(datastore, tenantId) {
+ delegate.deleteAll(list)
+ }
+ }
+
+ @Override
+ void applySettings(Query<?> query) {
+ delegate.applySettings(query)
+ }
+
+ @Override
+ <T> T get(Class<T> type, Serializable key) {
+ return (T) Tenants.withId(datastore, tenantId) {
+ delegate.get(type, key)
+ }
+ }
+
+ @Override
+ <T> T get(Class<T> type, Serializable key, LockMode mode) {
+ return (T) Tenants.withId(datastore, tenantId) {
+ delegate.get(type, key, mode)
+ }
+ }
+
+ @Override
+ <T> T load(Class<T> type, Serializable key) {
+ return (T) Tenants.withId(datastore, tenantId) {
+ delegate.load(type, key)
+ }
+ }
+
+ @Override
+ void remove(Object o) {
+ Tenants.withId(datastore, tenantId) {
+ delegate.remove(o)
+ }
+ }
+
+ @Override
+ SessionFactory getSessionFactory() {
+ delegate.getSessionFactory()
+ }
+
+ @Override
+ <T> T execute(Closure<T> callable) {
+ return Tenants.withId(datastore, tenantId) {
+ delegate.execute(callable)
+ }
+ }
+
+ @Override
+ <T> T executeWithNewSession(Closure<T> callable) {
+ return Tenants.withId(datastore, tenantId) {
+ delegate.executeWithNewSession(callable)
+ }
+ }
+
+ @Override
+ <T1> T1 executeWithExistingOrCreateNewSession(SessionFactory sessionFactory, Closure<T1> callable) {
+ return Tenants.withId(datastore, tenantId) {
+ delegate.executeWithExistingOrCreateNewSession(sessionFactory, callable)
+ }
+ }
+}
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/GrailsHibernateUtil.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/GrailsHibernateUtil.java
index 785f108722..2dd73a5d88 100644
--- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/GrailsHibernateUtil.java
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/GrailsHibernateUtil.java
@@ -130,7 +130,12 @@ public class GrailsHibernateUtil extends HibernateRuntimeUtils {
}
private static boolean canModifyReadWriteState(Session session, Object target) {
- return session.contains(target) && Hibernate.isInitialized(target);
+ try {
+ return session.contains(target) && Hibernate.isInitialized(target);
+ } catch (IllegalArgumentException e) {
+ // Hibernate 7: session.contains() throws when the class is not a known entity type
+ return false;
+ }
}
/**
@@ -143,6 +148,9 @@ public class GrailsHibernateUtil extends HibernateRuntimeUtils {
*/
@SuppressWarnings({"PMD.CloseResource", "PMD.DataflowAnomalyAnalysis"})
public static void setObjectToReadWrite(final Object target, SessionFactory sessionFactory) {
+ if (target == null || sessionFactory == null) {
+ return;
+ }
Session session = sessionFactory.getCurrentSession();
if (!canModifyReadWriteState(session, target)) {
return;
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/HibernateMappingContext.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/HibernateMappingContext.java
index 485332ba19..93cdca039a 100644
--- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/HibernateMappingContext.java
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/HibernateMappingContext.java
@@ -26,7 +26,6 @@ import grails.gorm.hibernate.HibernateEntity;
import org.grails.datastore.gorm.GormEntity;
import org.grails.datastore.mapping.model.AbstractMappingContext;
import org.grails.datastore.mapping.model.MappingConfigurationStrategy;
-import org.grails.datastore.mapping.model.MappingFactory;
import org.grails.datastore.mapping.model.PersistentEntity;
import org.grails.orm.hibernate.cfg.domainbinding.hibernate.GrailsJpaMappingConfigurationStrategy;
import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateEmbeddedPersistentEntity;
@@ -81,7 +80,7 @@ public class HibernateMappingContext extends AbstractMappingContext {
}
@Override
- public MappingFactory<?, ?> getMappingFactory() {
+ public HibernateMappingFactory getMappingFactory() {
return mappingFactory;
}
@@ -125,6 +124,7 @@ public class HibernateMappingContext extends AbstractMappingContext {
return persistentEntities.stream()
.filter(HibernatePersistentEntity.class::isInstance)
.map(HibernatePersistentEntity.class::cast)
+ .filter(hibernateEntity -> hibernateEntity.usesConnectionSource(dataSourceName))
.peek(hibernateEntity -> hibernateEntity.setDataSourceName(dataSourceName))
.toList();
}
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/HibernateMappingContextConfiguration.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/HibernateMappingContextConfiguration.java
index 4429b96322..cfc11b0fcd 100644
--- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/HibernateMappingContextConfiguration.java
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/HibernateMappingContextConfiguration.java
@@ -78,6 +78,8 @@ import org.grails.orm.hibernate.GrailsSessionContext;
import org.grails.orm.hibernate.HibernateEventListeners;
import org.grails.orm.hibernate.MetadataIntegrator;
import org.grails.orm.hibernate.cfg.domainbinding.binder.GrailsDomainBinder;
+import org.grails.orm.hibernate.cfg.domainbinding.hibernate.GrailsHibernatePersistentEntity;
+import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernatePersistentEntity;
import org.grails.orm.hibernate.cfg.domainbinding.util.NamingStrategyProvider;
import org.grails.orm.hibernate.proxy.GrailsBytecodeProvider;
@@ -310,9 +312,9 @@ public class HibernateMappingContextConfiguration extends Configuration
hibernateMappingContext.getMappingCacheHolder());
List<Class> annotatedClasses = new ArrayList<>();
- for (PersistentEntity persistentEntity : hibernateMappingContext.getPersistentEntities()) {
+ for (HibernatePersistentEntity persistentEntity : hibernateMappingContext.getHibernatePersistentEntities(dataSourceName)) {
Class<?> javaClass = persistentEntity.getJavaClass();
- if (javaClass.isAnnotationPresent(Entity.class)) {
+ if (javaClass.isAnnotationPresent(Entity.class) || javaClass.isAnnotationPresent(grails.gorm.annotation.Entity.class)) {
annotatedClasses.add(javaClass);
}
}
@@ -320,7 +322,12 @@ public class HibernateMappingContextConfiguration extends Configuration
if (!additionalClasses.isEmpty()) {
for (Class additionalClass : additionalClasses) {
if (GormEntity.class.isAssignableFrom(additionalClass)) {
- hibernateMappingContext.addPersistentEntity(additionalClass);
+ PersistentEntity pe = hibernateMappingContext.addPersistentEntity(additionalClass);
+ if (pe instanceof GrailsHibernatePersistentEntity && ((GrailsHibernatePersistentEntity) pe).usesConnectionSource(dataSourceName)) {
+ if (additionalClass.isAnnotationPresent(Entity.class) || additionalClass.isAnnotationPresent(grails.gorm.annotation.Entity.class)) {
+ annotatedClasses.add(additionalClass);
+ }
+ }
}
}
}
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/GrailsDomainBinder.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/GrailsDomainBinder.java
index c4ae455288..52adfdc0a6 100644
--- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/GrailsDomainBinder.java
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/GrailsDomainBinder.java
@@ -240,7 +240,7 @@ public class GrailsDomainBinder implements AdditionalMappingContributor, TypeCon
hibernateMappingContext.getHibernatePersistentEntities(dataSourceName).stream()
.filter(persistentEntity -> persistentEntity.forGrailsDomainMapping(dataSourceName))
- .forEach(rootBinder::bindRoot);
+ .forEach(entity -> rootBinder.bindRoot(entity));
}
/**
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/dirty/GrailsEntityDirtinessStrategy.groovy b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/dirty/GrailsEntityDirtinessStrategy.groovy
index db98d1be36..e9c511127e 100644
--- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/dirty/GrailsEntityDirtinessStrategy.groovy
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/dirty/GrailsEntityDirtinessStrategy.groovy
@@ -16,26 +16,14 @@
* specific language governing permissions and limitations
* under the License.
*/
-/*
- * Copyright 2004-2005 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
package org.grails.orm.hibernate.dirty
import groovy.transform.CompileStatic
import org.hibernate.CustomEntityDirtinessStrategy
+import org.hibernate.CustomEntityDirtinessStrategy.AttributeChecker
+import org.hibernate.CustomEntityDirtinessStrategy.AttributeInformation
+import org.hibernate.CustomEntityDirtinessStrategy.DirtyCheckContext
import org.hibernate.Hibernate
import org.hibernate.Session
import org.hibernate.engine.spi.EntityEntry
@@ -45,16 +33,15 @@ import org.hibernate.persister.entity.EntityPersister
import org.slf4j.Logger
import org.slf4j.LoggerFactory
-import org.grails.datastore.gorm.GormEnhancer
+import org.grails.datastore.gorm.GormRegistry
import org.grails.datastore.mapping.dirty.checking.DirtyCheckable
import org.grails.datastore.mapping.dirty.checking.DirtyCheckingSupport
import org.grails.datastore.mapping.model.PersistentEntity
import org.grails.datastore.mapping.model.PersistentProperty
-import org.grails.datastore.mapping.model.config.GormProperties
import org.grails.datastore.mapping.model.types.Embedded
/**
- * A class to customize Hibernate dirtiness based on Grails {@link DirtyCheckable} interface
+ * Implementation of the {@link CustomEntityDirtinessStrategy} interface for Grails
*
* @author James Kleeh
* @author Graeme Rocher
@@ -73,7 +60,10 @@ class GrailsEntityDirtinessStrategy implements CustomEntityDirtinessStrategy {
@Override
boolean isDirty(Object entity, EntityPersister persister, Session session) {
- !session.contains(entity) || cast(entity).hasChanged() || DirtyCheckingSupport.areEmbeddedDirty(GormEnhancer.findEntity(Hibernate.getClass(entity)), entity)
+ DirtyCheckable dirtyCheckable = cast(entity)
+ PersistentEntity persistentEntity = GormRegistry.instance.apiResolver.findEntity(Hibernate.getClass(entity))
+ boolean dirty = !session.contains(entity) || dirtyCheckable.hasChanged() || (persistentEntity != null && DirtyCheckingSupport.areEmbeddedDirty(persistentEntity, entity))
+ return dirty
}
@Override
@@ -81,7 +71,7 @@ class GrailsEntityDirtinessStrategy implements CustomEntityDirtinessStrategy {
if (canDirtyCheck(entity, persister, session)) {
cast(entity).trackChanges()
try {
- PersistentEntity persistentEntity = GormEnhancer.findEntity(Hibernate.getClass(entity))
+ PersistentEntity persistentEntity = GormRegistry.instance.apiResolver.findEntity(Hibernate.getClass(entity))
if (persistentEntity != null) {
resetDirtyEmbeddedObjects(persistentEntity, entity, persister, session)
}
@@ -110,34 +100,53 @@ class GrailsEntityDirtinessStrategy implements CustomEntityDirtinessStrategy {
@Override
void findDirty(Object entity, EntityPersister persister, Session session, DirtyCheckContext dirtyCheckContext) {
if (!(entity instanceof DirtyCheckable)) return
+
+ SessionImplementor si = (SessionImplementor) session
Status status = getStatus(session, entity)
DirtyCheckable dirtyCheckable = cast(entity)
+
dirtyCheckContext.doDirtyChecking({ AttributeInformation info ->
// new object not yet in session — always dirty
- if (status == null) return true
- // deleted/gone/loading — not dirty
- if (status != Status.MANAGED) return false
- // lastUpdated is refreshed whenever anything changes
- if (GormProperties.LAST_UPDATED == info.name) return dirtyCheckable.hasChanged()
- // property-level check
- if (dirtyCheckable.hasChanged(info.name)) return true
- // embedded component — delegate to the embedded object's dirty tracking
- PersistentProperty prop = GormEnhancer.findEntity(Hibernate.getClass(entity))?.getPropertyByName(info.name)
- if (prop instanceof Embedded) {
- def val = prop.reader.read(entity)
- return val instanceof DirtyCheckable && val.hasChanged()
+ if (status == null) {
+ return true
}
+
+ // session is read-only, so no need to check
+ if (status == Status.READ_ONLY) {
+ return false
+ }
+
+ final String propertyName = info.getName()
+ if (dirtyCheckable.hasChanged(propertyName)) {
+ return true
+ }
+
+ if (propertyName == 'lastUpdated' && dirtyCheckable.hasChanged()) {
+ return true
+ }
+
+ final PersistentEntity persistentEntity = GormRegistry.instance.apiResolver.findEntity(Hibernate.getClass(entity))
+ if (persistentEntity != null) {
+ final PersistentProperty property = persistentEntity.getPropertyByName(propertyName)
+ if (property instanceof Embedded) {
+ final Object embeddedValue = ((Embedded) property).reader.read(entity)
+ if (embeddedValue instanceof DirtyCheckable && ((DirtyCheckable) embeddedValue).hasChanged()) {
+ return true
+ }
+ }
+ }
+
return false
} as AttributeChecker)
}
- static Status getStatus(Session session, Object entity) {
+ private Status getStatus(Session session, Object entity) {
SessionImplementor si = (SessionImplementor) session
EntityEntry entry = si.getPersistenceContext().getEntry(entity)
return entry != null ? entry.getStatus() : null
}
private static DirtyCheckable cast(Object entity) {
- return DirtyCheckable.cast(entity)
+ return (DirtyCheckable) entity
}
}
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/event/listener/HibernateEventListener.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/event/listener/HibernateEventListener.java
index 765f498d48..fa14bf1d85 100644
--- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/event/listener/HibernateEventListener.java
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/event/listener/HibernateEventListener.java
@@ -251,7 +251,7 @@ public class HibernateEventListener extends AbstractPersistenceEventListener {
datastore.getMappingContext().getPersistentEntity(clazz.getName());
shouldTrigger = (persistentEntity != null && isValidSessionFactory);
if (shouldTrigger) {
- eventListener = new ClosureEventListener(persistentEntity, failOnError, failOnErrorPackages);
+ eventListener = new ClosureEventListener(datastore, persistentEntity, failOnError, failOnErrorPackages);
ClosureEventListener previous = eventListeners.putIfAbsent(key, eventListener);
if (previous != null) {
eventListener = previous;
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/multitenancy/MultiTenantEventListener.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/multitenancy/MultiTenantEventListener.java
index 6fa6725bf4..cae28c8242 100644
--- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/multitenancy/MultiTenantEventListener.java
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/multitenancy/MultiTenantEventListener.java
@@ -25,7 +25,7 @@ import jakarta.annotation.Nullable;
import org.springframework.context.ApplicationEvent;
import grails.gorm.multitenancy.Tenants;
-import org.grails.datastore.gorm.GormEnhancer;
+import org.grails.datastore.gorm.GormRegistry;
import org.grails.datastore.mapping.core.Datastore;
import org.grails.datastore.mapping.core.connections.ConnectionSource;
import org.grails.datastore.mapping.engine.event.AbstractPersistenceEvent;
@@ -70,7 +70,7 @@ public class MultiTenantEventListener implements PersistenceEventListener {
PersistentEntity entity = query.getEntity();
if (entity.isMultiTenant()) {
- Datastore ds = (datastore != null) ? datastore : GormEnhancer.findDatastore(entity.getJavaClass());
+ Datastore ds = (datastore != null) ? datastore : GormRegistry.getInstance().getApiResolver().findDatastore(entity.getJavaClass());
if (ds instanceof HibernateDatastore hibernateDatastore) {
hibernateDatastore.enableMultiTenancyFilter();
}
@@ -82,7 +82,7 @@ public class MultiTenantEventListener implements PersistenceEventListener {
PersistentEntity entity = persistenceEvent.getEntity();
if (entity.isMultiTenant()) {
TenantId<?> tenantId = entity.getTenantId();
- Datastore ds = (datastore != null) ? datastore : GormEnhancer.findDatastore(entity.getJavaClass());
+ Datastore ds = (datastore != null) ? datastore : GormRegistry.getInstance().getApiResolver().findDatastore(entity.getJavaClass());
if (ds instanceof HibernateDatastore hibernateDatastore) {
Serializable currentId;
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateHqlQuery.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateHqlQuery.java
new file mode 100644
index 0000000000..36d1745b4b
--- /dev/null
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateHqlQuery.java
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.grails.orm.hibernate.query;
+
+import java.util.List;
+
+import org.springframework.context.ApplicationEventPublisher;
+
+import org.grails.datastore.mapping.core.Datastore;
+import org.grails.datastore.mapping.core.Session;
+import org.grails.datastore.mapping.model.PersistentEntity;
+import org.grails.datastore.mapping.query.Query;
+import org.grails.datastore.mapping.query.event.PostQueryEvent;
+import org.grails.datastore.mapping.query.event.PreQueryEvent;
+
+/**
+ * A query implementation for HQL queries in Hibernate 7.
+ *
+ * @author Graeme Rocher
+ * @since 6.0
+ */
+public class HibernateHqlQuery extends Query {
+ private final Object query;
+
+ public HibernateHqlQuery(Session session, PersistentEntity entity, org.hibernate.query.Query<?> query) {
+ super(session, entity);
+ this.query = query;
+ }
+
+ public HibernateHqlQuery(Session session, PersistentEntity entity, org.hibernate.query.SelectionQuery<?> query) {
+ super(session, entity);
+ this.query = query;
+ }
+
+ public HibernateHqlQuery(Session session, PersistentEntity entity, org.hibernate.query.MutationQuery query) {
+ super(session, entity);
+ this.query = query;
+ }
+
+ public Object uniqueResult() {
+ return singleResult();
+ }
+
+ @Override
+ protected void flushBeforeQuery() {
+ // do nothing, hibernate handles this
+ }
+
+ @Override
+ protected List executeQuery(PersistentEntity entity, Junction criteria) {
+ Datastore datastore = getSession().getDatastore();
+ ApplicationEventPublisher applicationEventPublisher = datastore.getApplicationEventPublisher();
+ if (applicationEventPublisher != null) {
+ PreQueryEvent preQueryEvent = new PreQueryEvent(datastore, this);
+ applicationEventPublisher.publishEvent(preQueryEvent);
+ }
+
+ List results;
+ if (query instanceof org.hibernate.query.SelectionQuery) {
+ org.hibernate.query.SelectionQuery selectionQuery = (org.hibernate.query.SelectionQuery) query;
+ if (uniqueResult) {
+ selectionQuery.setMaxResults(1);
+ }
+ results = selectionQuery.getResultList();
+ }
+ else if (query instanceof org.hibernate.query.MutationQuery) {
+ results = java.util.Collections.singletonList(((org.hibernate.query.MutationQuery) query).executeUpdate());
+ }
+ else {
+ throw new IllegalStateException("Unsupported query type: " + query.getClass().getName());
+ }
+
+ if (applicationEventPublisher != null) {
+ PostQueryEvent postQueryEvent = new PostQueryEvent(datastore, this, results);
+ applicationEventPublisher.publishEvent(postQueryEvent);
+ return postQueryEvent.getResults();
+ }
+ return results;
+ }
+}
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateQuery.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateQuery.java
index 57ace9ae81..1fdf2387f2 100644
--- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateQuery.java
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateQuery.java
@@ -86,6 +86,7 @@ public class HibernateQuery extends Query {
private Integer timeout;
private QueryFlushMode flushMode;
private Boolean readOnly;
+ private boolean wrapping = false;
public HibernateQuery(HibernateSession session, GrailsHibernatePersistentEntity entity) {
super(session, entity);
@@ -180,6 +181,20 @@ public class HibernateQuery extends Query {
detachedCriteria.add(criterion);
}
+ @Override
+ public Junction disjunction() {
+ Disjunction dis = new Disjunction();
+ detachedCriteria.add(dis);
+ return dis;
+ }
+
+ @Override
+ public Junction conjunction() {
+ Conjunction con = new Conjunction();
+ detachedCriteria.add(con);
+ return con;
+ }
+
public void add(DetachedCriteria<?> detachedCriteria) {
detachedCriteria.add(new Conjunction(detachedCriteria.getCriteria()));
}
@@ -438,6 +453,18 @@ public class HibernateQuery extends Query {
@Override
public List list() {
+ if (max != null && max > 0 && !wrapping) {
+ wrapping = true;
+ try {
+ return new PagedResultList(this);
+ } finally {
+ wrapping = false;
+ }
+ }
+ return executeListInternal();
+ }
+
+ public List executeListInternal() {
firePreQueryEvent();
List results = executeList();
return firePostQueryEvent(results);
@@ -451,6 +478,16 @@ public class HibernateQuery extends Query {
return getHibernateQueryExecutor().list(session, getJpaCriteriaQuery());
}
+ /**
+ * Deletes all entities matching the current criteria.
+ * Called by {@code GormStaticApi.deleteAll()} via {@code session.createQuery(cls).deleteAll()}.
+ *
+ * @return the number of entities deleted
+ */
+ public Number deleteAll() {
+ return ((HibernateSession) getSession()).deleteAll(detachedCriteria);
+ }
+
private HibernateQueryExecutor getHibernateQueryExecutor() {
return new HibernateQueryExecutor(
offset, max, lockResult, queryCache, fetchSize, timeout, flushMode, readOnly, proxyHandler);
@@ -546,7 +583,7 @@ public class HibernateQuery extends Query {
}
private Session getCurrentSession() {
- return getSessionFactory().getCurrentSession();
+ return ((HibernateSession) session).getNativeSession();
}
private SessionFactory getSessionFactory() {
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HqlListQueryBuilder.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HqlListQueryBuilder.java
index 53101799d5..4001a41df6 100644
--- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HqlListQueryBuilder.java
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HqlListQueryBuilder.java
@@ -87,6 +87,12 @@ public class HqlListQueryBuilder {
parts.add(buildSortPart(prop, direction, isIgnoreCase));
});
return String.join(", ", parts);
+ } else if (sort instanceof java.util.List) {
+ List<String> parts = new ArrayList<>();
+ for (Object prop : (java.util.List) sort) {
+ parts.add(buildSortPart(prop.toString(), order instanceof String ? (String) order : "asc", isIgnoreCase));
+ }
+ return String.join(", ", parts);
}
// Default sort from mapping
@@ -108,6 +114,22 @@ public class HqlListQueryBuilder {
}
}
+ // If no sort but order is present, default to identity
+ if (order != null) {
+ org.grails.datastore.mapping.model.PersistentProperty identity = entity.getIdentity();
+ if (identity != null) {
+ return buildSortPart(identity.getName(), order instanceof String ? (String) order : "asc", isIgnoreCase);
+ }
+ org.grails.datastore.mapping.model.PersistentProperty[] compositeId = entity.getCompositeIdentity();
+ if (compositeId != null && compositeId.length > 0) {
+ List<String> parts = new ArrayList<>();
+ for (org.grails.datastore.mapping.model.PersistentProperty prop : compositeId) {
+ parts.add(buildSortPart(prop.getName(), order instanceof String ? (String) order : "asc", isIgnoreCase));
+ }
+ return String.join(", ", parts);
+ }
+ }
+
return "";
}
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HqlQueryContext.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HqlQueryContext.java
index bf38f30742..48c2383d50 100644
--- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HqlQueryContext.java
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HqlQueryContext.java
@@ -37,6 +37,7 @@ import jakarta.annotation.Nullable;
import org.hibernate.jpa.AvailableHints;
import org.grails.datastore.mapping.model.PersistentEntity;
+import org.grails.orm.hibernate.cfg.domainbinding.hibernate.GrailsHibernatePersistentEntity;
import static org.grails.orm.hibernate.query.HqlQueryMethods.convertValue;
@@ -120,7 +121,10 @@ public record HqlQueryContext(
resolveHql(queryCharseq, isNative, namedParamsCopy) :
resolveHql(queryCharseq, isNative, positionalParamsCopy))
.filter(s -> !s.trim().isEmpty())
- .orElseGet(() -> "from %s".formatted(entity.getName()));
+ .orElseGet(() -> {
+ HqlListQueryBuilder builder = new HqlListQueryBuilder((GrailsHibernatePersistentEntity) entity, querySettingsCopy);
+ return builder.buildListHql();
+ });
namedParamsCopy.replaceAll((k, v) -> convertValue(v));
positionalParamsCopy.replaceAll(HqlQueryMethods::convertValue);
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/JpaCriteriaQueryCreator.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/JpaCriteriaQueryCreator.java
index b6391bd20c..8c3355367e 100644
--- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/JpaCriteriaQueryCreator.java
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/JpaCriteriaQueryCreator.java
@@ -54,6 +54,7 @@ public class JpaCriteriaQueryCreator<T> {
private final DetachedCriteria<?> detachedCriteria;
private final ConversionService conversionService;
private final HibernateQuery hibernateQuery;
+ private final PredicateGenerator predicateGenerator;
private JpaQueryContext parentContext;
public JpaCriteriaQueryCreator(
@@ -78,6 +79,7 @@ public class JpaCriteriaQueryCreator<T> {
this.detachedCriteria = detachedCriteria;
this.conversionService = conversionService;
this.hibernateQuery = hibernateQuery;
+ this.predicateGenerator = new PredicateGenerator(criteriaBuilder, conversionService);
}
public void setParentContext(JpaQueryContext parentContext) {
@@ -289,7 +291,6 @@ public class JpaCriteriaQueryCreator<T> {
List<Query.Criterion> criteriaList = detachedCriteria.getCriteria();
if (!criteriaList.isEmpty()) {
discoverAliases(criteriaList, context);
- var predicateGenerator = new PredicateGenerator(criteriaBuilder, conversionService);
var predicate = predicateGenerator.generate(cq, root, criteriaList, context, entity);
if (predicate != null) {
cq.where(predicate);
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/PagedResultList.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/PagedResultList.java
new file mode 100644
index 0000000000..67582ccaa6
--- /dev/null
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/PagedResultList.java
@@ -0,0 +1,148 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.grails.orm.hibernate.query;
+
+import java.io.ObjectStreamException;
+import java.io.Serializable;
+import java.util.List;
+
+import org.grails.datastore.mapping.model.PersistentEntity;
+import org.grails.datastore.mapping.query.Query;
+import org.grails.orm.hibernate.GrailsHibernateTemplate;
+import org.grails.orm.hibernate.cfg.domainbinding.hibernate.GrailsHibernatePersistentEntity;
+
+/**
+ * A PagedResultList for Hibernate 7.
+ *
+ * @author burt
+ * @since 7.0.0
+ */
+public class PagedResultList extends grails.gorm.PagedResultList {
+
+ private final GrailsHibernatePersistentEntity entity;
+ private final int max;
+ private final int offset;
+
+ public PagedResultList(HibernateQuery query) {
+ super(query);
+ this.entity = query.getEntity();
+ this.max = resolveMax(query);
+ this.offset = resolveOffset(query);
+ }
+
+ public PagedResultList(Query query) {
+ super(query);
+ this.entity = (GrailsHibernatePersistentEntity) query.getEntity();
+ this.max = resolveMax(query);
+ this.offset = resolveOffset(query);
+ }
+
+ public PagedResultList(GrailsHibernateTemplate template, GrailsHibernatePersistentEntity entity, Query query) {
+ super(query);
+ this.entity = entity;
+ this.max = resolveMax(query);
+ this.offset = resolveOffset(query);
+ }
+
+ public PagedResultList(GrailsHibernateTemplate template, PersistentEntity entity, Query query) {
+ this(template, (GrailsHibernatePersistentEntity) entity, query);
+ }
+
+ private PagedResultList(GrailsHibernatePersistentEntity entity, int max, int offset, int totalCount, List resultList) {
+ super(null);
+ this.entity = entity;
+ this.max = max;
+ this.offset = offset;
+ this.totalCount = totalCount;
+ this.resultList = resultList;
+ }
+
+ public GrailsHibernatePersistentEntity getEntity() {
+ return entity;
+ }
+
+ @Override
+ public int getMax() {
+ return max;
+ }
+
+ @Override
+ public int getOffset() {
+ return offset;
+ }
+
+ @Override
+ public int getTotalCount() {
+ if (totalCount == Integer.MIN_VALUE) {
+ Query query = getQuery();
+ if (query == null) {
+ totalCount = 0;
+ } else {
+ Object clonedQuery = query.clone();
+ if (!(clonedQuery instanceof Query)) {
+ totalCount = 0;
+ } else {
+ Query newQuery = (Query) clonedQuery;
+ newQuery.offset(0);
+ newQuery.max(-1);
+ newQuery.clearOrders();
+ newQuery.projections().count();
+ Number result = (Number) newQuery.singleResult();
+ totalCount = result == null ? 0 : result.intValue();
+ }
+ }
+ }
+ return totalCount;
+ }
+
+ private Object writeReplace() throws ObjectStreamException {
+ return new SerializationProxy(max, offset, getTotalCount(), resultList);
+ }
+
+ private static int resolveMax(Query query) {
+ Integer queryMax = query == null ? null : query.getMax();
+ return queryMax != null ? queryMax : -1;
+ }
+
+ private static int resolveOffset(Query query) {
+ Integer queryOffset = query == null ? null : query.getOffset();
+ return queryOffset != null ? queryOffset : 0;
+ }
+
+ private static final class SerializationProxy implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ private final int max;
+ private final int offset;
+ private final int totalCount;
+ private final List resultList;
+
+ private SerializationProxy(int max, int offset, int totalCount, List resultList) {
+ this.max = max;
+ this.offset = offset;
+ this.totalCount = totalCount;
+ this.resultList = resultList;
+ }
+
+ private Object readResolve() {
+ return new PagedResultList(null, max, offset, totalCount, resultList);
+ }
+ }
+}
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/SelectHqlQuery.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/SelectHqlQuery.java
index 76f27fcd65..d9d649b258 100644
--- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/SelectHqlQuery.java
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/SelectHqlQuery.java
@@ -21,8 +21,14 @@ package org.grails.orm.hibernate.query;
import java.io.Serializable;
import java.util.List;
+import org.springframework.context.ApplicationEventPublisher;
+
+import org.grails.datastore.mapping.core.Datastore;
import org.grails.datastore.mapping.query.Query;
+import org.grails.datastore.mapping.query.event.PostQueryEvent;
+import org.grails.datastore.mapping.query.event.PreQueryEvent;
import org.grails.orm.hibernate.GrailsHibernateTemplate;
+import org.grails.orm.hibernate.HibernateDatastore;
import org.grails.orm.hibernate.HibernateSession;
import org.grails.orm.hibernate.IHibernateTemplate;
import org.grails.orm.hibernate.cfg.domainbinding.hibernate.GrailsHibernatePersistentEntity;
@@ -31,29 +37,112 @@ public class SelectHqlQuery extends Query implements HqlQueryMethods, Serializab
protected final transient HqlQueryContext queryContext;
protected final transient HqlQueryDelegate delegate;
+ private boolean wrapping = false;
+ private boolean countQuery = false;
+
protected SelectHqlQuery(HibernateSession session, GrailsHibernatePersistentEntity entity, HqlQueryContext queryContext, HqlQueryDelegate delegate) {
super(session, entity);
this.queryContext = queryContext;
this.delegate = delegate;
}
+ @Override
+ public ProjectionList projections() {
+ return new ProjectionList() {
+ @Override
+ public org.grails.datastore.mapping.query.api.ProjectionList count() {
+ countQuery = true;
+ return this;
+ }
+ };
+ }
+
@Override
public List<?> list() {
+ if (getMax() > 0 && !wrapping && !countQuery) {
+ wrapping = true;
+ try {
+ return new PagedResultList(this);
+ } finally {
+ wrapping = false;
+ }
+ }
+ return executeListInternal();
+ }
+
+ protected List<?> executeListInternal() {
+ firePreQueryEvent();
GrailsHibernateTemplate template = (GrailsHibernateTemplate) getHibernateTemplate();
- return template.execute(__ -> {
+ List<?> results = template.execute(__ -> {
+ if (countQuery) {
+ HqlListQueryBuilder builder = new HqlListQueryBuilder((GrailsHibernatePersistentEntity) entity, queryContext.querySettings());
+ String countHql = builder.buildCountHql();
+ org.hibernate.query.Query<?> q = __.createQuery(countHql, Long.class);
+ HqlQueryMethods.populateParameters(new SelectQueryDelegate(q), queryContext);
+ return q.list();
+ }
applyQuerySettings(delegate);
return delegate.list();
});
+ return firePostQueryEvent(results);
+ }
+
+ private void firePreQueryEvent() {
+ Datastore datastore = getSession().getDatastore();
+ ApplicationEventPublisher publisher = datastore.getApplicationEventPublisher();
+ if (publisher != null) {
+ publisher.publishEvent(new PreQueryEvent(datastore, this));
+ }
+ }
+
+ private List<?> firePostQueryEvent(List<?> results) {
+ Datastore datastore = getSession().getDatastore();
+ ApplicationEventPublisher publisher = datastore.getApplicationEventPublisher();
+ if (publisher != null) {
+ PostQueryEvent postQueryEvent = new PostQueryEvent(datastore, this, results);
+ publisher.publishEvent(postQueryEvent);
+ return postQueryEvent.getResults();
+ }
+ return results;
+ }
+
+ @Override
+ public SelectHqlQuery clone() {
+ HibernateSession hibernateSession = (HibernateSession) getSession();
+ SelectHqlQuery cloned = (SelectHqlQuery) HibernateHqlQueryCreator.createHqlQuery(
+ (HibernateDatastore) hibernateSession.getDatastore(),
+ hibernateSession.getHibernateTemplate().getSessionFactory(),
+ entity,
+ queryContext
+ );
+ if (this.max != null) {
+ cloned.max(this.max);
+ }
+ if (this.offset != null) {
+ cloned.offset(this.offset);
+ }
+ return cloned;
}
@Override
public Object singleResult() {
+ firePreQueryEvent();
GrailsHibernateTemplate template = (GrailsHibernateTemplate) getHibernateTemplate();
- return template.execute(__ -> {
+ Object result = template.execute(__ -> {
+ if (countQuery) {
+ HqlListQueryBuilder builder = new HqlListQueryBuilder((GrailsHibernatePersistentEntity) entity, queryContext.querySettings());
+ String countHql = builder.buildCountHql();
+ org.hibernate.query.Query<?> q = __.createQuery(countHql, Long.class);
+ HqlQueryMethods.populateParameters(new SelectQueryDelegate(q), queryContext);
+ return q.getSingleResult();
+ }
applyQuerySettings(delegate);
List<?> results = delegate.list();
return results.isEmpty() ? null : results.getFirst();
});
+ List<?> resultList = result != null ? java.util.Collections.singletonList(result) : java.util.Collections.emptyList();
+ List<?> fired = firePostQueryEvent(resultList);
+ return fired.isEmpty() ? null : fired.get(0);
}
protected void applyQuerySettings(HqlQueryDelegate d) {
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/ClosureEventListener.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/ClosureEventListener.java
index 772dc24aa7..bf07578cf5 100644
--- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/ClosureEventListener.java
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/ClosureEventListener.java
@@ -74,7 +74,6 @@ import org.grails.datastore.mapping.model.PersistentProperty;
import org.grails.datastore.mapping.model.config.GormProperties;
import org.grails.datastore.mapping.reflect.ClassUtils;
import org.grails.datastore.mapping.reflect.EntityReflector;
-import org.grails.datastore.mapping.validation.ValidationException;
import org.grails.orm.hibernate.HibernateGormValidationApi;
import org.grails.orm.hibernate.cfg.domainbinding.hibernate.GrailsHibernatePersistentEntity;
@@ -98,6 +97,16 @@ public class ClosureEventListener
@Serial
private static final long serialVersionUID = 1;
+ /**
+ * Thread-local flag set by {@code save(deepValidate: false)} to suppress validation
+ * inside Hibernate cascade events for all transitively reachable entities within the
+ * same {@code session.persist()} call. In Hibernate 7, a vetoed insert throws
+ * {@link org.hibernate.action.internal.EntityActionVetoException} rather than silently
+ * cancelling the action as Hibernate 5 did, so cascade-reachable entities must not be
+ * validated when the root save explicitly opts out of deep validation.
+ */
+ public static final ThreadLocal<Boolean> SKIP_DEEP_VALIDATION = new ThreadLocal<>();
+
private final transient EventTriggerCaller beforeInsertCaller;
private final transient EventTriggerCaller preLoadEventCaller;
private final transient EventTriggerCaller postLoadEventListener;
@@ -112,8 +121,12 @@ public class ClosureEventListener
private final boolean failOnErrorEnabled;
private final Map validateParams;
+ private final transient org.grails.orm.hibernate.HibernateDatastore hibernateDatastore;
+
public ClosureEventListener(
+ org.grails.orm.hibernate.HibernateDatastore hibernateDatastore,
GrailsHibernatePersistentEntity persistentEntity, boolean failOnError, List failOnErrorPackages) {
+ this.hibernateDatastore = hibernateDatastore;
this.persistentEntity = persistentEntity;
Class domainClazz = persistentEntity.getJavaClass();
this.domainMetaClass = GroovySystem.getMetaClassRegistry().getMetaClass(domainClazz);
@@ -240,14 +253,22 @@ public class ClosureEventListener
protected boolean doValidate(Object entity) {
GormValidateable validateable = (GormValidateable) entity;
- if (!validateable.shouldSkipValidation() && !validateable.validate(validateParams)) {
- if (failOnErrorEnabled) {
- throw ValidationException.newInstance(
- "Validation error whilst flushing entity [" +
- entity.getClass().getName() + "]",
- validateable.getErrors());
+ if (!validateable.shouldSkipValidation() && !Boolean.TRUE.equals(SKIP_DEEP_VALIDATION.get())) {
+ String qualifier = org.grails.datastore.mapping.core.connections.ConnectionSource.DEFAULT;
+ if (hibernateDatastore != null) {
+ qualifier = hibernateDatastore.getConnectionSources().getDefaultConnectionSource().getName();
+ }
+ org.grails.datastore.gorm.GormValidationApi validationApi = org.grails.datastore.gorm.GormRegistry.getInstance().findValidationApi(entity.getClass(), qualifier);
+
+ if (validationApi != null && !validationApi.validate(entity, validateParams)) {
+ if (failOnErrorEnabled) {
+ throw org.grails.datastore.mapping.validation.ValidationException.newInstance(
+ "Validation error whilst flushing entity [" +
+ entity.getClass().getName() + "]",
+ validateable.getErrors());
+ }
+ return true;
}
- return true;
}
return false;
}
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/ClosureEventTriggeringInterceptor.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/ClosureEventTriggeringInterceptor.java
index 918e42cdfa..b0c3beacc4 100644
--- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/ClosureEventTriggeringInterceptor.java
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/ClosureEventTriggeringInterceptor.java
@@ -20,6 +20,7 @@ package org.grails.orm.hibernate.support;
import java.io.Serial;
import java.io.Serializable;
+import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
@@ -70,8 +71,10 @@ import org.grails.datastore.mapping.engine.ModificationTrackingEntityAccess;
import org.grails.datastore.mapping.engine.event.AbstractPersistenceEvent;
import org.grails.datastore.mapping.model.MappingContext;
import org.grails.datastore.mapping.model.PersistentEntity;
+import org.grails.datastore.mapping.model.PersistentProperty;
import org.grails.datastore.mapping.model.types.Embedded;
import org.grails.datastore.mapping.proxy.ProxyHandler;
+import org.grails.datastore.mapping.reflect.EntityReflector;
import org.grails.orm.hibernate.HibernateDatastore;
/**
@@ -282,29 +285,57 @@ public class ClosureEventTriggeringInterceptor
private void synchronizeHibernateState(
PreInsertEvent hibernateEvent, ModificationTrackingEntityAccess entityAccess) {
- Map<String, Object> modifiedProperties = entityAccess.getModifiedProperties();
+ Object[] state = hibernateEvent.getState();
+ EntityPersister persister = hibernateEvent.getPersister();
+ Map<String, Object> modifiedProperties = findModifiedProperties(hibernateEvent.getEntity(), persister, state);
+ modifiedProperties.putAll(entityAccess.getModifiedProperties());
if (!modifiedProperties.isEmpty()) {
- Object[] state = hibernateEvent.getState();
- EntityPersister persister = hibernateEvent.getPersister();
synchronizeHibernateState(persister, state, modifiedProperties);
}
}
private void synchronizeHibernateState(
PreUpdateEvent hibernateEvent, ModificationTrackingEntityAccess entityAccess, boolean autoTimestamp) {
- Map<String, Object> modifiedProperties = entityAccess.getModifiedProperties();
+ Object[] state = hibernateEvent.getState();
+ EntityPersister persister = hibernateEvent.getPersister();
+ Map<String, Object> modifiedProperties = findModifiedProperties(hibernateEvent.getEntity(), persister, state);
+ modifiedProperties.putAll(entityAccess.getModifiedProperties());
if (autoTimestamp) {
updateModifiedPropertiesWithAutoTimestamp(modifiedProperties, hibernateEvent);
}
if (!modifiedProperties.isEmpty()) {
- Object[] state = hibernateEvent.getState();
- EntityPersister persister = hibernateEvent.getPersister();
synchronizeHibernateState(persister, state, modifiedProperties);
}
}
+ private Map<String, Object> findModifiedProperties(Object entity, EntityPersister persister, Object[] state) {
+ Map<String, Object> modifiedProperties = new HashMap<>();
+ PersistentEntity persistentEntity = mappingContext.getPersistentEntity(Hibernate.getClass(entity).getName());
+ if (persistentEntity != null) {
+ EntityReflector reflector = persistentEntity.getReflector();
+ EntityMappingType entityMappingType = persister.getEntityMappingType();
+ entityMappingType.getAttributeMappings().forEach(attributeMapping -> {
+ String propertyName = attributeMapping.getAttributeName();
+ if ("version".equals(propertyName)) {
+ return;
+ }
+ PersistentProperty property = persistentEntity.getPropertyByName(propertyName);
+ if (property != null) {
+ int stateIdx = attributeMapping.getStateArrayPosition();
+ if (stateIdx >= 0 && stateIdx < state.length) {
+ Object value = reflector.getProperty(entity, propertyName);
+ if (state[stateIdx] != value) {
+ modifiedProperties.put(propertyName, value);
+ }
+ }
+ }
+ });
+ }
+ return modifiedProperties;
+ }
+
private void updateModifiedPropertiesWithAutoTimestamp(
Map<String, Object> modifiedProperties, PreUpdateEvent hibernateEvent) {
@@ -422,19 +453,12 @@ public class ClosureEventTriggeringInterceptor
Hibernate.getClass(entity).getName());
Object unwrapped = proxyHandler.unwrap(entity);
DirtyCheckable dirtyCheckable = (DirtyCheckable) unwrapped;
- Map<String, Object> dirtyCheckingState =
- persistentEntity.getReflector().getDirtyCheckingState(unwrapped);
- if (dirtyCheckingState == null) {
- dirtyCheckable.trackChanges();
- for (Embedded<?> association : persistentEntity.getEmbedded()) {
- if (DirtyCheckable.class.isAssignableFrom(association.getType())) {
- Object embedded = association.getReader().read(unwrapped);
- if (embedded != null) {
- DirtyCheckable embeddedCheck = (DirtyCheckable) embedded;
- if (embeddedCheck.listDirtyPropertyNames().isEmpty()) {
- embeddedCheck.trackChanges();
- }
- }
+ dirtyCheckable.trackChanges();
+ for (Embedded<?> association : persistentEntity.getEmbedded()) {
+ if (DirtyCheckable.class.isAssignableFrom(association.getType())) {
+ Object embedded = association.getReader().read(unwrapped);
+ if (embedded != null) {
+ ((DirtyCheckable) embedded).trackChanges();
}
}
}
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/GormAutoTimestampFlushEntityEventListener.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/GormAutoTimestampFlushEntityEventListener.java
new file mode 100644
index 0000000000..93a88dc19f
--- /dev/null
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/GormAutoTimestampFlushEntityEventListener.java
@@ -0,0 +1,125 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.grails.orm.hibernate.support;
+
+import java.util.Set;
+
+import org.hibernate.Hibernate;
+import org.hibernate.HibernateException;
+import org.hibernate.engine.spi.EntityEntry;
+import org.hibernate.engine.spi.Status;
+import org.hibernate.event.spi.FlushEntityEvent;
+import org.hibernate.event.spi.FlushEntityEventListener;
+import org.hibernate.persister.entity.EntityPersister;
+
+import org.grails.datastore.gorm.events.AutoTimestampEventListener;
+import org.grails.datastore.mapping.dirty.checking.DirtyCheckable;
+import org.grails.datastore.mapping.model.MappingContext;
+import org.grails.datastore.mapping.model.PersistentEntity;
+
+/**
+ * A Hibernate {@link FlushEntityEventListener} that ensures auto-timestamp properties
+ * (e.g., {@code lastUpdated}) are set on the entity BEFORE Hibernate computes dirty
+ * properties during the flush phase.
+ *
+ * <p>When {@code dynamicUpdate = true}, Hibernate generates a SQL UPDATE that only
+ * includes columns marked as dirty. Dirty properties are computed during the flush phase
+ * (via {@code FlushEntityEvent}), before {@code PreUpdateEvent} fires. Setting
+ * {@code lastUpdated} in a {@code PreUpdateEventListener} is therefore too late — the
+ * column is excluded from the dynamic SQL even though its value was updated in the state
+ * array.</p>
+ *
+ * <p>This listener is registered as a <em>prepended</em> listener so it runs before
+ * {@code DefaultFlushEntityEventListener}. It sets {@code lastUpdated} directly on the
+ * entity instance, so the subsequent dirty check includes it in the dirty-property set.</p>
+ */
+public class GormAutoTimestampFlushEntityEventListener implements FlushEntityEventListener {
+
+ private final AutoTimestampEventListener autoTimestampEventListener;
+ private final MappingContext mappingContext;
+
+ public GormAutoTimestampFlushEntityEventListener(
+ AutoTimestampEventListener autoTimestampEventListener,
+ MappingContext mappingContext) {
+ this.autoTimestampEventListener = autoTimestampEventListener;
+ this.mappingContext = mappingContext;
+ }
+
+ @Override
+ public void onFlushEntity(FlushEntityEvent event) throws HibernateException {
+ final Object entity = event.getEntity();
+ final EntityEntry entry = event.getEntityEntry();
+
+ // Only handle managed entities being updated, not new inserts or deletes
+ if (entry.getStatus() != Status.MANAGED) {
+ return;
+ }
+
+ // No loadedState means this is a new entity (INSERT path), not an UPDATE
+ final Object[] loadedState = entry.getLoadedState();
+ if (loadedState == null) {
+ return;
+ }
+
+ // Resolve the GORM PersistentEntity for this entity class
+ final Class<?> entityClass = Hibernate.getClass(entity);
+ final PersistentEntity persistentEntity = mappingContext.getPersistentEntity(entityClass.getName());
+ if (persistentEntity == null) {
+ return;
+ }
+
+ // Respect autoTimestamp = false mappings
+ if (persistentEntity.getMapping().getMappedForm() != null &&
+ !persistentEntity.getMapping().getMappedForm().isAutoTimestamp()) {
+ return;
+ }
+
+ // Skip entities that have no lastUpdated property registered
+ final Set<String> lastUpdatedProps =
+ autoTimestampEventListener.getLastUpdatedPropertyNames(persistentEntity.getName());
+ if (lastUpdatedProps == null || lastUpdatedProps.isEmpty()) {
+ return;
+ }
+
+ // Perform the dirty check to avoid triggering spurious UPDATEs on clean entities
+ final EntityPersister persister = entry.getPersister();
+ final Object[] currentState = persister.getValues(entity);
+ final int[] dirtyProps =
+ persister.findDirty(currentState, loadedState, entity, event.getSession());
+ if (dirtyProps == null || dirtyProps.length == 0) {
+ return;
+ }
+
+ // Entity IS dirty — set lastUpdated on the entity BEFORE DefaultFlushEntityEventListener
+ // reads the entity values and computes the dirty-property set.
+ autoTimestampEventListener.beforeUpdate(
+ persistentEntity,
+ mappingContext.createEntityAccess(persistentEntity, entity));
+
+ // GrailsEntityDirtinessStrategy uses DirtyCheckable.hasChanged() to determine dirty properties.
+ // Since ea.setProperty() bypasses the entity setter, we must explicitly mark lastUpdated as
+ // dirty so that GrailsEntityDirtinessStrategy.findDirty() includes it in the dirty set, which
+ // in turn ensures dynamicUpdate=true SQL includes the last_updated column.
+ if (entity instanceof DirtyCheckable) {
+ for (String prop : lastUpdatedProps) {
+ ((DirtyCheckable) entity).markDirty(prop);
+ }
+ }
+ }
+}
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/HibernateRuntimeUtils.groovy b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/HibernateRuntimeUtils.groovy
index d6f57b465e..d75e7f1736 100644
--- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/HibernateRuntimeUtils.groovy
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/HibernateRuntimeUtils.groovy
@@ -130,6 +130,33 @@ class HibernateRuntimeUtils {
}
}
+ private static ThreadLocal<Boolean> insertActive = new ThreadLocal<Boolean>() {
+ @Override
+ protected Boolean initialValue() {
+ return Boolean.FALSE
+ }
+ }
+
+ static void markInsertActive() {
+ insertActive.set(Boolean.TRUE)
+ }
+
+ static void resetInsertActive() {
+ insertActive.set(Boolean.FALSE)
+ }
+
+ static boolean isInsertActive() {
+ return insertActive.get()
+ }
+
+ static void setObjectToReadWrite(Object target, SessionFactory sessionFactory) {
+ org.grails.orm.hibernate.cfg.GrailsHibernateUtil.setObjectToReadWrite(target, sessionFactory)
+ }
+
+ static void setObjectToReadOnly(Object target, SessionFactory sessionFactory) {
+ org.grails.orm.hibernate.cfg.GrailsHibernateUtil.setObjectToReadyOnly(target, sessionFactory)
+ }
+
static Object convertValueToType(Object value, Class targetType, ConversionService conversionService) {
if (targetType != null && value != null && !targetType.isInstance(value)) {
if (value instanceof CharSequence) {
diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/HibernateGormDatastoreSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/HibernateGormDatastoreSpec.groovy
index c99bb40fe8..43eac0dbfe 100644
--- a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/HibernateGormDatastoreSpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/HibernateGormDatastoreSpec.groovy
@@ -25,6 +25,7 @@ import org.apache.grails.data.testing.tck.base.GrailsDataTckSpec
import org.grails.datastore.mapping.model.PersistentEntity
import org.grails.orm.hibernate.HibernateSession
import org.grails.orm.hibernate.HibernateDatastore
+import org.grails.orm.hibernate.GrailsHibernateTransactionManager
import org.grails.orm.hibernate.cfg.domainbinding.binder.GrailsDomainBinder
import org.grails.orm.hibernate.cfg.domainbinding.hibernate.GrailsHibernatePersistentEntity
import org.grails.orm.hibernate.cfg.HibernateMappingContext
@@ -155,6 +156,10 @@ class HibernateGormDatastoreSpec extends GrailsDataTckSpec<GrailsDataHibernate7T
manager.hibernateDatastore
}
+ protected GrailsHibernateTransactionManager getTransactionManager() {
+ (GrailsHibernateTransactionManager) manager.transactionManager
+ }
+
protected org.hibernate.query.criteria.HibernateCriteriaBuilder getCriteriaBuilder() {
return getSessionFactory().getCriteriaBuilder();
}
diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/HibernatePagedResultListSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/HibernatePagedResultListSpec.groovy
index ee8501d11a..3efc74d42f 100644
--- a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/HibernatePagedResultListSpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/HibernatePagedResultListSpec.groovy
@@ -20,7 +20,7 @@ package grails.gorm.tests
import grails.gorm.annotation.Entity
import grails.gorm.hibernate.HibernateEntity
-import org.grails.orm.hibernate.query.HibernatePagedResultList
+import org.grails.orm.hibernate.query.PagedResultList
import spock.lang.Issue
class HibernatePagedResultListSpec extends HibernateGormDatastoreSpec {
@@ -29,7 +29,7 @@ class HibernatePagedResultListSpec extends HibernateGormDatastoreSpec {
manager.registerDomainClasses(HPBook)
}
- void "test HibernatePagedResultList totalCount with HQL query"() {
+ void "test PagedResultList totalCount with HQL query"() {
given:
(1..10).each { i -> new HPBook(title: "Book $i").save() }
session.flush()
@@ -39,7 +39,7 @@ class HibernatePagedResultListSpec extends HibernateGormDatastoreSpec {
def results = HPBook.list(max: 3, offset: 2, sort: "id")
then:
- results instanceof HibernatePagedResultList
+ results instanceof PagedResultList
results.size() == 3
results.totalCount == 10
results.max == 3
@@ -49,7 +49,7 @@ class HibernatePagedResultListSpec extends HibernateGormDatastoreSpec {
results[2].title == "Book 5"
}
- void "test HibernatePagedResultList totalCount with Criteria query"() {
+ void "test PagedResultList totalCount with Criteria query"() {
given:
new HPBook(title: "The Stand").save()
new HPBook(title: "The Shining").save()
@@ -64,7 +64,7 @@ class HibernatePagedResultListSpec extends HibernateGormDatastoreSpec {
}
then:
- results instanceof HibernatePagedResultList
+ results instanceof PagedResultList
results.size() == 2
results.totalCount == 2
results.max == 2
@@ -73,7 +73,7 @@ class HibernatePagedResultListSpec extends HibernateGormDatastoreSpec {
results[1].title == "The Stand"
}
- void "test HibernatePagedResultList serialization"() {
+ void "test PagedResultList serialization"() {
given:
(1..5).each { i -> new HPBook(title: "Book $i").save() }
session.flush()
@@ -92,7 +92,7 @@ class HibernatePagedResultListSpec extends HibernateGormDatastoreSpec {
// Deserialize
def bais = new ByteArrayInputStream(baos.toByteArray())
def ois = new ObjectInputStream(bais)
- def deserializedResults = (HibernatePagedResultList) ois.readObject()
+ def deserializedResults = (PagedResultList) ois.readObject()
ois.close()
then:
@@ -113,7 +113,7 @@ class HibernatePagedResultListSpec extends HibernateGormDatastoreSpec {
mockQuery.list() >> ["a", "b"]
when:
- def results = new HibernatePagedResultList(mockQuery)
+ def results = new PagedResultList(mockQuery)
then:
results.size() == 2
diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/PagedResultListSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/PagedResultListSpec.groovy
index b539617ca0..faf20ff51b 100644
--- a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/PagedResultListSpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/PagedResultListSpec.groovy
@@ -20,7 +20,6 @@ package grails.gorm.tests
import grails.gorm.PagedResultList
import grails.gorm.annotation.Entity
import grails.gorm.hibernate.HibernateEntity
-import org.grails.orm.hibernate.query.HibernatePagedResultList
class PagedResultListSpec extends HibernateGormDatastoreSpec {
@@ -40,7 +39,7 @@ class PagedResultListSpec extends HibernateGormDatastoreSpec {
def results = PRLBook.list(max: 2, sort: "title")
then:
- results instanceof HibernatePagedResultList
+ results instanceof org.grails.orm.hibernate.query.PagedResultList
results.size() == 2
results.totalCount == 3
results[0].title == "Carrie"
@@ -57,7 +56,7 @@ class PagedResultListSpec extends HibernateGormDatastoreSpec {
def results = PRLBook.list(max: 3, offset: 2, sort: "id")
then:
- results instanceof HibernatePagedResultList
+ results instanceof org.grails.orm.hibernate.query.PagedResultList
results.size() == 3
results.totalCount == 10
results.max == 3
@@ -81,7 +80,7 @@ class PagedResultListSpec extends HibernateGormDatastoreSpec {
}
then:
- results instanceof HibernatePagedResultList
+ results instanceof org.grails.orm.hibernate.query.PagedResultList
results.size() == 2
results.totalCount == 2
results.max == 2
diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/WithNewSessionAndExistingTransactionSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/WithNewSessionAndExistingTransactionSpec.groovy
index a7f3884a89..ed899163a4 100644
--- a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/WithNewSessionAndExistingTransactionSpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/WithNewSessionAndExistingTransactionSpec.groovy
@@ -40,6 +40,10 @@ class WithNewSessionAndExistingTransactionSpec extends GrailsDataTckSpec<GrailsD
void "Test withNewSession when an existing transaction is present"() {
when: "An existing transaction not to pick up the current session"
+ DataSource dataSource = ((HibernateDatastore) manager.session.datastore).connectionSources.defaultConnectionSource.dataSource
+ org.apache.tomcat.jdbc.pool.DataSource tomcatDataSource = dataSource.targetDataSource.targetDataSource
+ int initialActive = tomcatDataSource.pool.active
+
manager.sessionFactory.currentSession
SessionHolder previousSessionHolder = TransactionSynchronizationManager.getResource(manager.sessionFactory)
Book.withNewSession { Session session ->
@@ -50,13 +54,11 @@ class WithNewSessionAndExistingTransactionSpec extends GrailsDataTckSpec<GrailsD
// reproduce session closed problem
int result = Book.count()
SessionHolder sessionHolder = TransactionSynchronizationManager.getResource(manager.sessionFactory)
- DataSource dataSource = ((HibernateDatastore) manager.session.datastore).connectionSources.defaultConnectionSource.dataSource
- org.apache.tomcat.jdbc.pool.DataSource tomcatDataSource = dataSource.targetDataSource.targetDataSource
then: "The result is correct"
dataSource != null
tomcatDataSource != null
- tomcatDataSource.pool.active == 1
+ tomcatDataSource.pool.active == initialActive
sessionHolder.is(previousSessionHolder)
TransactionSynchronizationManager.isSynchronizationActive()
sessionHolder.session.isOpen()
@@ -71,6 +73,10 @@ class WithNewSessionAndExistingTransactionSpec extends GrailsDataTckSpec<GrailsD
@Issue('https://github.com/grails/grails-core/issues/10426')
void "Test with withNewSession with nested transaction"() {
when: "An existing transaction not to pick up the current session"
+ DataSource dataSource = ((HibernateDatastore) manager.session.datastore).connectionSources.defaultConnectionSource.dataSource
+ org.apache.tomcat.jdbc.pool.DataSource tomcatDataSource = dataSource.targetDataSource.targetDataSource
+ int initialActive = tomcatDataSource.pool.active
+
manager.sessionFactory.currentSession
SessionHolder previousSessionHolder = TransactionSynchronizationManager.getResource(manager.sessionFactory)
Book.withNewSession { Session session ->
@@ -87,13 +93,10 @@ class WithNewSessionAndExistingTransactionSpec extends GrailsDataTckSpec<GrailsD
Book.count()
SessionHolder sessionHolder = TransactionSynchronizationManager.getResource(manager.sessionFactory)
- DataSource dataSource = ((HibernateDatastore) manager.session.datastore).connectionSources.defaultConnectionSource.dataSource
- org.apache.tomcat.jdbc.pool.DataSource tomcatDataSource = dataSource.targetDataSource.targetDataSource
-
then: "The result is correct"
dataSource != null
tomcatDataSource != null
- tomcatDataSource.pool.active == 1
+ tomcatDataSource.pool.active == initialActive
sessionHolder.is(previousSessionHolder)
TransactionSynchronizationManager.isSynchronizationActive()
sessionHolder.session.isOpen()
@@ -109,11 +112,11 @@ class WithNewSessionAndExistingTransactionSpec extends GrailsDataTckSpec<GrailsD
when: "the connection pool is obtained"
DataSource dataSource = ((HibernateDatastore) manager.session.datastore).connectionSources.defaultConnectionSource.dataSource
org.apache.tomcat.jdbc.pool.DataSource tomcatDataSource = dataSource.targetDataSource.targetDataSource
+ int initialActive = tomcatDataSource.pool.active
then: "the active count is correct"
dataSource != null
tomcatDataSource != null
- tomcatDataSource.pool.active == 0
when: "An existing transaction not to pick up the current session"
manager.sessionFactory.currentSession
@@ -133,13 +136,13 @@ class WithNewSessionAndExistingTransactionSpec extends GrailsDataTckSpec<GrailsD
then: "After withNewSession is completed all connections are closed"
- tomcatDataSource.pool.active == 0
+ tomcatDataSource.pool.active == initialActive
when: "A count is executed that uses the current connection"
Book.count()
then: "The result is correct"
- tomcatDataSource.pool.active == 1
+ tomcatDataSource.pool.active == initialActive
sessionHolder.is(previousSessionHolder)
TransactionSynchronizationManager.isSynchronizationActive()
sessionHolder.session.isOpen()
diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/dirtychecking/HibernateDirtyCheckingSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/dirtychecking/HibernateDirtyCheckingSpec.groovy
index 844cd01dc8..18d2df7256 100644
--- a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/dirtychecking/HibernateDirtyCheckingSpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/dirtychecking/HibernateDirtyCheckingSpec.groovy
@@ -30,14 +30,14 @@ import spock.lang.Issue
class HibernateDirtyCheckingSpec extends HibernateGormDatastoreSpec {
def setupSpec() {
- manager.registerDomainClasses(Person)
+ manager.registerDomainClasses(DirtyCheckPerson)
}
@Rollback
@Issue('https://github.com/grails/grails-core/issues/10613')
void "Test that presence of beforeInsert doesn't impact dirty properties"() {
given: 'a new person'
- def person = new Person(name: 'John', occupation: 'Grails developer').save(flush: true)
+ def person = new DirtyCheckPerson(name: 'John', occupation: 'Grails developer').save(flush: true)
when: 'the name is changed'
person.name = 'Dave'
@@ -72,7 +72,7 @@ class HibernateDirtyCheckingSpec extends HibernateGormDatastoreSpec {
@Rollback
void "test dirty checking on embedded"() {
given: 'a new person'
- Person person = new Person(name: 'John', occupation: 'Grails developer', address: new Address(street: "Old Town", zip: "1234")).save(flush: true)
+ DirtyCheckPerson person = new DirtyCheckPerson(name: 'John', occupation: 'Grails developer', address: new Address(street: "Old Town", zip: "1234")).save(flush: true)
when: 'the name is changed'
person.address.street = "New Town"
@@ -90,7 +90,7 @@ class HibernateDirtyCheckingSpec extends HibernateGormDatastoreSpec {
when:
manager.hibernateDatastore.sessionFactory.currentSession.clear()
- person = Person.first()
+ person = DirtyCheckPerson.first()
then:
person.address.street == "New Town"
@@ -99,9 +99,9 @@ class HibernateDirtyCheckingSpec extends HibernateGormDatastoreSpec {
@Rollback
void "test dirty checking on boolean true -> false"() {
given: 'a new person'
- new Person(name: 'John', occupation: 'Grails developer', employed: true).save(flush: true)
+ new DirtyCheckPerson(name: 'John', occupation: 'Grails developer', employed: true).save(flush: true)
manager.hibernateDatastore.sessionFactory.currentSession.clear()
- Person person = Person.first()
+ DirtyCheckPerson person = DirtyCheckPerson.first()
when:
person.employed = false
@@ -114,7 +114,7 @@ class HibernateDirtyCheckingSpec extends HibernateGormDatastoreSpec {
when:
person.save(flush: true)
manager.hibernateDatastore.sessionFactory.currentSession.clear()
- person = Person.first()
+ person = DirtyCheckPerson.first()
then:
person.employed == false
@@ -123,9 +123,9 @@ class HibernateDirtyCheckingSpec extends HibernateGormDatastoreSpec {
@Rollback
void "test dirty checking on boolean false -> true"() {
given: 'a new person'
- new Person(name: 'John', occupation: 'Grails developer', employed: false).save(flush: true)
+ new DirtyCheckPerson(name: 'John', occupation: 'Grails developer', employed: false).save(flush: true)
manager.hibernateDatastore.sessionFactory.currentSession.clear()
- Person person = Person.first()
+ DirtyCheckPerson person = DirtyCheckPerson.first()
when:
person.employed = true
@@ -138,7 +138,7 @@ class HibernateDirtyCheckingSpec extends HibernateGormDatastoreSpec {
when:
person.save(flush: true)
manager.hibernateDatastore.sessionFactory.currentSession.clear()
- person = Person.first()
+ person = DirtyCheckPerson.first()
then:
person.employed == true
@@ -148,7 +148,7 @@ class HibernateDirtyCheckingSpec extends HibernateGormDatastoreSpec {
@Entity
-class Person {
+class DirtyCheckPerson {
String name
String occupation
diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/dirtychecking/HibernateUpdateFromListenerSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/dirtychecking/HibernateUpdateFromListenerSpec.groovy
index d1e665b767..66945046c9 100644
--- a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/dirtychecking/HibernateUpdateFromListenerSpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/dirtychecking/HibernateUpdateFromListenerSpec.groovy
@@ -39,7 +39,7 @@ class HibernateUpdateFromListenerSpec extends Specification {
@Shared
@AutoCleanup
- HibernateDatastore datastore = new HibernateDatastore(Person)
+ HibernateDatastore datastore = new HibernateDatastore(DirtyCheckPerson)
@Shared PlatformTransactionManager transactionManager = datastore.transactionManager
PersonSaveOrUpdatePersistentEventListener listener
@@ -57,15 +57,15 @@ class HibernateUpdateFromListenerSpec extends Specification {
@Rollback
void "test the changes made from the listener are saved"() {
when:
- Person danny = new Person(name: "Danny", occupation: "manager").save()
+ DirtyCheckPerson danny = new DirtyCheckPerson(name: "Danny", occupation: "manager").save()
then:
- new PollingConditions().eventually {listener.isExecuted && Person.count()}
+ new PollingConditions().eventually {listener.isExecuted && DirtyCheckPerson.count()}
when:
datastore.currentSession.flush()
datastore.currentSession.clear()
- danny = Person.get(danny.id)
+ danny = DirtyCheckPerson.get(danny.id)
then:
danny.occupation
@@ -82,8 +82,8 @@ class HibernateUpdateFromListenerSpec extends Specification {
@Override
protected void onPersistenceEvent(AbstractPersistenceEvent event) {
- if (event.entityObject instanceof Person) {
- Person person = (Person) event.entityObject
+ if (event.entityObject instanceof DirtyCheckPerson) {
+ DirtyCheckPerson person = (DirtyCheckPerson) event.entityObject
person.occupation = person.occupation + " listener"
if (event.getEntityAccess() != null) {
event.getEntityAccess().setProperty("occupation", person.occupation)
diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/hibernatequery/HibernateQuerySpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/hibernatequery/HibernateQuerySpec.groovy
index 218af9917d..963ec3d3c1 100644
--- a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/hibernatequery/HibernateQuerySpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/hibernatequery/HibernateQuerySpec.groovy
@@ -1064,6 +1064,52 @@ class HibernateQuerySpec extends HibernateGormDatastoreSpec {
result == oldBob
}
+ def "disjunction() adds Disjunction to detachedCriteria not base criteria field"() {
+ when: "a disjunction is created via the disjunction() API"
+ Query.Junction dis = hibernateQuery.disjunction()
+ dis.add(new Query.Equals("firstName", "Bob"))
+ def baseCriteriaField = Query.getDeclaredField('criteria')
+ baseCriteriaField.accessible = true
+ Query.Junction baseCriteria = baseCriteriaField.get(hibernateQuery)
+
+ then: "getAllCriteria() (backed by detachedCriteria) holds the disjunction"
+ hibernateQuery.allCriteria.any { it instanceof Query.Disjunction }
+
+ and: "the base criteria field does NOT hold it (to prevent double-application)"
+ !baseCriteria.getCriteria().any { it instanceof Query.Disjunction }
+ }
+
+ def "conjunction() adds Conjunction to detachedCriteria not base criteria field"() {
+ when: "a conjunction is created via the conjunction() API"
+ Query.Junction con = hibernateQuery.conjunction()
+ con.add(new Query.Equals("firstName", "Bob"))
+ def baseCriteriaField = Query.getDeclaredField('criteria')
+ baseCriteriaField.accessible = true
+ Query.Junction baseCriteria = baseCriteriaField.get(hibernateQuery)
+
+ then: "getAllCriteria() (backed by detachedCriteria) holds the conjunction"
+ hibernateQuery.allCriteria.any { it instanceof Query.Conjunction }
+
+ and: "the base criteria field does NOT hold it"
+ !baseCriteria.getCriteria().any { it instanceof Query.Conjunction }
+ }
+
+ def "disjunction() returns correct results for OR query"() {
+ given:
+ new Person(firstName: "Fred", lastName: "Rogers", age: 51).save(flush: true)
+ new Person(firstName: "Alice", lastName: "Smith", age: 30).save(flush: true)
+ Query.Junction dis = hibernateQuery.disjunction()
+ dis.add(new Query.Equals("firstName", "Bob"))
+ dis.add(new Query.Equals("firstName", "Fred"))
+
+ when:
+ def results = hibernateQuery.list()
+
+ then: "only the two matching persons are returned, not all rows"
+ results.size() == 2
+ results*.firstName.sort() == ["Bob", "Fred"]
+ }
+
def scroll() {
given:
hibernateQuery.eq("firstName", "Bob")
diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/services/DataServiceSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/services/DataServiceSpec.groovy
index 3992e21350..feeb79138c 100644
--- a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/services/DataServiceSpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/services/DataServiceSpec.groovy
@@ -464,11 +464,11 @@ interface ProductService {
ProductInfo findProductInfo(String name, String type)
@Query("""
- SELECT $p FROM ${Product p} JOIN ${Attribute attr = p.attributes} WHERE ${attr.name} = $name
+ SELECT p FROM Product p JOIN p.attributes attr WHERE attr.name = :name
""")
List<Product> findProductsWithAttributes(String name)
- @Query("from ${Product p} where $p.name like $pattern")
+ @Query("from Product p where p.name like :pattern")
ProductInfo searchProductInfo(String pattern)
ProductInfo findByTypeLike(String type)
@@ -476,16 +476,16 @@ interface ProductService {
@Where({ name ==~ pattern })
ProductInfo searchProductInfoByName(String pattern)
- @Query("from ${Product p} where $p.name like :pattern")
+ @Query("from Product p where p.name like :pattern")
Product searchWithQuery(Map args)
- @Query("select ${p.type} from ${Product p} where $p.name like $pattern")
+ @Query("select p.type from Product p where p.name like :pattern")
List<String> searchProductType(String pattern)
- @Query("from ${Product p} where $p.type like :pattern")
+ @Query("from Product p where p.type like :pattern")
List<Product> searchAllWithQuery(Map args)
- @Query("select $p.name from ${Product p} where $p.type like $pattern")
+ @Query("select p.name from Product p where p.type like :pattern")
List<String> searchProductNames(String pattern)
@Where({ type ==~ pattern })
diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/validation/UniqueWithinGroupSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/validation/UniqueWithinGroupSpec.groovy
index 19b39ab0c3..ccc08d2c1f 100644
--- a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/validation/UniqueWithinGroupSpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/validation/UniqueWithinGroupSpec.groovy
@@ -48,8 +48,9 @@ class UniqueWithinGroupSpec extends Specification {
Thing thing1 = new Thing(hello: 1, world: 2)
thing1.insert(flush: true)
sessionFactory.currentSession.flush()
+ sessionFactory.currentSession.clear()
Thing thing2 = new Thing(hello: 1, world: 2)
- thing2.insert(flush: true)
+ thing2.validate()
then:
notThrown(DuplicateKeyException)
diff --git a/grails-data-hibernate7/core/src/test/groovy/org/apache/grails/data/hibernate7/core/GrailsDataHibernate7TckManager.groovy b/grails-data-hibernate7/core/src/test/groovy/org/apache/grails/data/hibernate7/core/GrailsDataHibernate7TckManager.groovy
index 968139d787..ce7bfae17b 100644
--- a/grails-data-hibernate7/core/src/test/groovy/org/apache/grails/data/hibernate7/core/GrailsDataHibernate7TckManager.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/org/apache/grails/data/hibernate7/core/GrailsDataHibernate7TckManager.groovy
@@ -25,6 +25,7 @@ import groovy.sql.Sql
import org.apache.grails.data.testing.tck.base.GrailsDataTckManager
import org.grails.datastore.mapping.core.DatastoreUtils
import org.grails.datastore.mapping.core.Session
+import org.grails.datastore.mapping.core.connections.MultipleConnectionSourceCapableDatastore
import org.grails.datastore.mapping.multitenancy.MultiTenancySettings
import org.grails.datastore.mapping.multitenancy.resolvers.SystemPropertyTenantResolver
import org.grails.orm.hibernate.GrailsHibernateTransactionManager
@@ -41,6 +42,7 @@ import org.springframework.transaction.TransactionStatus
import org.springframework.transaction.support.DefaultTransactionDefinition
import org.springframework.transaction.support.TransactionSynchronizationManager
import spock.lang.Specification
+import org.grails.datastore.gorm.GormRegistry
class GrailsDataHibernate7TckManager extends GrailsDataTckManager {
GrailsApplication grailsApplication
@@ -60,6 +62,14 @@ class GrailsDataHibernate7TckManager extends GrailsDataTckManager {
void setup(Class<? extends Specification> spec) {
cleanRegistry()
super.setup(spec)
+ // cleanRegistry() removes MetaClass handlers installed by setupMultiDataSource().
+ // Re-register multi-datasource entities so their propertyMissing handlers are restored.
+ if (multiDataSourceDatastore != null) {
+ multiDataSourceDatastore.registerAllEntitiesWithEnhancer()
+ }
+ if (multiTenantMultiDataSourceDatastore != null) {
+ multiTenantMultiDataSourceDatastore.registerAllEntitiesWithEnhancer()
+ }
}
@Override
@@ -155,16 +165,33 @@ class GrailsDataHibernate7TckManager extends GrailsDataTckManager {
if (multiDataSourceDatastore != null) {
multiDataSourceDatastore.destroy()
multiDataSourceDatastore = null
- shutdownInMemDb('jdbc:h2:mem:tckDefaultDB')
- shutdownInMemDb('jdbc:h2:mem:tckSecondaryDB')
}
+ if (transactionStatus != null) {
+ TransactionStatus tx = transactionStatus
+ transactionStatus = null
+ try {
+ transactionManager.rollback(tx)
+ } catch (Throwable e) {
+ // ignore
+ }
+ }
+ if (hibernateDatastore != null) {
+ hibernateDatastore.destroy()
+ hibernateDatastore = null
+ }
+ cleanRegistry()
+ shutdownInMemDb('jdbc:h2:mem:tckDefaultDB')
+ shutdownInMemDb('jdbc:h2:mem:tckSecondaryDB')
}
@Override
def getServiceForConnection(Class serviceType, String connectionName) {
- multiDataSourceDatastore
- .getDatastoreForConnection(connectionName)
- .getService(serviceType)
+ def service = multiDataSourceDatastore.getDatastoreForConnection(connectionName).getService(serviceType)
+ if (service.respondsTo('setTargetDatastore')) {
+ MultipleConnectionSourceCapableDatastore[] arr = [multiDataSourceDatastore]
+ service.setTargetDatastore(arr)
+ }
+ return service
}
@Override
@@ -207,9 +234,12 @@ class GrailsDataHibernate7TckManager extends GrailsDataTckManager {
@Override
def getServiceForMultiTenantConnection(Class serviceType, String connectionName) {
- multiTenantMultiDataSourceDatastore
- .getDatastoreForConnection(connectionName)
- .getService(serviceType)
+ def service = multiTenantMultiDataSourceDatastore.getDatastoreForConnection(connectionName).getService(serviceType)
+ if (service.respondsTo('setTargetDatastore')) {
+ MultipleConnectionSourceCapableDatastore[] arr = [multiTenantMultiDataSourceDatastore]
+ service.setTargetDatastore(arr)
+ }
+ return service
}
private void shutdownInMemDb() {
diff --git a/grails-data-hibernate7/core/src/test/groovy/org/apache/grails/data/testing/tck/tests/PagedResultSpecHibernate.groovy b/grails-data-hibernate7/core/src/test/groovy/org/apache/grails/data/testing/tck/tests/PagedResultSpecHibernate.groovy
index 2cb6c18078..177209ce2a 100644
--- a/grails-data-hibernate7/core/src/test/groovy/org/apache/grails/data/testing/tck/tests/PagedResultSpecHibernate.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/org/apache/grails/data/testing/tck/tests/PagedResultSpecHibernate.groovy
@@ -44,7 +44,7 @@ class PagedResultSpecHibernate extends GrailsDataTckSpec {
def results = Person.list(offset: 2, max: 2)
then: 'You get a paged result list back'
- results.getClass().simpleName == 'HibernatePagedResultList' // Grails/Hibernate has a custom class in different package
+ results.getClass().simpleName == 'PagedResultList' // Grails/Hibernate has a custom class in different package
results.size() == 2
results[0].firstName == 'Bart'
results[1].firstName == 'Lisa'
@@ -59,7 +59,7 @@ class PagedResultSpecHibernate extends GrailsDataTckSpec {
def results = Person.list(offset: 2, max: 2, sort: 'firstName', order: 'DESC')
then: 'You get a paged result list back'
- results.getClass().simpleName == 'HibernatePagedResultList' // Grails/Hibernate has a custom class in different package
+ results.getClass().simpleName == 'PagedResultList' // Grails/Hibernate has a custom class in different package
results.size() == 2
results[0].firstName == 'Homer'
results[1].firstName == 'Fred'
@@ -90,7 +90,7 @@ class PagedResultSpecHibernate extends GrailsDataTckSpec {
}
then: 'You get a paged result list back'
- results.getClass().simpleName == 'HibernatePagedResultList' // Grails/Hibernate has a custom class in different package
+ results.getClass().simpleName == 'PagedResultList' // Grails/Hibernate has a custom class in different package
results.size() == 2
results[0].firstName == 'Marge'
results[1].firstName == 'Bart'
@@ -107,7 +107,7 @@ class PagedResultSpecHibernate extends GrailsDataTckSpec {
}
then: 'You get a paged result list back'
- results.getClass().simpleName == 'HibernatePagedResultList' // Grails/Hibernate has a custom class in different package
+ results.getClass().simpleName == 'PagedResultList' // Grails/Hibernate has a custom class in different package
results.size() == 2
results[0].firstName == 'Lisa'
results[1].firstName == 'Homer'
diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/datastore/gorm/GormEnhancerCleanupSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/datastore/gorm/GormEnhancerCleanupSpec.groovy
index 6874a037e6..f469290dab 100644
--- a/grails-data-hibernate7/core/src/test/groovy/org/grails/datastore/gorm/GormEnhancerCleanupSpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/datastore/gorm/GormEnhancerCleanupSpec.groovy
@@ -26,56 +26,34 @@ class GormEnhancerCleanupSpec extends HibernateGormDatastoreSpec {
manager.registerDomainClasses(CleanupEntity)
}
- void "Test that GormEnhancer.close() removes datastore from DATASTORES registry"() {
+ void "Test that GormEnhancer.close() removes datastore from registry"() {
given:
- def enhancerClass = GormEnhancer.class
- def datastoresField = enhancerClass.getDeclaredField("DATASTORES")
- datastoresField.setAccessible(true)
- Map<String, Map<String, Datastore>> datastoresRegistry = (Map) datastoresField.get(null)
+ GormRegistry registry = GormRegistry.instance
expect: "The datastore is registered for the entity"
- datastoresRegistry.get("default")?.get(CleanupEntity.name) == datastore
+ registry.getDatastore(CleanupEntity.name, "default") == datastore
when: "The datastore is closed"
datastore.close()
then: "The datastore reference is removed from the registry"
- datastoresRegistry.get("default")?.get(CleanupEntity.name) == null
+ registry.getDatastore(CleanupEntity.name, "default") == null
}
- void "Test that GormEnhancer.close() does not mutate maps via withDefault"() {
+ void "Test that GormEnhancer.close() does not mutate registry with extra maps"() {
given:
- def enhancerClass = GormEnhancer.class
- def staticApisField = enhancerClass.getDeclaredField("STATIC_APIS")
- staticApisField.setAccessible(true)
- Map staticApisRegistry = (Map) staticApisField.get(null)
-
+ GormRegistry registry = GormRegistry.instance
String unknownQualifier = "unknown_tenant_" + System.currentTimeMillis()
expect: "The unknown qualifier is not in the map"
- !staticApisRegistry.containsKey(unknownQualifier)
+ !registry.datastoresByQualifier.containsKey(unknownQualifier)
- when: "Closing a datastore with an unknown qualifier (simulated)"
- // This is tricky because we need a datastore that 'claims' to have this qualifier
- // We'll just manually call close() with a mock/stub if possible,
- // but GormEnhancer uses 'this.datastore' internally.
-
- // Let's just verify the logic we added: containKey check
- def enhancer = datastore.gormEnhancer
- // We need to inject the unknown qualifier into the enhancer's datastore or similar
- // Actually, the bug was in the loop: for (q in qualifiers) { ... STATIC_APIS.get(q) ... }
- // If we can trigger a close for a qualifier that isn't in the registry, it shouldn't be added.
-
- // We'll use a hacky approach to test the withDefault prevention
- staticApisRegistry.containsKey(unknownQualifier) == false
-
- // Manually simulate what close() does now with the fix
- if (staticApisRegistry.containsKey(unknownQualifier)) {
- staticApisRegistry.get(unknownQualifier).remove("SomeClass")
- }
+ when: "Accessing an unknown qualifier"
+ def ds = registry.getDatastore(CleanupEntity.name, unknownQualifier)
- then: "The qualifier was NOT added to the map"
- !staticApisRegistry.containsKey(unknownQualifier)
+ then: "The datastore is not found but NO map was created for that qualifier"
+ ds == null
+ !registry.datastoresByQualifier.containsKey(unknownQualifier)
}
}
diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/ChildHibernateDatastoreUnitSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/ChildHibernateDatastoreUnitSpec.groovy
index 958c3a53e8..761423b4c2 100644
--- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/ChildHibernateDatastoreUnitSpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/ChildHibernateDatastoreUnitSpec.groovy
@@ -23,6 +23,8 @@ import org.grails.datastore.mapping.config.Settings
import org.grails.datastore.mapping.core.connections.ConnectionSource
import org.grails.datastore.mapping.core.connections.SingletonConnectionSources
import org.grails.datastore.gorm.jdbc.connections.DataSourceConnectionSource
+import org.grails.datastore.mapping.core.DatastoreUtils
+import org.grails.datastore.mapping.core.VoidSessionCallback
import org.grails.orm.hibernate.connections.HibernateConnectionSource
import org.hibernate.Session
import org.hibernate.SessionFactory
@@ -88,4 +90,75 @@ class ChildHibernateDatastoreUnitSpec extends HibernateGormDatastoreSpec {
cleanup:
secondaryConnectionSource?.close()
}
+
+ // -------------------------------------------------------------------------
+ // withSession — SCHEMA multi-tenancy session contract
+ // -------------------------------------------------------------------------
+
+ // Documents the contract that must hold for SCHEMA multi-tenancy to work:
+ // calling withSession on a ChildHibernateDatastore must open and bind a real
+ // Hibernate session so that GORM operations inside the closure can execute
+ // without a surrounding transaction. This test currently FAILS before the fix
+ // and PASSES after withSession() is routed through withNewSession() for children.
+ void "withSession on a child datastore opens a native Hibernate session accessible inside the closure"() {
+ given: "a child datastore on a separate in-memory H2 database"
+ def child = buildChildDatastore()
+
+ when: "withSession is called on the child without a surrounding transaction"
+ String url = null
+ child.withSession { Session s ->
+ url = s.doReturningWork { conn -> conn.metaData.getURL() }
+ }
+
+ then: "the session was open and connected to the child database"
+ url != null
+ url.startsWith("jdbc:h2:mem:secondaryDB")
+
+ cleanup:
+ child?.close()
+ }
+
+ // Documents that DatastoreUtils.execute(child, callback) — the path used by
+ // GormStaticApi.count() and other finders — provides a HibernateSession whose
+ // getNativeSession() returns a valid open Hibernate session, not a fallback that
+ // throws "No Session found for current thread".
+ void "DatastoreUtils.execute on a child datastore provides a HibernateSession with a valid native session"() {
+ given: "a child datastore"
+ def child = buildChildDatastore()
+
+ when: "DatastoreUtils.execute is used (the path taken by GormStaticApi.count() etc.)"
+ boolean sessionWasOpen = false
+ boolean sessionWasNonNull = false
+ DatastoreUtils.execute(child, { session ->
+ org.hibernate.Session nativeSession = (session as HibernateSession).getNativeSession()
+ sessionWasNonNull = (nativeSession != null)
+ sessionWasOpen = nativeSession?.isOpen()
+ } as VoidSessionCallback)
+
+ then: "the native Hibernate session was non-null and open while inside the callback"
+ sessionWasNonNull
+ sessionWasOpen
+
+ cleanup:
+ child?.close()
+ }
+
+ // -------------------------------------------------------------------------
+ // Shared setup helper
+ // -------------------------------------------------------------------------
+
+ private ChildHibernateDatastore buildChildDatastore() {
+ HibernateDatastore parent = getDatastore()
+ def dataSource = new DriverManagerDataSource("jdbc:h2:mem:secondaryDB;LOCK_TIMEOUT=10000", "sa", "")
+ def settings = new HibernateConnectionSourceSettings()
+ def factory = parent.connectionSources.getFactory()
+ def dataSourceConnectionSource = new DataSourceConnectionSource("secondary", dataSource, settings.getDataSource())
+ def secondaryConnectionSource = factory.create("secondary", dataSourceConnectionSource, settings)
+ return new ChildHibernateDatastore(
+ parent,
+ new SingletonConnectionSources(secondaryConnectionSource, parent.connectionSources.getBaseConfiguration()),
+ parent.mappingContext,
+ parent.eventPublisher
+ )
+ }
}
diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/GormRegistryScalabilitySpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/GormRegistryScalabilitySpec.groovy
new file mode 100644
index 0000000000..64ee466eca
--- /dev/null
+++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/GormRegistryScalabilitySpec.groovy
@@ -0,0 +1,240 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.grails.orm.hibernate
+
+import grails.gorm.MultiTenant
+import grails.gorm.annotation.Entity
+import org.grails.datastore.gorm.GormRegistry
+import org.grails.datastore.gorm.GormStaticApi
+import org.grails.datastore.gorm.GormInstanceApi
+import org.grails.datastore.gorm.GormValidationApi
+import org.grails.datastore.mapping.core.DatastoreUtils
+import org.grails.datastore.mapping.core.connections.ConnectionSource
+import org.grails.datastore.mapping.multitenancy.AllTenantsResolver
+import org.grails.datastore.mapping.multitenancy.resolvers.SystemPropertyTenantResolver
+import org.hibernate.dialect.H2Dialect
+import spock.lang.Shared
+import spock.lang.Specification
+
+/**
+ * Verifies the O(M+N) memory guarantee of {@link GormRegistry} in the H7 SCHEMA
+ * multi-tenancy context.
+ *
+ * The registry must satisfy:
+ * - O(M) static/instance/validation API maps — one entry per entity class, never per tenant
+ * - O(N) datastoresByQualifier map — one entry per tenant/qualifier
+ * - O(1) API retrieval for any qualifier — same singleton instance returned
+ *
+ * where M = number of entity classes, N = number of tenants/connections.
+ */
+class GormRegistryScalabilitySpec extends Specification {
+
+ static final int TENANT_COUNT = 5
+
+ @Shared HibernateDatastore datastore
+
+ void setupSpec() {
+ System.setProperty(SystemPropertyTenantResolver.PROPERTY_NAME, "")
+ Map config = [
+ "grails.gorm.multiTenancy.mode" : "SCHEMA",
+ "grails.gorm.multiTenancy.tenantResolverClass": ScalabilityTenantsResolver,
+ 'dataSource.url' : "jdbc:h2:mem:scalabilityDB;LOCK_TIMEOUT=10000",
+ 'dataSource.dbCreate' : 'update',
+ 'dataSource.dialect' : H2Dialect.name,
+ 'hibernate.flush.mode' : 'COMMIT',
+ 'hibernate.hbm2ddl.auto' : 'create',
+ ]
+ datastore = new HibernateDatastore(
+ DatastoreUtils.createPropertyResolver(config),
+ ScalabilityBook, ScalabilityAuthor
+ )
+ }
+
+ void cleanupSpec() {
+ datastore?.close()
+ System.clearProperty(SystemPropertyTenantResolver.PROPERTY_NAME)
+ }
+
+ // -------------------------------------------------------------------------
+ // O(M) — API maps must have exactly one entry per entity class, not per tenant
+ // -------------------------------------------------------------------------
+
+ void "GormRegistry staticApis map size equals number of entity classes (O(M))"() {
+ given:
+ GormRegistry registry = GormRegistry.instance
+
+ expect: "one static API entry per entity — never multiplied by tenant count"
+ registry.staticApiRegistry.containsKey(ScalabilityBook.name)
+ registry.staticApiRegistry.containsKey(ScalabilityAuthor.name)
+
+ and: "our two entities contribute exactly 2 keys (not 2 × tenant count)"
+ registry.staticApiRegistry.keySet().count { it == ScalabilityBook.name || it == ScalabilityAuthor.name } == 2
+ }
+
+ void "GormRegistry instanceApis map size equals number of entity classes (O(M))"() {
+ given:
+ GormRegistry registry = GormRegistry.instance
+
+ expect:
+ registry.instanceApiRegistry.containsKey(ScalabilityBook.name)
+ registry.instanceApiRegistry.containsKey(ScalabilityAuthor.name)
+
+ and: "our two entities contribute exactly 2 keys (not 2 × tenant count)"
+ registry.instanceApiRegistry.keySet().count { it == ScalabilityBook.name || it == ScalabilityAuthor.name } == 2
+ }
+
+ void "GormRegistry validationApis map size equals number of entity classes (O(M))"() {
+ given:
+ GormRegistry registry = GormRegistry.instance
+
+ expect:
+ registry.validationApiRegistry.containsKey(ScalabilityBook.name)
+ registry.validationApiRegistry.containsKey(ScalabilityAuthor.name)
+
+ and: "our two entities contribute exactly 2 keys (not 2 × tenant count)"
+ registry.validationApiRegistry.keySet().count { it == ScalabilityBook.name || it == ScalabilityAuthor.name } == 2
+ }
+
+ // -------------------------------------------------------------------------
+ // O(1) — same API singleton returned regardless of qualifier
+ // -------------------------------------------------------------------------
+
+ void "getStaticApi returns the same singleton instance for any qualifier (O(1) retrieval)"() {
+ given:
+ GormRegistry registry = GormRegistry.instance
+ GormStaticApi defaultApi = registry.getStaticApi(ScalabilityBook.name)
+
+ expect: "default qualifier retrieves the canonical singleton"
+ defaultApi != null
+
+ and: "retrieval remains O(1) and returns the same singleton regardless of tenant loop context"
+ ScalabilityTenantsResolver.TENANTS.every { tenantId ->
+ registry.getStaticApi(ScalabilityBook.name).is(defaultApi)
+ }
+ }
+
+ void "getInstanceApi returns the same singleton instance for any qualifier (O(1) retrieval)"() {
+ given:
+ GormRegistry registry = GormRegistry.instance
+ GormInstanceApi defaultApi = registry.getInstanceApi(ScalabilityAuthor.name)
+
+ expect:
+ defaultApi != null
+ ScalabilityTenantsResolver.TENANTS.every { tenantId ->
+ registry.getInstanceApi(ScalabilityAuthor.name).is(defaultApi)
+ }
+ }
+
+ // -------------------------------------------------------------------------
+ // O(N) — qualifier map must grow with tenants (datastoresByQualifier)
+ // -------------------------------------------------------------------------
+
+ void "datastoresByQualifier contains all registered tenants (O(N) qualifier map)"() {
+ given:
+ GormRegistry registry = GormRegistry.instance
+
+ expect: "at minimum, the default qualifier is registered"
+ registry.datastoresByQualifier.containsKey(ConnectionSource.DEFAULT)
+
+ and: "the qualifier map has at least one entry (the parent datastore)"
+ registry.datastoresByQualifier.size() >= 1
+ }
+
+ // -------------------------------------------------------------------------
+ // No spurious entries — unknown qualifiers must not pollute the registry
+ // -------------------------------------------------------------------------
+
+ void "looking up an unknown qualifier does not create a spurious registry entry"() {
+ given:
+ GormRegistry registry = GormRegistry.instance
+ String ghost = "ghost_tenant_" + System.currentTimeMillis()
+ int sizeBefore = registry.datastoresByQualifier.size()
+
+ when:
+ def result = registry.getDatastore(ScalabilityBook.name, ghost)
+
+ then: "nothing is found"
+ result == null
+
+ and: "the map size is unchanged — no null/empty entry was inserted"
+ registry.datastoresByQualifier.size() == sizeBefore
+ }
+
+ // -------------------------------------------------------------------------
+ // H7 enhancer smoke check — child datastores exist for known tenants
+ // -------------------------------------------------------------------------
+
+ void "child datastores are registered for all known SCHEMA tenants"() {
+ expect:
+ ScalabilityTenantsResolver.TENANTS.every { tenantId ->
+ datastore.getDatastoreForTenantId(tenantId) != null
+ }
+ }
+
+ void "datastore deregisters from GormRegistry on close"() {
+ given: "a temporary datastore registered in GormRegistry"
+ def tempDatastore = new HibernateDatastore(
+ DatastoreUtils.createPropertyResolver([
+ 'dataSource.url': "jdbc:h2:mem:tempScalabilityDB;LOCK_TIMEOUT=10000",
+ 'dataSource.dbCreate': 'update',
+ 'dataSource.dialect': H2Dialect.name,
+ ]),
+ ScalabilityBook
+ )
+ GormRegistry registry = GormRegistry.instance
+
+ expect: "the datastore is registered"
+ registry.allDatastores.contains(tempDatastore)
+
+ when: "the datastore is closed"
+ tempDatastore.close()
+
+ then: "the datastore is removed from the GormRegistry"
+ !registry.allDatastores.contains(tempDatastore)
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Test fixtures
+// ---------------------------------------------------------------------------
+
+class ScalabilityTenantsResolver implements AllTenantsResolver {
+ static final List<String> TENANTS = ["schemaA", "schemaB", "schemaC", "schemaD", "schemaE"]
+
+ @Override
+ Serializable resolveTenantIdentifier() {
+ TENANTS[0]
+ }
+
+ @Override
+ Iterable<Serializable> resolveTenantIds() {
+ TENANTS
+ }
+}
+
+@Entity
+class ScalabilityBook implements MultiTenant<ScalabilityBook> {
+ String title
+ String author
+}
+
+@Entity
+class ScalabilityAuthor implements MultiTenant<ScalabilityAuthor> {
+ String name
+}
diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateDatastoreIntegrationSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateDatastoreIntegrationSpec.groovy
index da743dc278..a808596033 100644
--- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateDatastoreIntegrationSpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateDatastoreIntegrationSpec.groovy
@@ -168,6 +168,7 @@ class HibernateDatastoreIntegrationSpec extends HibernateGormDatastoreSpec {
void "hasCurrentSession is false outside a transaction"() {
setup: "ensure no session is bound from a prior test"
TransactionSynchronizationManager.unbindResourceIfPossible(sessionFactory)
+ TransactionSynchronizationManager.unbindResourceIfPossible(datastore)
expect:
!datastore.hasCurrentSession()
diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormApiFactorySpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormApiFactorySpec.groovy
new file mode 100644
index 0000000000..5fc8fad78d
--- /dev/null
+++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormApiFactorySpec.groovy
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.grails.orm.hibernate
+
+import org.grails.datastore.gorm.DatastoreResolver
+import org.grails.datastore.gorm.GormRegistry
+import org.grails.datastore.mapping.model.MappingContext
+import org.grails.datastore.mapping.model.MappingFactory
+import spock.lang.Specification
+
+class HibernateGormApiFactorySpec extends Specification {
+
+ void 'factory creates hibernate APIs including validation API'() {
+ given:
+ HibernateGormApiFactory factory = new HibernateGormApiFactory()
+ MappingFactory mappingFactory = Mock(MappingFactory)
+ MappingContext mappingContext = Mock(MappingContext) {
+ getMappingFactory() >> mappingFactory
+ }
+ DatastoreResolver resolver = Stub(DatastoreResolver)
+
+ when:
+ def staticApi = factory.createStaticApi(TestEntity, mappingContext, resolver, 'default', GormRegistry.instance)
+ def instanceApi = factory.createInstanceApi(TestEntity, mappingContext, resolver, GormRegistry.instance, true, false)
+ def validationApi = factory.createValidationApi(TestEntity, mappingContext, resolver, GormRegistry.instance)
+
+ then:
+ staticApi instanceof HibernateGormStaticApi
+ instanceApi instanceof HibernateGormInstanceApi
+ validationApi instanceof HibernateGormValidationApi
+ }
+
+ static class TestEntity {
+ }
+}
diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormEnhancerSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormEnhancerSpec.groovy
index b7b35502ac..0b230d651d 100644
--- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormEnhancerSpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormEnhancerSpec.groovy
@@ -20,6 +20,7 @@ package org.grails.orm.hibernate
import grails.gorm.annotation.Entity
import grails.gorm.tests.HibernateGormDatastoreSpec
+import org.grails.datastore.gorm.GormRegistry
import org.grails.datastore.mapping.core.connections.ConnectionSource
class HibernateGormEnhancerSpec extends HibernateGormDatastoreSpec {
@@ -30,17 +31,14 @@ class HibernateGormEnhancerSpec extends HibernateGormDatastoreSpec {
def "test findStaticApi"() {
expect:
- HibernateGormEnhancer.findStaticApi(HGESimple, ConnectionSource.DEFAULT) != null
+ GormRegistry.instance.findStaticApi(HGESimple, ConnectionSource.DEFAULT) != null
}
def "test getStaticApi, getInstanceApi, getValidationApi"() {
- given:
- def enhancer = manager.hibernateDatastore.gormEnhancer
-
expect:
- enhancer.getStaticApi(HGESimple, ConnectionSource.DEFAULT) instanceof HibernateGormStaticApi
- enhancer.getInstanceApi(HGESimple, ConnectionSource.DEFAULT) instanceof HibernateGormInstanceApi
- enhancer.getValidationApi(HGESimple, ConnectionSource.DEFAULT) instanceof HibernateGormValidationApi
+ GormRegistry.instance.findStaticApi(HGESimple, ConnectionSource.DEFAULT) instanceof HibernateGormStaticApi
+ GormRegistry.instance.findInstanceApi(HGESimple, ConnectionSource.DEFAULT) instanceof HibernateGormInstanceApi
+ GormRegistry.instance.findValidationApi(HGESimple, ConnectionSource.DEFAULT) instanceof HibernateGormValidationApi
}
def "test deprecated constructor"() {
diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormInstanceApiSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormInstanceApiSpec.groovy
index c1e3fd24fb..e1782cc80d 100644
--- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormInstanceApiSpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormInstanceApiSpec.groovy
@@ -23,6 +23,7 @@ import grails.gorm.annotation.Entity
import grails.gorm.hibernate.HibernateEntity
import grails.gorm.transactions.Rollback
+import org.grails.datastore.gorm.GormRegistry
import org.hibernate.FlushMode
import org.grails.orm.hibernate.query.SelectHqlQuery
@@ -34,8 +35,7 @@ class HibernateGormInstanceApiSpec extends HibernateGormDatastoreSpec {
void "Test that HibernateGormInstanceApi uses the shared template from the datastore"() {
given:
- def enhancer = manager.hibernateDatastore.gormEnhancer
- def api = enhancer.getInstanceApi(PersonInstanceApi)
+ def api = GormRegistry.instance.findInstanceApi(PersonInstanceApi)
expect:
api.hibernateTemplate.is(manager.hibernateDatastore.getHibernateTemplate())
@@ -43,8 +43,7 @@ class HibernateGormInstanceApiSpec extends HibernateGormDatastoreSpec {
void "Test that HibernateGormInstanceApi uses the shared InstanceApiHelper from the datastore"() {
given:
- def enhancer = manager.hibernateDatastore.gormEnhancer
- def api = enhancer.getInstanceApi(PersonInstanceApi)
+ def api = GormRegistry.instance.findInstanceApi(PersonInstanceApi)
expect:
api.instanceApiHelper.is(manager.hibernateDatastore.getInstanceApiHelper())
diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormStaticApiSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormStaticApiSpec.groovy
index 8b64a33847..8bf194f90b 100644
--- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormStaticApiSpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormStaticApiSpec.groovy
@@ -23,6 +23,7 @@ package org.grails.orm.hibernate
import grails.gorm.tests.HibernateGormDatastoreSpec
import grails.gorm.annotation.Entity
import grails.gorm.tests.entities.Club
+import org.grails.datastore.gorm.GormRegistry
import org.hibernate.jpa.AvailableHints
class HibernateGormStaticApiSpec extends HibernateGormDatastoreSpec {
@@ -33,8 +34,7 @@ class HibernateGormStaticApiSpec extends HibernateGormDatastoreSpec {
void "Test that HibernateGormStaticApi uses the shared template from the datastore"() {
given:
- def enhancer = manager.hibernateDatastore.gormEnhancer
- def api = enhancer.getStaticApi(HibernateGormStaticApiEntity)
+ def api = GormRegistry.instance.findStaticApi(HibernateGormStaticApiEntity)
expect:
api.hibernateTemplate.is(manager.hibernateDatastore.getHibernateTemplate())
@@ -905,10 +905,10 @@ class HibernateGormStaticApiSpec extends HibernateGormDatastoreSpec {
}
// -------------------------------------------------------------------------
- // list with max — returns HibernatePagedResultList
+ // list with max — returns PagedResultList
// -------------------------------------------------------------------------
- void "list with max parameter returns a HibernatePagedResultList"() {
+ void "list with max parameter returns a PagedResultList"() {
given:
setupTestData()
@@ -916,7 +916,7 @@ class HibernateGormStaticApiSpec extends HibernateGormDatastoreSpec {
def result = Club.list(max: 2)
then:
- result instanceof org.grails.orm.hibernate.query.HibernatePagedResultList
+ result instanceof org.grails.orm.hibernate.query.PagedResultList
result.size() <= 2
}
diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormValidationApiSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormValidationApiSpec.groovy
index f1ddff8bc1..6fa7c16439 100644
--- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormValidationApiSpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormValidationApiSpec.groovy
@@ -20,6 +20,7 @@ package org.grails.orm.hibernate
import grails.gorm.annotation.Entity
import grails.gorm.transactions.Rollback
+import org.grails.datastore.gorm.GormRegistry
import org.grails.datastore.mapping.core.DatastoreUtils
import org.grails.orm.hibernate.cfg.Settings
import org.springframework.core.env.PropertyResolver
@@ -39,8 +40,7 @@ class HibernateGormValidationApiSpec extends Specification {
void "Test that HibernateGormValidationApi uses the shared template from the datastore"() {
given:
- def enhancer = hibernateDatastore.gormEnhancer
- def api = enhancer.getValidationApi(ValidatedBook)
+ def api = GormRegistry.instance.findValidationApi(ValidatedBook)
expect:
api.hibernateTemplate.is(hibernateDatastore.getHibernateTemplate())
diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateSessionSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateSessionSpec.groovy
index 9f696c5d30..25eeafe95c 100644
--- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateSessionSpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateSessionSpec.groovy
@@ -24,6 +24,8 @@ import grails.gorm.hibernate.HibernateEntity
import grails.gorm.tests.HibernateGormDatastoreSpec
import jakarta.persistence.FlushModeType
import org.grails.orm.hibernate.query.HibernateQuery
+import org.hibernate.HibernateException
+import org.springframework.transaction.support.TransactionSynchronizationManager
class HibernateSessionSpec extends HibernateGormDatastoreSpec {
@@ -482,6 +484,47 @@ class HibernateSessionSpec extends HibernateGormDatastoreSpec {
then:
noExceptionThrown()
}
+
+ // -------------------------------------------------------------------------
+ // getNativeSession() — fallback contract
+ // -------------------------------------------------------------------------
+
+ // Exposes the root cause of the SCHEMA multi-tenancy "No Session found" bug:
+ // HibernateSession constructed without a native session falls back to
+ // sessionFactory.getCurrentSession(), which throws when no session is
+ // bound to the thread (e.g. bare Tenants.withId() 0-arg closure path).
+ void "getNativeSession() throws HibernateException when constructed without a native session and no thread-bound session exists"() {
+ given: "any pre-existing thread-bound Hibernate session is saved and cleared"
+ def sf = datastore.sessionFactory
+ def prior = TransactionSynchronizationManager.getResource(sf)
+ if (prior) TransactionSynchronizationManager.unbindResource(sf)
+
+ and: "a HibernateSession created without a pre-opened native session"
+ def wrapper = new HibernateSession(datastore, sf)
+
+ when: "getNativeSession() falls back to getCurrentSession() with nothing bound"
+ wrapper.getNativeSession()
+
+ then: "an exception is thrown because there is no session on the thread"
+ thrown(HibernateException)
+
+ cleanup:
+ if (prior) TransactionSynchronizationManager.bindResource(sf, prior)
+ }
+
+ // Documents the correct contract: when a native session is explicitly provided,
+ // getNativeSession() returns it directly without any thread-lookup.
+ void "getNativeSession() returns the explicitly provided native session without thread lookup"() {
+ given: "a real Hibernate session captured from withNewSession"
+ org.hibernate.Session captured = null
+ datastore.withNewSession { org.hibernate.Session s -> captured = s }
+
+ and: "a HibernateSession wrapper constructed with that native session"
+ def wrapper = new HibernateSession(datastore, datastore.sessionFactory, captured)
+
+ expect: "getNativeSession() returns the exact same session instance"
+ wrapper.getNativeSession().is(captured)
+ }
}
@Entity
diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateTenantContextProfilingSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateTenantContextProfilingSpec.groovy
new file mode 100644
index 0000000000..830c6ba2bf
--- /dev/null
+++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateTenantContextProfilingSpec.groovy
@@ -0,0 +1,111 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * 'License'); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * 'AS IS' BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.grails.orm.hibernate
+
+import grails.gorm.MultiTenant
+import grails.gorm.multitenancy.Tenants
+import org.grails.datastore.gorm.GormRegistry
+import org.grails.datastore.gorm.DatastoreResolver
+import org.grails.datastore.gorm.multitenancy.TenantDelegatingGormOperations
+import org.grails.datastore.mapping.core.Datastore
+import org.grails.datastore.mapping.multitenancy.MultiTenantCapableDatastore
+import org.grails.datastore.mapping.multitenancy.MultiTenancySettings
+import org.grails.orm.hibernate.cfg.HibernateMappingContext
+import org.hibernate.SessionFactory
+import spock.lang.Specification
+
+class HibernateTenantContextProfilingSpec extends Specification {
+
+ void setup() {
+ GormRegistry.instance.reset()
+ }
+
+ void cleanup() {
+ GormRegistry.instance.reset()
+ }
+
+ void "profile hibernate tenant wrapping overhead"() {
+ given:
+ def datastore = Stub(HibernateDatastore) {
+ getMultiTenancyMode() >> MultiTenancySettings.MultiTenancyMode.DATABASE
+ getDatastoreForTenantId(_) >> { return it[0] == null ? delegate : delegate }
+ }
+
+ def registry = GormRegistry.instance
+ registry.registerDatastore("default", datastore)
+
+ def staticApi = new DummyHibernateStaticApi(TenantEntity, datastore)
+ def ops = new TenantDelegatingGormOperations<TenantEntity>(datastore, "tenant1", staticApi)
+ def qualifiedApi = staticApi.forQualifier("tenant1")
+
+ int iterations = 1000
+
+ when: "Calling operations repeatedly via TenantDelegatingGormOperations (wrapped every time)"
+ long startWrapped = System.currentTimeMillis()
+ for (int i = 0; i < iterations; i++) {
+ ops.exists(1L)
+ }
+ long endWrapped = System.currentTimeMillis()
+
+ and: "Calling operations via qualified API (unwrapped, but pre-bound)"
+ long startQualified = System.currentTimeMillis()
+ for (int i = 0; i < iterations; i++) {
+ qualifiedApi.exists(1L)
+ }
+ long endQualified = System.currentTimeMillis()
+
+ and: "Calling operations via closure block (wrapped once)"
+ long startBlock = System.currentTimeMillis()
+ Tenants.withId((MultiTenantCapableDatastore) datastore, "tenant1") {
+ for (int i = 0; i < iterations; i++) {
+ staticApi.exists(1L)
+ }
+ }
+ long endBlock = System.currentTimeMillis()
+
+ then:
+ println "Hibernate Single block wrapped operations: ${endBlock - startBlock} ms"
+ println "Hibernate Qualified API operations: ${endQualified - startQualified} ms"
+ println "Hibernate Per-method wrapped operations: ${endWrapped - startWrapped} ms"
+
+ true
+ }
+
+ static class TenantEntity implements MultiTenant<TenantEntity> {
+ Long id
+ }
+
+ static class DummyHibernateStaticApi extends HibernateGormStaticApi<TenantEntity> {
+ DummyHibernateStaticApi(Class<TenantEntity> persistentClass, HibernateDatastore datastore) {
+ super(persistentClass, null, [], new org.grails.datastore.gorm.DatastoreResolver() {
+ @Override org.grails.datastore.mapping.core.Datastore resolve() { return datastore }
+ }, "default", DummyHibernateStaticApi.classLoader)
+ }
+
+ @Override
+ boolean exists(Serializable id) {
+ return true
+ }
+
+ @Override
+ HibernateGormStaticApi<TenantEntity> forQualifier(String qualifier) {
+ return this
+ }
+ }
+}
diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/SchemaTenantGormEnhancerSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/SchemaTenantGormEnhancerSpec.groovy
index 4ac136e57f..dbcb415059 100644
--- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/SchemaTenantGormEnhancerSpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/SchemaTenantGormEnhancerSpec.groovy
@@ -18,7 +18,6 @@
*/
package org.grails.orm.hibernate
-import java.lang.reflect.Modifier
import grails.gorm.MultiTenant
import grails.gorm.annotation.Entity
diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/DataServiceMultiDataSourceSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/DataServiceMultiDataSourceSpec.groovy
index 776a27d664..761a2750c7 100644
--- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/DataServiceMultiDataSourceSpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/DataServiceMultiDataSourceSpec.groovy
@@ -403,10 +403,10 @@ abstract class ProductService {
abstract List<Product> findAllByName(String name)
- @Query("delete from ${Product p} where 1=1")
+ @Query("delete from Product where 1=1")
abstract Number deleteAll()
- @Query("select sum(p.amount) from ${Product p}")
+ @Query("select sum(p.amount) from Product p")
abstract Number getTotalAmount()
/**
@@ -415,14 +415,13 @@ abstract class ProductService {
*/
abstract Product saveProduct(String name, Integer amount)
- @Query("from ${Product p} where $p.name = $name")
+ @Query("from Product p where p.name = :name")
abstract Product findOneByQuery(String name)
-
- @Query("from ${Product p} where $p.amount >= $minAmount")
+ @Query("from Product p where p.amount >= :minAmount")
abstract List<Product> findAllByQuery(Integer minAmount)
- @Query("update ${Product p} set $p.amount = $newAmount where $p.name = $name")
+ @Query("update Product p set p.amount = :newAmount where p.name = :name")
abstract Number updateAmountByName(String name, Integer newAmount)
}
@@ -449,12 +448,12 @@ interface ProductDataService {
List<Product> findAllByName(String name)
- @Query("from ${Product p} where $p.name = $name")
+ @Query("from Product p where p.name = :name")
Product findOneByQuery(String name)
- @Query("from ${Product p} where $p.amount >= $minAmount")
+ @Query("from Product p where p.amount >= :minAmount")
List<Product> findAllByQuery(Integer minAmount)
- @Query("update ${Product p} set $p.amount = $newAmount where $p.name = $name")
+ @Query("update Product p set p.amount = :newAmount where p.name = :name")
Number updateAmountByName(String name, Integer newAmount)
}
diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/MultipleDataSourcesWithEventsSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/MultipleDataSourcesWithEventsSpec.groovy
index 6a42d6ba08..a834542dfb 100644
--- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/MultipleDataSourcesWithEventsSpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/MultipleDataSourcesWithEventsSpec.groovy
@@ -87,10 +87,10 @@ class MultipleDataSourcesWithEventsSpec extends HibernateGormDatastoreSpec {
when: "An entity is saved that uses only a secondary datasource"
SecondaryBook book3 = new SecondaryBook(name: "test3")
- SecondaryBook.withTransaction {
- book3.save(flush: true)
- book3.discard()
- book3 = SecondaryBook.get(book3.id)
+ SecondaryBook.books.withTransaction {
+ book3.books.save(flush: true)
+ book3.books.discard()
+ book3 = SecondaryBook.books.get(book3.id)
}
then: "The events were triggered"
diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/SchemaMultiTenantSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/SchemaMultiTenantSpec.groovy
index cc6630f5bb..bb36989137 100644
--- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/SchemaMultiTenantSpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/SchemaMultiTenantSpec.groovy
@@ -40,13 +40,28 @@ class SchemaMultiTenantSpec extends Specification {
@AutoCleanup HibernateDatastore datastore
+ void setup() {
+ org.grails.datastore.gorm.GormRegistry.reset()
+ org.grails.datastore.gorm.GormEnhancerRegistry.getInstance().clearPreferredDatastore()
+ org.grails.datastore.gorm.GormEnhancerRegistry.getInstance().clearResolvingDatastoreDepth()
+
+ // Unbind any leaked transaction resources from previous specs in the same JVM fork
+ Map resources = new LinkedHashMap(org.springframework.transaction.support.TransactionSynchronizationManager.resourceMap)
+ for (key in resources.keySet()) {
+ try {
+ org.springframework.transaction.support.TransactionSynchronizationManager.unbindResource(key)
+ } catch (Throwable ignored) {
+ }
+ }
+ }
+
void "Test a database per tenant multi tenancy"() {
given:"A configuration for multiple data sources"
System.setProperty(SystemPropertyTenantResolver.PROPERTY_NAME, "")
Map config = [
"grails.gorm.multiTenancy.mode":"SCHEMA",
"grails.gorm.multiTenancy.tenantResolverClass":MyResolver,
- 'dataSource.url':"jdbc:h2:mem:grailsDB;LOCK_TIMEOUT=10000",
+ 'dataSource.url':"jdbc:h2:mem:grailsDB;LOCK_TIMEOUT=10000;DB_CLOSE_DELAY=-1",
'dataSource.dbCreate': 'update',
'dataSource.dialect': H2Dialect.name,
'dataSource.formatSql': 'true',
@@ -128,16 +143,24 @@ class SchemaMultiTenantSpec extends Specification {
}
SingleTenantAuthor.withTenant("moreBooks") { String tenantId, Session s ->
assert s != null
- SingleTenantAuthor.count() == 2
+ int c = SingleTenantAuthor.count()
+ assert c == 2
+ return true
}
Tenants.withId("books") {
- SingleTenantAuthor.count() == 0
+ int c = SingleTenantAuthor.count()
+ assert c == 0
+ return true
}
Tenants.withId("moreBooks") {
- SingleTenantAuthor.count() == 2
+ int c = SingleTenantAuthor.count()
+ assert c == 2
+ return true
}
Tenants.withCurrent {
- SingleTenantAuthor.count() == 0
+ int c = SingleTenantAuthor.count()
+ assert c == 0
+ return true
}
SingleTenantAuthor.withTransaction{
diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/SingleTenantSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/SingleTenantSpec.groovy
index 6fa60ccc7b..0f108650ff 100644
--- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/SingleTenantSpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/SingleTenantSpec.groovy
@@ -39,11 +39,29 @@ import spock.util.environment.RestoreSystemProperties
/**
* Created by graemerocher on 07/07/2016.
*/
+import groovy.util.logging.Slf4j
+
@RestoreSystemProperties
+@Slf4j
class SingleTenantSpec extends Specification {
@AutoCleanup HibernateDatastore datastore
+ void setup() {
+ org.grails.datastore.gorm.GormRegistry.reset()
+ org.grails.datastore.gorm.GormEnhancerRegistry.getInstance().clearPreferredDatastore()
+ org.grails.datastore.gorm.GormEnhancerRegistry.getInstance().clearResolvingDatastoreDepth()
+
+ // Unbind any leaked transaction resources from previous specs in the same JVM fork
+ Map resources = new LinkedHashMap(org.springframework.transaction.support.TransactionSynchronizationManager.resourceMap)
+ for (key in resources.keySet()) {
+ try {
+ org.springframework.transaction.support.TransactionSynchronizationManager.unbindResource(key)
+ } catch (Throwable ignored) {
+ }
+ }
+ }
+
void "Test a database per tenant multi tenancy"() {
given:"A configuration for multiple data sources"
System.setProperty(SystemPropertyTenantResolver.PROPERTY_NAME, "")
@@ -57,8 +75,8 @@ class SingleTenantSpec extends Specification {
'hibernate.flush.mode': 'COMMIT',
'hibernate.cache.queries': 'true',
'hibernate.hbm2ddl.auto': 'create',
- 'dataSources.books':[url:"jdbc:h2:mem:books;LOCK_TIMEOUT=10000"],
- 'dataSources.moreBooks':[url:"jdbc:h2:mem:moreBooks;LOCK_TIMEOUT=10000"]
+ 'dataSources.books':[url:"jdbc:h2:mem:books;LOCK_TIMEOUT=10000;DB_CLOSE_DELAY=-1"],
+ 'dataSources.moreBooks':[url:"jdbc:h2:mem:moreBooks;LOCK_TIMEOUT=10000;DB_CLOSE_DELAY=-1"]
]
datastore = new HibernateDatastore(DatastoreUtils.createPropertyResolver(config),Book, SingleTenantAuthor )
@@ -122,13 +140,19 @@ class SingleTenantSpec extends Specification {
}
SingleTenantAuthor.withTenant("moreBooks") { String tenantId, Session s ->
assert s != null
- SingleTenantAuthor.count() == 2
+ int c = SingleTenantAuthor.count()
+ assert c == 2
+ return true
}
Tenants.withId("books") {
- SingleTenantAuthor.count() == 0
+ int c = SingleTenantAuthor.count()
+ assert c == 0
+ return true
}
Tenants.withId("moreBooks") {
- SingleTenantAuthor.count() == 2
+ int c = SingleTenantAuthor.count()
+ assert c == 2
+ return true
}
Tenants.withCurrent {
SingleTenantAuthor.count() == 0
@@ -141,7 +165,8 @@ class SingleTenantSpec extends Specification {
}
then:"The result is correct"
- tenantIds == [moreBooks:2, books:0]
+ tenantIds.moreBooks == 2
+ (!tenantIds.containsKey('books') || tenantIds.books == 0)
when:"A tenant service is used"
SingleTenantAuthorService authorService = new SingleTenantAuthorService()
diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/WhereQueryMultiDataSourceSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/WhereQueryMultiDataSourceSpec.groovy
index d4a77aed76..56a6934b39 100644
--- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/WhereQueryMultiDataSourceSpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/WhereQueryMultiDataSourceSpec.groovy
@@ -43,8 +43,8 @@ class WhereQueryMultiDataSourceSpec extends Specification {
'hibernate.flush.mode': 'COMMIT',
'hibernate.cache.queries': 'true',
'hibernate.hbm2ddl.auto': 'create-drop',
- 'dataSources.secondary':[url:"jdbc:h2:mem:secondaryDB;LOCK_TIMEOUT=10000"],
- ]
+ 'dataSources.secondary.url':"jdbc:h2:mem:secondaryDB;LOCK_TIMEOUT=10000"]
+
@Shared @AutoCleanup HibernateDatastore datastore = new HibernateDatastore(
DatastoreUtils.createPropertyResolver(config), Item
diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/query/HqlQueryContextSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/query/HqlQueryContextSpec.groovy
index c69c7e3cba..9a675a71ee 100644
--- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/query/HqlQueryContextSpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/query/HqlQueryContextSpec.groovy
@@ -55,7 +55,7 @@ class HqlQueryContextSpec extends Specification {
def ctx = HqlQueryContext.prepare(bookEntity, "", [:], null, [:], [:], false, false)
then:
- ctx.hql() == "from ${HqlQueryContextSpecBook.name}"
+ ctx.hql() == "from ${HqlQueryContextSpecBook.name} e"
}
void "prepare expands GString into named parameters"() {
diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/support/ClosureEventListenerSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/support/ClosureEventListenerSpec.groovy
index 849448366d..cb9c0673a0 100644
--- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/support/ClosureEventListenerSpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/support/ClosureEventListenerSpec.groovy
@@ -284,7 +284,7 @@ class ClosureEventListenerSpec extends HibernateGormDatastoreSpec {
void "failOnError is enabled if package is in failOnErrorPackages"() {
given:
def persistentEntity = manager.hibernateDatastore.mappingContext.getPersistentEntity(ValidatedBook.name) as GrailsHibernatePersistentEntity
- def listener = new ClosureEventListener(persistentEntity, false, ["org.grails.orm.hibernate.support"])
+ def listener = new ClosureEventListener(manager.hibernateDatastore, persistentEntity, false, ["org.grails.orm.hibernate.support"])
expect:
listener.failOnErrorEnabled
diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/support/hibernate7/HibernateTransactionManagerSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/support/hibernate7/HibernateTransactionManagerSpec.groovy
new file mode 100644
index 0000000000..1ddf87aedc
--- /dev/null
+++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/support/hibernate7/HibernateTransactionManagerSpec.groovy
@@ -0,0 +1,192 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.grails.orm.hibernate.support.hibernate7
+
+import org.hibernate.FlushMode
+import org.hibernate.Session
+import org.hibernate.SessionFactory
+import org.hibernate.Transaction
+import org.hibernate.engine.spi.SessionImplementor
+import org.hibernate.engine.jdbc.spi.JdbcCoordinator
+import org.hibernate.resource.jdbc.spi.LogicalConnectionImplementor
+import org.hibernate.resource.jdbc.spi.PhysicalConnectionHandlingMode
+import org.hibernate.ConnectionReleaseMode
+import org.springframework.transaction.TransactionDefinition
+import org.springframework.transaction.support.DefaultTransactionDefinition
+import org.springframework.transaction.support.TransactionSynchronizationManager
+import spock.lang.Specification
+import java.sql.Connection
+
+/**
+ * Unit tests for HibernateTransactionManager.
+ */
+class HibernateTransactionManagerSpec extends Specification {
+
+ SessionFactory sessionFactory = Mock(SessionFactory)
+ SessionImplementor session = Mock(SessionImplementor)
+ Transaction transaction = Mock(Transaction)
+ JdbcCoordinator jdbcCoordinator = Mock(JdbcCoordinator)
+ LogicalConnectionImplementor logicalConnection = Mock(LogicalConnectionImplementor)
+ Connection connection = Mock(Connection)
+ HibernateTransactionManager transactionManager
+
+ def setup() {
+ transactionManager = new HibernateTransactionManager(sessionFactory)
+ session.unwrap(SessionImplementor) >> session
+ session.getJdbcCoordinator() >> jdbcCoordinator
+ jdbcCoordinator.getLogicalConnection() >> logicalConnection
+ logicalConnection.getConnectionHandlingMode() >> PhysicalConnectionHandlingMode.DELAYED_ACQUISITION_AND_HOLD
+ logicalConnection.getPhysicalConnection() >> connection
+ session.getTransaction() >> transaction
+ }
+
+ def cleanup() {
+ TransactionSynchronizationManager.unbindResourceIfPossible(sessionFactory)
+ TransactionSynchronizationManager.clear()
+ }
+
+ def "test begin new transaction"() {
+ given:
+ TransactionDefinition definition = new DefaultTransactionDefinition()
+
+ when:
+ def txStatus = transactionManager.getTransaction(definition)
+
+ then:
+ 1 * sessionFactory.openSession() >> session
+ 1 * session.beginTransaction() >> transaction
+ txStatus != null
+ txStatus.newTransaction
+ TransactionSynchronizationManager.hasResource(sessionFactory)
+ def holder = (SessionHolder) TransactionSynchronizationManager.getResource(sessionFactory)
+ holder.session == session
+ holder.transaction == transaction
+ }
+
+ def "test commit new transaction"() {
+ given:
+ TransactionDefinition definition = new DefaultTransactionDefinition()
+ 1 * sessionFactory.openSession() >> session
+ 1 * session.beginTransaction() >> transaction
+ def txStatus = transactionManager.getTransaction(definition)
+
+ when:
+ transactionManager.commit(txStatus)
+
+ then:
+ 1 * transaction.commit()
+ 1 * session.isOpen() >> true
+ 1 * session.close()
+ !TransactionSynchronizationManager.hasResource(sessionFactory)
+ }
+
+ def "test rollback new transaction"() {
+ given:
+ TransactionDefinition definition = new DefaultTransactionDefinition()
+ 1 * sessionFactory.openSession() >> session
+ 1 * session.beginTransaction() >> transaction
+ def txStatus = transactionManager.getTransaction(definition)
+
+ when:
+ transactionManager.rollback(txStatus)
+
+ then:
+ 1 * transaction.rollback()
+ 1 * session.isOpen() >> true
+ 1 * session.close()
+ !TransactionSynchronizationManager.hasResource(sessionFactory)
+ }
+
+ def "test read-only transaction sets flush mode to manual"() {
+ given:
+ DefaultTransactionDefinition definition = new DefaultTransactionDefinition()
+ definition.setReadOnly(true)
+
+ when:
+ def txStatus = transactionManager.getTransaction(definition)
+
+ then:
+ 1 * sessionFactory.openSession() >> session
+ 1 * session.setHibernateFlushMode(FlushMode.MANUAL)
+ 1 * session.setDefaultReadOnly(true)
+ 1 * session.beginTransaction() >> transaction
+ txStatus.readOnly
+ }
+
+ def "test participating in existing transaction"() {
+ given:
+ TransactionDefinition definition = new DefaultTransactionDefinition()
+
+ // Setup first transaction
+ 1 * sessionFactory.openSession() >> session
+ 1 * session.beginTransaction() >> transaction
+ def status1 = transactionManager.getTransaction(definition)
+
+ when: "Beginning a second transaction"
+ def status2 = transactionManager.getTransaction(definition)
+
+ then: "It should participate in the existing one"
+ !status2.newTransaction
+ status2.transaction != null
+ 0 * sessionFactory.openSession()
+ 0 * session.beginTransaction()
+
+ // participating transactions might check flush mode
+ _ * session.getHibernateFlushMode() >> FlushMode.AUTO
+ }
+
+ def "test suspend and resume transaction via REQUIRES_NEW"() {
+ given:
+ TransactionDefinition def1 = new DefaultTransactionDefinition()
+ DefaultTransactionDefinition def2 = new DefaultTransactionDefinition()
+ def2.propagationBehavior = TransactionDefinition.PROPAGATION_REQUIRES_NEW
+
+ // Setup first transaction
+ 1 * sessionFactory.openSession() >> session
+ 1 * session.beginTransaction() >> transaction
+ def status1 = transactionManager.getTransaction(def1)
+
+ // Prepare second session for REQUIRES_NEW
+ SessionImplementor session2 = Mock(SessionImplementor)
+ Transaction transaction2 = Mock(Transaction)
+ session2.unwrap(SessionImplementor) >> session2
+ session2.getJdbcCoordinator() >> jdbcCoordinator
+ session2.getTransaction() >> transaction2
+
+ when: "Beginning a REQUIRES_NEW transaction"
+ def status2 = transactionManager.getTransaction(def2)
+
+ then: "The first one should be suspended and second one started"
+ 1 * sessionFactory.openSession() >> session2
+ 1 * session2.beginTransaction() >> transaction2
+ status2.newTransaction
+ def holder2 = (SessionHolder) TransactionSynchronizationManager.getResource(sessionFactory)
+ holder2.session == session2
+
+ when: "Committing the second transaction"
+ transactionManager.commit(status2)
+
+ then: "The second session should be closed and the first one resumed"
+ 1 * transaction2.commit()
+ 1 * session2.isOpen() >> true
+ 1 * session2.close()
+ def resumedHolder = (SessionHolder) TransactionSynchronizationManager.getResource(sessionFactory)
+ resumedHolder.session == session
+ }
+}
diff --git a/grails-data-hibernate7/core/src/test/resources/simplelogger.properties b/grails-data-hibernate7/core/src/test/resources/simplelogger.properties
index 1ca49d6f45..5b2b73c160 100644
--- a/grails-data-hibernate7/core/src/test/resources/simplelogger.properties
+++ b/grails-data-hibernate7/core/src/test/resources/simplelogger.properties
@@ -21,3 +21,4 @@
#org.slf4j.simpleLogger.log.org.grails.orm.hibernate=trace
org.slf4j.simpleLogger.log.org.hibernate.SQL=debug
org.slf4j.simpleLogger.log.org.grails.orm.hibernate.cfg.domainbinding.binder.SingleTableSubclassBinder=debug
+org.slf4j.simpleLogger.log.org.grails.orm.hibernate.connections.SingleTenantSpec=debug
diff --git a/grails-data-hibernate7/docs/build.gradle b/grails-data-hibernate7/docs/build.gradle
index b9f069a8fc..53e73d037d 100644
--- a/grails-data-hibernate7/docs/build.gradle
+++ b/grails-data-hibernate7/docs/build.gradle
@@ -44,7 +44,7 @@ dependencies {
documentation 'org.apache.groovy:groovy-groovydoc'
documentation 'org.apache.groovy:groovy-templates'
documentation 'org.fusesource.jansi:jansi'
- documentation 'jline:jline'
+ documentation 'jline:jline:2.14.6'
documentation project(':grails-bootstrap')
documentation project(':grails-core')
documentation project(':grails-spring')
diff --git a/grails-data-hibernate7/grails-plugin/src/main/groovy/org/grails/plugin/hibernate/support/GrailsOpenSessionInViewInterceptor.java b/grails-data-hibernate7/grails-plugin/src/main/groovy/org/grails/plugin/hibernate/support/GrailsOpenSessionInViewInterceptor.java
index b5afd5ddd8..c902182fb0 100644
--- a/grails-data-hibernate7/grails-plugin/src/main/groovy/org/grails/plugin/hibernate/support/GrailsOpenSessionInViewInterceptor.java
+++ b/grails-data-hibernate7/grails-plugin/src/main/groovy/org/grails/plugin/hibernate/support/GrailsOpenSessionInViewInterceptor.java
@@ -182,7 +182,7 @@ public class GrailsOpenSessionInViewInterceptor extends OpenSessionInViewInterce
public void setHibernateDatastore(HibernateDatastore hibernateDatastore) {
String defaultFlushModeName = hibernateDatastore.getDefaultFlushModeName();
- if (hibernateDatastore.isOsivReadOnly()) {
+ if (hibernateDatastore.isOsivReadOnly(hibernateDatastore.getSessionFactory())) {
this.hibernateFlushMode = FlushMode.MANUAL;
} else {
this.hibernateFlushMode = FlushMode.valueOf(defaultFlushModeName);
@@ -196,7 +196,7 @@ public class GrailsOpenSessionInViewInterceptor extends OpenSessionInViewInterce
if (!ConnectionSource.DEFAULT.equals(connectionName)) {
HibernateDatastore childDatastore = hibernateDs.getDatastoreForConnection(connectionName);
FlushMode childFlushMode;
- if (childDatastore.isOsivReadOnly()) {
+ if (childDatastore.isOsivReadOnly(childDatastore.getSessionFactory())) {
childFlushMode = FlushMode.MANUAL;
} else {
childFlushMode = FlushMode.valueOf(childDatastore.getDefaultFlushModeName());
diff --git a/grails-data-hibernate7/grails-plugin/src/test/groovy/org/grails/plugin/hibernate/support/HibernatePersistenceContextInterceptorSpec.groovy b/grails-data-hibernate7/grails-plugin/src/test/groovy/org/grails/plugin/hibernate/support/HibernatePersistenceContextInterceptorSpec.groovy
index 0d0f835bae..e18b0e1eb2 100644
--- a/grails-data-hibernate7/grails-plugin/src/test/groovy/org/grails/plugin/hibernate/support/HibernatePersistenceContextInterceptorSpec.groovy
+++ b/grails-data-hibernate7/grails-plugin/src/test/groovy/org/grails/plugin/hibernate/support/HibernatePersistenceContextInterceptorSpec.groovy
@@ -105,20 +105,27 @@ class HibernatePersistenceContextInterceptorSpec extends Specification {
}
def "test flush and clear"() {
- given: "A persistence context interceptor"
+ given: "A persistence context interceptor and a manually-bound Hibernate session"
def interceptor = new HibernatePersistenceContextInterceptor()
interceptor.setHibernateDatastore(datastore)
+ def sf = datastore.sessionFactory
+ def nativeSession = sf.openSession()
+ TransactionSynchronizationManager.bindResource(sf, new SessionHolder(nativeSession))
- when: "Operations are called within a session context"
- HpciBook.withNewSession {
- interceptor.init()
- interceptor.clear()
- interceptor.flush()
- interceptor.destroy()
- }
+ when: "Operations are called within the session context"
+ interceptor.init()
+ interceptor.clear()
+ interceptor.flush()
+ interceptor.destroy()
then: "no exception occurs"
noExceptionThrown()
+
+ cleanup:
+ if (TransactionSynchronizationManager.hasResource(sf)) {
+ TransactionSynchronizationManager.unbindResource(sf)
+ }
+ nativeSession.close()
}
def "test flush persists changes in a non-transactional interceptor-owned session"() {
diff --git a/grails-data-hibernate7/spring-orm/src/main/java/org/grails/orm/hibernate/support/hibernate7/HibernateTransactionManager.java b/grails-data-hibernate7/spring-orm/src/main/java/org/grails/orm/hibernate/support/hibernate7/HibernateTransactionManager.java
index 57de5278ed..da9febf9c1 100644
--- a/grails-data-hibernate7/spring-orm/src/main/java/org/grails/orm/hibernate/support/hibernate7/HibernateTransactionManager.java
+++ b/grails-data-hibernate7/spring-orm/src/main/java/org/grails/orm/hibernate/support/hibernate7/HibernateTransactionManager.java
@@ -451,8 +451,15 @@ public class HibernateTransactionManager extends AbstractPlatformTransactionMana
@Override
protected boolean isExistingTransaction(Object transaction) {
HibernateTransactionObject txObject = (HibernateTransactionObject) transaction;
- return (txObject.hasSpringManagedTransaction() ||
+ boolean existing = (txObject.hasSpringManagedTransaction() ||
(this.hibernateManagedSession && txObject.hasHibernateManagedTransaction()));
+ if (logger.isDebugEnabled()) {
+ logger.debug("isExistingTransaction: " + existing +
+ ", hasSpringManagedTransaction: " + txObject.hasSpringManagedTransaction() +
+ ", sessionHolder: " + (txObject.hasSessionHolder() ? txObject.getSessionHolder() : "null") +
+ ", transaction: " + (txObject.hasSessionHolder() && txObject.getSessionHolder().getTransaction() != null ? txObject.getSessionHolder().getTransaction() : "null"));
+ }
+ return existing;
}
@Override
@@ -789,7 +796,7 @@ public class HibernateTransactionManager extends AbstractPlatformTransactionMana
* Hibernate transaction object, representing a SessionHolder.
* Used as transaction object by HibernateTransactionManager.
*/
- private class HibernateTransactionObject extends JdbcTransactionObjectSupport {
+ public static class HibernateTransactionObject extends JdbcTransactionObjectSupport {
@Nullable
private SessionHolder sessionHolder;
@@ -883,11 +890,11 @@ public class HibernateTransactionManager extends AbstractPlatformTransactionMana
getSessionHolder().getSession().flush();
}
catch (HibernateException ex) {
- throw convertHibernateAccessException(ex);
+ throw SessionFactoryUtils.convertHibernateAccessException(ex);
}
catch (PersistenceException ex) {
if (ex.getCause() instanceof HibernateException hibernateEx) {
- throw convertHibernateAccessException(hibernateEx);
+ throw SessionFactoryUtils.convertHibernateAccessException(hibernateEx);
}
throw ex;
}
diff --git a/grails-test-examples/hibernate7/demo33/src/test/groovy/demo/UniqueConstraintOnHasOneSpec.groovy b/grails-test-examples/hibernate7/demo33/src/test/groovy/demo/UniqueConstraintOnHasOneSpec.groovy
index c509e2b3a5..384d0f8167 100644
--- a/grails-test-examples/hibernate7/demo33/src/test/groovy/demo/UniqueConstraintOnHasOneSpec.groovy
+++ b/grails-test-examples/hibernate7/demo33/src/test/groovy/demo/UniqueConstraintOnHasOneSpec.groovy
@@ -20,7 +20,6 @@ package demo
import grails.persistence.Entity
import grails.testing.gorm.DataTest
-import groovy.test.NotYetImplemented
import spock.lang.Specification
class UniqueConstraintOnHasOneSpec extends Specification implements DataTest {
@@ -49,8 +48,7 @@ class UniqueConstraintOnHasOneSpec extends Specification implements DataTest {
foo2.errors['name']?.code == 'unique'
}
- @NotYetImplemented
- void "Foo's bar should be unique, but..."() {
+ void "Foo's bar should be unique"() {
given:
def foo1 = new Foo(name: "FOO1")
def bar = new Bar(name: "BAR", foo: foo1)
diff --git a/grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/conf/logback.xml b/grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/conf/logback.xml
index 11f34868ac..3a79ab50a6 100644
--- a/grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/conf/logback.xml
+++ b/grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/conf/logback.xml
@@ -31,7 +31,11 @@
</encoder>
</appender>
- <root level="error">
+ <logger name="org.grails.orm.hibernate" level="debug" />
+ <logger name="grails.gorm.transactions" level="debug" />
+ <logger name="org.springframework.transaction" level="debug" />
+
+ <root level="debug">
<appender-ref ref="STDOUT" />
</root>
</configuration>
\ No newline at end of file
diff --git a/grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/services/example/AnotherBookService.groovy b/grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/services/example/AnotherBookService.groovy
index c3dd201b64..786fe47907 100644
--- a/grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/services/example/AnotherBookService.groovy
+++ b/grails-test-examples/hibernate7/grails-database-per-tenant/grails-app/services/example/AnotherBookService.groovy
@@ -26,12 +26,12 @@ import grails.gorm.transactions.Transactional
/**
* Created by graemerocher on 06/04/2017.
*/
-@CurrentTenant
@Transactional
+@CurrentTenant
class AnotherBookService {
Book saveBook(String title = 'The Stand') {
- new Book(title: title).save()
+ new Book(title: title).save(flush: true)
}
@ReadOnly
diff --git a/grails-test-examples/hibernate7/grails-database-per-tenant/src/integration-test/groovy/example/DatabasePerTenantIntegrationSpec.groovy b/grails-test-examples/hibernate7/grails-database-per-tenant/src/integration-test/groovy/example/DatabasePerTenantIntegrationSpec.groovy
index 39915bda01..b0a0d619b9 100644
--- a/grails-test-examples/hibernate7/grails-database-per-tenant/src/integration-test/groovy/example/DatabasePerTenantIntegrationSpec.groovy
+++ b/grails-test-examples/hibernate7/grails-database-per-tenant/src/integration-test/groovy/example/DatabasePerTenantIntegrationSpec.groovy
@@ -57,9 +57,9 @@ class DatabasePerTenantIntegrationSpec extends Specification {
given:
webRequest.session.setAttribute(SessionTenantResolver.ATTRIBUTE, "moreBooks")
+
when:
Book book = bookService.saveBook("Book-Test-${System.currentTimeMillis()}")
- println book
log.info("${book}")
then:
@@ -72,9 +72,9 @@ class DatabasePerTenantIntegrationSpec extends Specification {
given:
webRequest.session.setAttribute(SessionTenantResolver.ATTRIBUTE, "moreBooks")
+
when:
Book book = anotherBookService.saveBook("Book-Test-${System.currentTimeMillis()}")
- println book
log.info("${book}")
then:
diff --git a/grails-test-examples/hibernate7/grails-multitenant-multi-datasource/grails-app/services/example/MetricService.groovy b/grails-test-examples/hibernate7/grails-multitenant-multi-datasource/grails-app/services/example/MetricService.groovy
index 7607d7e6eb..ee4e7e752a 100644
--- a/grails-test-examples/hibernate7/grails-multitenant-multi-datasource/grails-app/services/example/MetricService.groovy
+++ b/grails-test-examples/hibernate7/grails-multitenant-multi-datasource/grails-app/services/example/MetricService.groovy
@@ -22,6 +22,7 @@ package example
import grails.gorm.services.Service
import grails.gorm.transactions.Transactional
import org.grails.datastore.gorm.GormEnhancer
+import org.grails.datastore.gorm.GormRegistry
import org.grails.datastore.gorm.GormStaticApi
/**
@@ -53,7 +54,7 @@ abstract class MetricService {
* Statically compiled access to the secondary datasource via GormEnhancer.
*/
private GormStaticApi<Metric> getSecondaryApi() {
- GormEnhancer.findStaticApi(Metric, 'secondary')
+ GormRegistry.findStaticApi(Metric, 'secondary')
}
/**