Skip to content
Open
Show file tree
Hide file tree
Changes from 82 commits
Commits
Show all changes
101 commits
Select commit Hold shift + click to select a range
27d8682
fix: capture STACK_LOG output via a Logback appender instead of Syste…
borinquenkid Jul 29, 2026
a8227fd
fix: convert GrailsExceptionResolverSpec to LogCapture, remove last S…
borinquenkid Jul 29, 2026
6a9d739
feat: add SessionResolver infrastructure and extend core datastore APIs
borinquenkid Jun 27, 2026
8d53475
fix: restore concrete Service trait methods to fix @CompileStatic com…
borinquenkid Jun 27, 2026
02a0ccb
fix: widen MongoMappingContext.initialize visibility to public
borinquenkid Jun 27, 2026
0e8105b
fix: address Copilot review findings on PR #15779
borinquenkid Jul 2, 2026
e225bd1
fix: address review feedback on SessionResolver/AbstractDatastore arc…
borinquenkid Jul 10, 2026
8679e72
fix: guard getTenantId() DISCRIMINATOR mode and close review-flagged …
borinquenkid Jul 10, 2026
c38cd7d
fix: address jdaugherty's follow-up review on SessionResolver/Abstrac…
borinquenkid Jul 10, 2026
dcd111c
test(grails-datastore-core): close Codecov patch-coverage gap on getT…
borinquenkid Jul 12, 2026
499de72
feat: GORM O(M+N) scaling — GormRegistry, GormEnhancer, and core API …
borinquenkid Jun 27, 2026
7bc4ffe
fix: restore registerConstraints to GormEnhancer dropped in scaling r…
borinquenkid Jun 27, 2026
5754c3f
fix: restore registerConstraints hook and fix MongoStaticApi fields d…
borinquenkid Jun 27, 2026
6fed94d
fix: distinguish datasource connection qualifier from tenant ID in Ab…
borinquenkid Jun 27, 2026
f61e021
fix: mode-aware tenant lookup and bound DatastoreResolvers in GormReg…
borinquenkid Jun 27, 2026
196fcd1
fix: register HibernateGormApiFactory in registerConstraints; update …
borinquenkid Jun 27, 2026
d99ba09
style: remove unused ConnectionSource import and fix consecutive blan…
borinquenkid Jun 27, 2026
c67b22e
fix: restore deprecated GormEnhancer extension points for backward co…
borinquenkid Jun 27, 2026
7ccb667
fix: restore deprecated backward-compat API surface for adapter modules
borinquenkid Jun 27, 2026
288dc84
fix: restore deprecated setDatastore and persistentEntity for H7 adap…
borinquenkid Jun 27, 2026
d831368
fix: resolve null datastore in H5/H7 validation and dynamic finder di…
borinquenkid Jun 28, 2026
dbd89ff
fix: remove redundant this.datastore assignment in HibernateGormStati…
borinquenkid Jun 28, 2026
612b444
Fix MongoDB functional tests: fall back to enhancer API factory when …
borinquenkid Jun 28, 2026
4c9d219
fix: restore null-tolerant service/datastore contracts in GormRegistr…
borinquenkid Jun 28, 2026
b1ddb0a
fix: clear GormRegistry core-impl regressions (H5 core 42→6)
borinquenkid Jun 28, 2026
eb5faa0
fix: resolve multi-tenant dynamic finder dispatch and tenant session …
borinquenkid Jun 28, 2026
5857c35
fix: honor getAll id order, null slots, and id conversion in Hibernat…
borinquenkid Jun 28, 2026
c2ac4a3
fix: register MongoGormApiFactory so Mongo entities resolve a MongoSt…
borinquenkid Jun 29, 2026
c9d1bdb
fix: apply pagination args before Query.list() in findAll-by-example
borinquenkid Jun 29, 2026
24a07ed
fix: implement find/findAll-by-example via findWhere/findAllWhere del…
borinquenkid Jun 29, 2026
1e8c6f1
fix: H7 first/last(Map) inherit core's order-aware implementation
borinquenkid Jun 29, 2026
fcaec18
test: align H7 core tests with the GormRegistry rewrite
borinquenkid Jun 29, 2026
d862b47
fix: H7 disjunction/conjunction/negation operate on detachedCriteria
borinquenkid Jun 29, 2026
e823b48
fix: MultiTenant.eachTenant must not force current-tenant resolution
borinquenkid Jun 29, 2026
9612d4d
test: isolate Mongo multi-tenancy specs from singleton GormRegistry p…
borinquenkid Jun 29, 2026
a6e8c9e
fix: @CurrentTenant data services resolve the primary datastore for t…
borinquenkid Jun 29, 2026
d321bc6
test: @PendingFeature the pre-existing Mongo count-over-OR disjunctio…
borinquenkid Jun 29, 2026
f121b88
fix: resolve GORM regressions surfaced by the registry refactor
borinquenkid Jun 30, 2026
293536b
chore: strip scope-creep config noise from the GormRegistry PR
borinquenkid Jun 30, 2026
e3de608
fix: address Copilot review findings on PR #15780
borinquenkid Jul 2, 2026
02f0314
test: verify @Service+@CurrentTenant routing in DATABASE multi-tenanc…
borinquenkid Jul 4, 2026
3943b06
fix: restore copyAnnotations dedup guard needed by ServiceTransformation
borinquenkid Jul 10, 2026
b6c2470
test(grails-datamapping-core): add GormApiResolverSpec closing PR #15…
borinquenkid Jul 12, 2026
1960c29
fix(grails-datamapping-core): restore saveAll's persisted-id return v…
borinquenkid Jul 12, 2026
29c6c4a
test(grails-datamapping-core): add GormRegistryCoverageSpec closing P…
borinquenkid Jul 12, 2026
e304181
test(grails-datamapping-core): add GormEnhancerCoverageSpec closing P…
borinquenkid Jul 12, 2026
580d4b5
test(grails-datamapping-core): add GormValidationApiCoverageSpec clos…
borinquenkid Jul 12, 2026
3581b3d
test(grails-datamapping-core): add GormInstanceApiSpec closing PR #15…
borinquenkid Jul 12, 2026
6827398
test(grails-datamapping-core): add AbstractGormApiSpec closing PR #15…
borinquenkid Jul 12, 2026
476c6c7
test(grails-datamapping-core): add TenantsSpec closing PR #15780 cove…
borinquenkid Jul 12, 2026
e16530c
test(grails-datamapping-core): add DynamicFinderCoverageSpec closing …
borinquenkid Jul 12, 2026
6985f4e
test(grails-datamapping-core): add ServiceTransformationCoverageSpec …
borinquenkid Jul 13, 2026
868eb22
test(grails-datamapping-core): add MultiTenantEventListenerSpec closi…
borinquenkid Jul 13, 2026
97e03a3
test(grails-datamapping-core): add AbstractStringQueryImplementerCove…
borinquenkid Jul 13, 2026
6ebcac4
test(grails-datamapping-core): add DefaultTenantServiceSpec closing P…
borinquenkid Jul 13, 2026
d42e0b4
test(grails-datamapping-core): add CurrentTenantHolderSpec closing PR…
borinquenkid Jul 13, 2026
496913a
test(grails-datamapping-core): add TenantDelegatingGormOperationsSpec…
borinquenkid Jul 13, 2026
a3b355f
test(grails-datamapping-core): add GrailsTransactionTemplateSpec clos…
borinquenkid Jul 13, 2026
5de185d
fix(grails-datamapping-core): guard CriteriaBuilder's cache/join/sele…
borinquenkid Jul 13, 2026
d4cf852
test(grails-datamapping-core): add GormStaticApiRegistrySpec closing …
borinquenkid Jul 13, 2026
c230c25
test(grails-datamapping-core): add ConnectionSourceNameResolverSpec c…
borinquenkid Jul 13, 2026
66acf2d
test(grails-datamapping-core): add GormValidationApiRegistrySpec and …
borinquenkid Jul 13, 2026
a6da013
test(grails-datamapping-core): add AbstractFinderSpec closing PR #157…
borinquenkid Jul 13, 2026
bd6a5c8
test(grails-datamapping-core): add FindAllByImplementerCoverageSpec c…
borinquenkid Jul 13, 2026
6b3888b
test(grails-datamapping-core): add FindOrSaveByFinderSpec closing PR …
borinquenkid Jul 13, 2026
d2b675b
test(grails-datamapping-core): add AbstractDatastoreApiSpec closing P…
borinquenkid Jul 13, 2026
5ff48aa
test(grails-data-simple): add SimpleMapDatastoreSpec closing PR #1578…
borinquenkid Jul 13, 2026
6660e23
test(grails-datamapping-core): add AbstractServiceImplementerCoverage…
borinquenkid Jul 13, 2026
f52c8f7
test(grails-data-hibernate5-core): add GrailsDataTckManagerSpec closi…
borinquenkid Jul 13, 2026
39be872
test(grails-datamapping-core): extend MappingContextTraversableResolv…
borinquenkid Jul 13, 2026
b1b6c32
test(grails-datamapping-core): add DefaultTransactionServiceSpec clos…
borinquenkid Jul 13, 2026
380f240
test(grails-datamapping-core): add DefaultTransactionTemplateFactoryS…
borinquenkid Jul 13, 2026
b707271
test(grails-datamapping-core): extend TransactionalTransformSpec clos…
borinquenkid Jul 13, 2026
c111597
test(grails-data-mongodb-core): add MongoStaticApiSpec closing PR #15…
borinquenkid Jul 13, 2026
fd4f037
test: add core-class unit tests for the GormRegistry O(M+N) rewrite
borinquenkid Jun 29, 2026
2da1a7f
fix: address Copilot review findings on PR #15790
borinquenkid Jul 2, 2026
39c2075
fix: update ServiceTransformSpec for current Groovy MethodNode.typeDe…
borinquenkid Jul 4, 2026
a69aa3b
fix: address jdaugherty's 2026-07-29 review of the SessionResolver in…
borinquenkid Jul 29, 2026
d02fd44
style: restore blank line between methods after createSettings removal
borinquenkid Jul 29, 2026
eec296b
fix: address findings from adversarial review of the consolidated branch
borinquenkid Jul 29, 2026
0c3493b
fix: log swallowed exceptions in GORM datastore resolution instead of…
borinquenkid Jul 30, 2026
79be8e6
fix: loosen GormRegistryConcurrencySpec timing budget to reduce CI fl…
borinquenkid Jul 30, 2026
da09401
fix: address jdaugherty's 2026-08-03 review of the consolidated GormR…
borinquenkid Aug 5, 2026
4b116c2
fix: stop resolving TenantDelegatingGormOperations' datastore by class
borinquenkid Aug 5, 2026
3707a82
fix: make Datastore.hasCurrentSession() non-mutating
borinquenkid Aug 5, 2026
63c2c20
fix: repair DEFAULT routing deterministically when its datastore is r…
borinquenkid Aug 5, 2026
e011e08
feat: restore deleteAll()/deleteAll(Map) on GormStaticOperations, mat…
borinquenkid Aug 5, 2026
b81bda3
fix: remove GormRegistry's last silent arbitrary-datastore fallback
borinquenkid Aug 5, 2026
2dd1880
fix: don't clobber a pre-existing instance methodMissing handler in G…
borinquenkid Aug 5, 2026
4909abf
fix: throw when CurrentTenantHolder.get() cannot resolve a single tenant
borinquenkid Aug 5, 2026
b07c5d7
perf: stop probing connection names with exceptions in AbstractGormAp…
borinquenkid Aug 6, 2026
efe62b0
fix: only evict a GORM API on a positive datastore identity match
borinquenkid Aug 6, 2026
709d72a
refactor: drop dead DatastoreResolver plumbing from GormRegistry.regi…
borinquenkid Aug 6, 2026
d49ee59
fix: remove duplicate/PR-invented constraint-evaluation path, restore…
borinquenkid Aug 6, 2026
508cce3
refactor: strip debug-logging scaffolding from Tenants.withId and Gra…
borinquenkid Aug 6, 2026
fda9664
fix: make AstUtils.copyAnnotations dedup opt-in instead of global
borinquenkid Aug 6, 2026
ebad67a
docs: fix Tenants.withTenant javadoc claiming it opens a session
borinquenkid Aug 6, 2026
5f45fb1
docs: add multi-tenancy guide and upgrade notes for the GormRegistry …
borinquenkid Aug 6, 2026
bd16af7
Merge remote-tracking branch 'origin/8.0.x' into feat/gorm-registry-c…
borinquenkid Aug 6, 2026
c9c57e6
fix: restore AbstractGormApi.EXCLUDES to its 8.0.x contents and publi…
borinquenkid Aug 6, 2026
bc54db5
test: close PR #16066's Codecov patch-coverage gaps in the four large…
borinquenkid Aug 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion gradle/test-config.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,38 @@ dependencies {
add('testRuntimeOnly', 'org.objenesis:objenesis')
}

/**
* Recursively checks if a project has a direct or transitive dependency on a target project.
* Used to detect modules that rely on GORM (grails-datamapping-core), which are prone to
* cross-test registry pollution when executed in parallel (maxParallelForks > 1).
*
* Uses Gradle 9 compatible path lookup (proj.project(pd.getPath())) because
* ProjectDependency.getDependencyProject() was removed in Gradle 9.
*
* @param proj the project to inspect
* @param targetProjectName the name of the target dependency project (e.g. 'grails-datamapping-core')
* @param visited set of already visited projects to prevent infinite recursion in cyclic dependency configurations
* @return true if the project depends on the target project
*/
@groovy.transform.CompileStatic
boolean dependsOnProject(Project proj, String targetProjectName, Set<String> visited = new HashSet<String>()) {
if (proj.name == targetProjectName) return true
if (visited.contains(proj.name)) return false
visited.add(proj.name)
for (org.gradle.api.artifacts.Configuration config : proj.configurations) {
for (org.gradle.api.artifacts.Dependency dep : config.dependencies) {
if (dep instanceof org.gradle.api.artifacts.ProjectDependency) {
org.gradle.api.artifacts.ProjectDependency pd = (org.gradle.api.artifacts.ProjectDependency) dep
Project depProj = proj.project(pd.getPath())
if (dependsOnProject(depProj, targetProjectName, visited)) {
return true
}
}
}
}
return false
}

// Disable build cache for Groovy compilation in CI to ensure AST transformations are always applied.
// AST transformers are applied at compile time, and Gradle's incremental compilation might not detect
// when a transformer itself changes, leading to stale bytecode.
Expand Down Expand Up @@ -83,7 +115,11 @@ tasks.withType(Test).configureEach {
showStackTraces = true
}
excludes = ['**/*TestCase.class', '**/*$*.class']
maxParallelForks = configuredTestParallel

// Selectively isolate GORM (grails-datamapping-core) dependent tests to prevent GormRegistry conflicts
def isGormProject = dependsOnProject(project, 'grails-datamapping-core')
maxParallelForks = isGormProject ? 1 : configuredTestParallel

maxHeapSize = isCiBuild ? '768m' : '1024m'
forkEvery = hasProperty('forkEveryUnitTest') ? getProperty('forkEveryUnitTest') as long : (isCiBuild ? 50 : 100)
if (System.getProperty('debug.tests')) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,8 @@ class GrailsUtilStackFiltererSpec extends Specification {
}

def 'installed DefaultStackTraceFilterer honours logFullStackTraceOnFilter=false'() {
given: 'a configured log appender to capture the StackTrace log entry'
def logCapture = new LogCapture('StackTrace')
given: 'a capture of the dedicated STACK_LOG logger'
def logCapture = new LogCapture(DefaultStackTraceFilterer.STACK_LOG_NAME)

and: 'a filterer with the side-effect emission disabled'
def quietFilterer = new DefaultStackTraceFilterer()
Expand All @@ -117,15 +117,15 @@ class GrailsUtilStackFiltererSpec extends Specification {
GrailsUtil.deepSanitize(exceptionWithApplicationFrame())

then: "no 'Full Stack Trace:' entry is emitted"
logCapture.events.count { it.formattedMessage.contains(StackTraceFilterer.FULL_STACK_TRACE_MESSAGE) } == 0
logCapture.events.every { !it.formattedMessage.contains(StackTraceFilterer.FULL_STACK_TRACE_MESSAGE) }

cleanup:
logCapture.close()
}

def 'installed DefaultStackTraceFilterer emits Full Stack Trace by default'() {
given: 'a configured log appender to capture the StackTrace log entry'
def logCapture = new LogCapture('StackTrace')
given: 'a capture of the dedicated STACK_LOG logger'
def logCapture = new LogCapture(DefaultStackTraceFilterer.STACK_LOG_NAME)

and: 'a filterer with the default (enabled) side-effect emission'
def loudFilterer = new DefaultStackTraceFilterer()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,14 +226,14 @@ class GrailsBootstrapRegistryInitializerSpec extends Specification {
def context = contextWithProperties([
(Settings.SETTING_LOG_FULL_STACKTRACE_ON_FILTER): 'false'
])
def logCapture = new LogCapture('StackTrace')
def logCapture = new LogCapture(DefaultStackTraceFilterer.STACK_LOG_NAME)

when:
closeBootstrapContext(context)
GrailsUtil.deepSanitize(exceptionWithApplicationFrame())

then: "no 'Full Stack Trace:' entry is emitted"
logCapture.events.count { it.formattedMessage.contains(StackTraceFilterer.FULL_STACK_TRACE_MESSAGE) } == 0
logCapture.events.every { !it.formattedMessage.contains(StackTraceFilterer.FULL_STACK_TRACE_MESSAGE) }

cleanup:
logCapture.close()
Expand All @@ -242,7 +242,7 @@ class GrailsBootstrapRegistryInitializerSpec extends Specification {
def 'defaults logFullStackTraceOnFilter to true on the promoted DefaultStackTraceFilterer'() {
given:
def context = contextWithProperties([:])
def logCapture = new LogCapture('StackTrace')
def logCapture = new LogCapture(DefaultStackTraceFilterer.STACK_LOG_NAME)

when:
closeBootstrapContext(context)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,8 @@
package org.grails.gorm.graphql.fetcher.manager

import graphql.schema.DataFetchingEnvironment
import org.grails.datastore.gorm.GormEnhancer
import org.grails.datastore.gorm.GormRegistry
import org.grails.datastore.gorm.GormStaticApi
import org.grails.datastore.mapping.core.connections.ConnectionSource
import org.grails.datastore.mapping.model.PersistentEntity
import org.grails.gorm.graphql.binding.GraphQLDataBinder
import org.grails.gorm.graphql.fetcher.BindingGormDataFetcher
Expand Down Expand Up @@ -157,7 +156,7 @@ class GraphQLDataFetcherManagerSpec extends Specification {

void "test registering a binding fetcher"() {
given:
GormEnhancer.STATIC_APIS.put(ConnectionSource.DEFAULT, ['java.lang.String': Mock(GormStaticApi)])
GormRegistry.instance.staticApiRegistry.register('java.lang.String', Mock(GormStaticApi))

when:
manager.registerBindingDataFetcher(String, mockBindingFetcher)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* 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.DatastoreResolver
import org.grails.datastore.gorm.DefaultGormApiFactory
import org.grails.datastore.gorm.GormApiFactory
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.gorm.finders.FinderMethod
import org.grails.datastore.mapping.model.MappingContext

/**
* {@link GormApiFactory} that produces Hibernate 5-specific API instances, ensuring that
* {@code withNewSession} and related lifecycle methods use the Hibernate {@code SessionFactory}
* binding rather than the generic GORM {@code DatastoreUtils} path.
*/
@CompileStatic
class HibernateGormApiFactory implements GormApiFactory {

private final ClassLoader classLoader

HibernateGormApiFactory(ClassLoader classLoader) {
this.classLoader = classLoader
}

@Override
<D> GormStaticApi<D> createStaticApi(Class<D> persistentClass, MappingContext mappingContext, DatastoreResolver resolver, String qualifier, GormRegistry registry) {
HibernateDatastore hds = (HibernateDatastore) resolver.resolve()
List<FinderMethod> finders = new DefaultGormApiFactory().createDynamicFinders(resolver, mappingContext)
return new HibernateGormStaticApi<D>(persistentClass, hds, finders, classLoader, hds.getTransactionManager())
}

@Override
<D> GormInstanceApi<D> createInstanceApi(Class<D> persistentClass, MappingContext mappingContext, DatastoreResolver resolver, GormRegistry registry, boolean failOnError, boolean markDirty) {
HibernateDatastore hds = (HibernateDatastore) resolver.resolve()
GormInstanceApi<D> instanceApi = new HibernateGormInstanceApi<D>(persistentClass, hds, classLoader)
instanceApi.failOnError = failOnError
// The Hibernate datastore is authoritative for dirty marking (resolved from
// SETTING_MARK_DIRTY, default false). Hibernate performs its own snapshot dirty checking,
// so force-marking every save() dirty would re-update unchanged entities and fire spurious
// beforeUpdate/afterUpdate events. The generic enhancer default (true) must not override it.
instanceApi.markDirty = hds.markDirty
return instanceApi
}

@Override
<D> GormValidationApi<D> createValidationApi(Class<D> persistentClass, MappingContext mappingContext, DatastoreResolver resolver, GormRegistry registry) {
HibernateDatastore hds = (HibernateDatastore) resolver.resolve()
return new HibernateGormValidationApi<D>(persistentClass, hds, classLoader)
}

@Override
List<FinderMethod> createDynamicFinders(DatastoreResolver datastoreResolver, MappingContext mappingContext) {
new DefaultGormApiFactory().createDynamicFinders(datastoreResolver, mappingContext)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import org.springframework.transaction.PlatformTransactionManager

import org.grails.datastore.gorm.GormEnhancer
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.core.Datastore
Expand Down Expand Up @@ -75,6 +76,6 @@ class HibernateGormEnhancer extends GormEnhancer {

@Override
protected void registerConstraints(Datastore datastore) {
// no-op
GormRegistry.instance.registerApiFactory(HibernateDatastore, new HibernateGormApiFactory(Thread.currentThread().contextClassLoader))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ import org.springframework.transaction.PlatformTransactionManager
import org.springframework.transaction.support.TransactionSynchronizationManager

import grails.orm.HibernateCriteriaBuilder
import org.grails.datastore.gorm.GormEnhancer
import org.grails.datastore.gorm.finders.DynamicFinder
import org.grails.datastore.gorm.finders.FinderMethod
import org.grails.datastore.mapping.query.api.BuildableCriteria as GrailsCriteria
Expand Down Expand Up @@ -135,7 +134,11 @@ class HibernateGormStaticApi<D> extends AbstractHibernateGormStaticApi<D> {

@Override
def propertyMissing(String name) {
return GormEnhancer.findStaticApi(persistentClass, name)
// Delegate to the base implementation, which resolves dynamic finder property
// access to a finder closure before falling back to connection-source qualifier
// lookup. Returning a qualifier API unconditionally would break finder calls that
// Groovy resolves via the property channel (e.g. Entity.findByName(arg)).
return super.propertyMissing(name)
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,13 @@
package org.grails.orm.hibernate;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import jakarta.persistence.FlushModeType;
import jakarta.persistence.criteria.CriteriaBuilder;
Expand All @@ -34,6 +38,7 @@
import org.hibernate.proxy.HibernateProxy;

import org.springframework.context.ApplicationEventPublisher;
import org.springframework.core.convert.ConversionService;

import org.grails.datastore.gorm.timestamp.DefaultTimestampProvider;
import org.grails.datastore.mapping.model.PersistentEntity;
Expand Down Expand Up @@ -163,23 +168,65 @@ public long updateAll(final QueryableCriteria criteria, final Map<String, Object

public List retrieveAll(final Class type, final Iterable keys) {
final PersistentEntity persistentEntity = getMappingContext().getPersistentEntity(type.getName());
final Class idType = persistentEntity.getIdentity().getType();
final ConversionService conversionService = getMappingContext().getConversionService();

// Convert each requested id to the entity's identifier type, preserving order and
// duplicates. getAll() must return entities in the supplied id order with a null slot
// for any id that does not resolve to a row, so order is driven by the request rather
// than the database.
final List<Serializable> requestedIds = new ArrayList<>();
for (Object key : keys) {
requestedIds.add(convertToIdentifierType(key, idType, conversionService));
}

return getHibernateTemplate().execute(session -> {
final CriteriaBuilder criteriaBuilder = session.getCriteriaBuilder();
CriteriaQuery criteriaQuery = criteriaBuilder.createQuery(type);
final Root root = criteriaQuery.from(type);
final String id = persistentEntity.getIdentity().getName();
criteriaQuery = criteriaQuery.where(
criteriaBuilder.in(
root.get(id).in(getIterableAsCollection(keys))
)
);
final org.hibernate.query.Query jpaQuery = session.createQuery(criteriaQuery);
getHibernateTemplate().applySettings(jpaQuery);

return new HibernateHqlQuery(this, persistentEntity, jpaQuery).list();
// Query only the distinct, non-null ids; a missing id simply yields no row.
final Set<Serializable> distinctIds = new LinkedHashSet<>();
for (Serializable requestedId : requestedIds) {
if (requestedId != null) {
distinctIds.add(requestedId);
}
}

final Map<Object, Object> entitiesById = new HashMap<>();
if (!distinctIds.isEmpty()) {
final CriteriaBuilder criteriaBuilder = session.getCriteriaBuilder();
CriteriaQuery criteriaQuery = criteriaBuilder.createQuery(type);
final Root root = criteriaQuery.from(type);
final String id = persistentEntity.getIdentity().getName();
// Path.in(Collection) already yields a complete `id IN (...)` predicate; wrapping it
// in another criteriaBuilder.in(...) emitted a second empty `IN ()`
// (e.g. `id in (1,2) in ()`), which Hibernate rejects with a QuerySyntaxException.
criteriaQuery = criteriaQuery.where(root.get(id).in(distinctIds));
final org.hibernate.query.Query jpaQuery = session.createQuery(criteriaQuery);
getHibernateTemplate().applySettings(jpaQuery);

final List results = new HibernateHqlQuery(this, persistentEntity, jpaQuery).list();
for (Object entity : results) {
entitiesById.put(session.getIdentifier(entity), entity);
}
}

// Reassemble in the requested order, leaving a null slot for missing ids.
final List ordered = new ArrayList<>(requestedIds.size());
for (Serializable requestedId : requestedIds) {
ordered.add(requestedId == null ? null : entitiesById.get(requestedId));
}
return ordered;
});
}

private Serializable convertToIdentifierType(Object key, Class idType, ConversionService conversionService) {
if (key == null || idType.isInstance(key)) {
return (Serializable) key;
}
if (conversionService != null && conversionService.canConvert(key.getClass(), idType)) {
return (Serializable) conversionService.convert(key, idType);
}
return (Serializable) key;
}

public Query createQuery(Class type) {
return createQuery(type, null);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ class GrailsDataHibernate5TckManager extends GrailsDataTckManager {
ApplicationContext applicationContext
HibernateDatastore multiDataSourceDatastore
HibernateDatastore multiTenantMultiDataSourceDatastore
Map grailsConfig
ConfigObject grailsConfig = new ConfigObject()
boolean isTransactional = true

@Override
void setup(Class<? extends Specification> spec) {
Expand All @@ -63,19 +64,13 @@ class GrailsDataHibernate5TckManager extends GrailsDataTckManager {

@Override
Session createSession() {
ConfigObject config = new ConfigObject()
grailsConfig.dataSource.dbCreate = grailsConfig.dataSource.dbCreate ?: "create-drop"
grailsApplication = new DefaultGrailsApplication(domainClasses as Class[], new GroovyClassLoader(GrailsDataHibernate5TckManager.getClassLoader()))
if (grailsConfig) {
config.putAll(grailsConfig)
grailsApplication.config.putAll(grailsConfig)
}
if (!config.containsKey('dataSource.dbCreate') && !config.dataSource.containsKey('dbCreate')) {
config.dataSource.dbCreate = "create-drop"
}
boolean isTransactional = true

grailsApplication = new DefaultGrailsApplication(domainClasses, new GroovyClassLoader(GrailsDataHibernate5TckManager.getClassLoader()))
grailsApplication.config.putAll(config)

hibernateDatastore = new HibernateDatastore(DatastoreUtils.createPropertyResolver(config), domainClasses)
hibernateDatastore = new HibernateDatastore(DatastoreUtils.createPropertyResolver(grailsConfig), domainClasses as Class[])
transactionManager = hibernateDatastore.getTransactionManager()
sessionFactory = hibernateDatastore.sessionFactory
if (transactionStatus == null && isTransactional) {
Expand Down
Loading
Loading