Skip to content

Commit e2d9374

Browse files
authored
Merge pull request #15550 from apache/8.0.x-hibernate7-dev
Fixed regressions from Hibernate 5
2 parents 6cc9fda + 9c79935 commit e2d9374

75 files changed

Lines changed: 942 additions & 692 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ export GRADLE_OPTS="-Xms2G -Xmx5G"
5151
8. **No internal APIs in docs** - Only document public APIs; never reference internal or package-private classes and methods in user-facing documentation
5252
9. **Test via public APIs** - Tests must exercise behavior through the same APIs an end user calls; never invoke internal implementations, package-private methods, or bypass the public surface directly
5353
10. **Always review and extend tests** - Review existing unit and functional tests before making changes; every code change must include new or enhanced tests that cover the affected behavior
54+
11. **Every code touch must update all tests for the changed class** - When a class is modified, find and update every test that covers it — unit, integration, and TCK. Do not leave any existing test out of sync with the new code.
55+
12. **Clean violations before commit** - Before every automated commit, run `./gradlew clean test aggregateTestFailures --continue` from the root and ensure that `TEST_FAILURES.md` reports no issues and is removed.
5456

5557
## Available Skills
5658

@@ -229,8 +231,9 @@ class MyService { }
229231
1. **Fork & branch** from the target release branch (e.g., `7.0.x`)
230232
2. **Run tests** before submitting: `./gradlew build --rerun-tasks`
231233
3. **Run code style checks**: `./gradlew codeStyle`
232-
4. **Squash commits** into a single meaningful commit message
233-
5. **Reference issues** in PR description (e.g., "Fixes #1234")
234+
4. **Clean style violations**: Before committing, run `./gradlew clean aggregateStyleViolations` from the root and ensure that `CHECKSTYLE_VIOLATIONS.md`, `CODENARC_VIOLATIONS.md`, and `PMD_VIOLATIONS.md` have no issues.
235+
5. **Squash commits** into a single meaningful commit message
236+
6. **Reference issues** in PR description (e.g., "Fixes #1234")
234237

235238
### Review Process
236239

build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsTestPlugin.groovy

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,10 @@ class GrailsTestPlugin implements Plugin<Project> {
5050
slurper.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false)
5151
slurper.setFeature("http://xml.org/sax/features/namespaces", false)
5252

53-
project.subprojects.each { subproject ->
54-
def testResultsDir = subproject.layout.buildDirectory.dir("test-results").get().asFile
55-
if (testResultsDir.exists()) {
53+
def processedDirs = [] as Set
54+
55+
def processTestResults = { File testResultsDir, String moduleName ->
56+
if (testResultsDir.exists() && processedDirs.add(testResultsDir.absolutePath)) {
5657
testResultsDir.eachFileRecurse { file ->
5758
if (file.name.endsWith('.xml') && file.name.startsWith('TEST-')) {
5859
try {
@@ -61,7 +62,7 @@ class GrailsTestPlugin implements Plugin<Project> {
6162
if (testcase.failure.size() > 0 || testcase.error.size() > 0) {
6263
def failure = testcase.failure.size() > 0 ? testcase.failure[0] : testcase.error[0]
6364
failures << [
64-
module: subproject.name,
65+
module: moduleName,
6566
className: testcase.@classname.text(),
6667
testName: testcase.@name.text(),
6768
message: failure.@message.text() ?: failure.text().take(200),
@@ -77,6 +78,24 @@ class GrailsTestPlugin implements Plugin<Project> {
7778
}
7879
}
7980

81+
// 1. All subprojects
82+
project.subprojects.each { subproject ->
83+
def testResultsDir = subproject.layout.buildDirectory.dir("test-results").get().asFile
84+
processTestResults(testResultsDir, subproject.name)
85+
}
86+
87+
// 2. Any other directory in root that might have test results (like grails-docs or build-logic)
88+
project.layout.projectDirectory.asFile.eachDir { dir ->
89+
if (!dir.name.startsWith('.') && dir.name != 'build' && dir.name != 'node_modules') {
90+
def testResultsDir = new File(dir, "build/test-results")
91+
processTestResults(testResultsDir, dir.name)
92+
93+
// Also check for results in the directory itself (if it's a build output)
94+
def directTestResultsDir = new File(dir, "test-results")
95+
processTestResults(directTestResultsDir, dir.name)
96+
}
97+
}
98+
8099
writeReport(project, failures)
81100
}
82101
}

grails-data-hibernate5/core/src/test/groovy/grails/gorm/specs/WhereQueryOldIssueVerificationSpec.groovy

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,6 @@
1818
*/
1919
package grails.gorm.tests
2020

21-
import spock.lang.IgnoreIf
22-
import spock.lang.PendingFeatureIf
23-
2421
import grails.gorm.annotation.Entity
2522
import grails.gorm.hibernate.HibernateEntity
2623
import grails.gorm.transactions.Rollback
@@ -160,7 +157,6 @@ class WhereQueryOldIssueVerificationSpec extends Specification {
160157

161158
@Rollback
162159
@Issue('https://github.com/apache/grails-core/issues/14636')
163-
@PendingFeatureIf({ System.getProperty('hibernate5.gorm.suite') })
164160
def "many-to-many queries with sorting do not throw exception"() {
165161
given: "users and roles in a many-to-many relationship"
166162
def role1 = new WqRole(name: "ADMIN").save(flush: true)

grails-data-hibernate7/HIBERNATE7-BINDING.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ The `bindProperty` method in `GrailsPropertyBinder.java` has been successfully r
8282
* **IdentityBinder**: Main coordinator for binding entity identifiers (simple or composite).
8383
* **SimpleIdBinder**: Binds simple primary keys.
8484
* **CompositeIdBinder**: Binds composite primary keys.
85-
* **BasicValueIdCreator**: Factory for creating identifier `Value` objects and their generators.
85+
* **BasicValueCreator**: Factory for creating identifier `Value` objects and their generators.
8686
* **VersionBinder**: Binds the version property used for optimistic locking.
8787
* **NaturalIdentifierBinder**: Binds properties marked as `naturalId`.
8888

@@ -163,7 +163,7 @@ The `bindProperty` method in `GrailsPropertyBinder.java` has been successfully r
163163

164164
| Class | Status | Notes |
165165
| :--- | :--- | :--- |
166-
| `CollectionSecondPassBinder` | Migrated | Contains TODOs for unidirectional many-to-many. |
166+
| `CollectionSecondPassBinder` | Migrated | Unidirectional many-to-many support implemented. |
167167
| `GrailsSecondPass` | Migrated | |
168168
| `ListSecondPass` | Migrated | |
169169
| `ListSecondPassBinder` | Migrated | |
@@ -197,7 +197,7 @@ The `bindProperty` method in `GrailsPropertyBinder.java` has been successfully r
197197
| `TableForManyCalculator` | Migrated | |
198198
| `UniqueNameGenerator` | Migrated | |
199199
| `BackticksRemover` | Migrated | |
200-
| `BasicValueIdCreator` | Migrated | |
200+
| `BasicValueCreator` | Migrated | |
201201

202202
## Utility Class Refactoring & Mock Compatibility
203203

@@ -216,6 +216,9 @@ The `bindProperty` method in `GrailsPropertyBinder.java` has been successfully r
216216

217217
## Remaining Known Issues / TODOs
218218

219-
- `CollectionSecondPassBinder`: TODO support unidirectional many-to-many.
220219
- `GrailsIncrementGenerator`: Reflection hacks for Hibernate 7 (scheduled for removal in Hibernate 8).
221-
- **Multitenancy & CompositeId:** While many tests are passing, some complex scenarios in `MultiTenancyBidirectionalManyToManySpec` and `GlobalConstraintWithCompositeIdSpec` may still require attention or further validation in a full application context.
220+
221+
## Resolved Issues
222+
223+
- `CollectionSecondPassBinder`: Unidirectional many-to-many support implemented.
224+
- **Multitenancy & CompositeId:** `MultiTenancyBidirectionalManyToManySpec` and `GlobalConstraintWithCompositeIdSpec` have been fixed and validated.

grails-data-hibernate7/HIBERNATE_7_MIGRATION.md

Lines changed: 0 additions & 40 deletions
This file was deleted.

grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormInstanceApi.groovy

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -156,13 +156,7 @@ class HibernateGormInstanceApi<D> extends GormInstanceApi<D> {
156156
validateable.skipValidation(true)
157157

158158
try {
159-
String idPropertyName = domainClass.identity?.name ?: 'id'
160-
Object idVal = InvokerHelper.getProperty(target, idPropertyName)
161-
if (idVal == null) {
162-
return performPersist(target, shouldFlush)
163-
} else {
164-
return performMerge(target, shouldFlush)
165-
}
159+
return performUpsert(target, shouldFlush)
166160
} finally {
167161
validateable.skipValidation(false)
168162
}
@@ -240,14 +234,33 @@ class HibernateGormInstanceApi<D> extends GormInstanceApi<D> {
240234
return instance
241235
}
242236

237+
protected D performUpsert(D target, boolean shouldFlush) {
238+
PersistentEntity entity = persistentEntity
239+
String idPropertyName = entity.identity?.name ?: 'id'
240+
Object idVal = InvokerHelper.getProperty(target, idPropertyName)
241+
if (idVal == null) {
242+
return performPersist(target, shouldFlush)
243+
} else {
244+
return performMerge(target, shouldFlush)
245+
}
246+
}
247+
243248
protected D performMerge(final D target, final boolean flush) {
244249
hibernateTemplate.execute { Session session ->
245-
Object merged = session.merge(target)
250+
D merged = (D) session.merge(target)
246251
session.lock(merged, LockModeType.NONE)
252+
// Sync id back immediately so target has an identity
253+
String idProp = persistentEntity.identity?.name ?: 'id'
254+
InvokerHelper.setProperty(target, idProp, InvokerHelper.getProperty(merged, idProp))
247255
if (flush) {
248256
flushSession session
249257
}
250-
return (D) merged
258+
// Sync version after flush so the incremented value is captured
259+
PersistentProperty versionProperty = persistentEntity.version
260+
if (versionProperty != null) {
261+
InvokerHelper.setProperty(target, versionProperty.name, InvokerHelper.getProperty(merged, versionProperty.name))
262+
}
263+
return target
251264
}
252265
}
253266

grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormStaticApi.groovy

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,11 @@ class HibernateGormStaticApi<D> extends GormStaticApi<D> {
123123
(GormStaticApi<D>) HibernateGormEnhancer.findStaticApi(persistentClass, qualifier)
124124
}
125125

126+
@Override
127+
D merge(D d) {
128+
instanceApi.merge(d)
129+
}
130+
126131
@Override
127132
<T> T withNewSession(Closure<T> callable) {
128133
if (persistentEntity.isMultiTenant()) {

grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/CompositeIdentity.groovy renamed to grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/HibernateCompositeIdentity.groovy

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ import org.hibernate.MappingException
4242

4343
import org.grails.datastore.mapping.config.Property
4444
import org.grails.orm.hibernate.cfg.domainbinding.hibernate.GrailsHibernatePersistentEntity
45-
import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateIdentity
45+
import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernatePropertyIdentity
4646
import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernatePersistentProperty
4747

4848
/**
@@ -54,7 +54,7 @@ import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernatePersistentP
5454
@AutoClone
5555
@Builder(builderStrategy = SimpleStrategy, prefix = '')
5656
@CompileStatic
57-
class CompositeIdentity extends Property implements HibernateIdentity {
57+
class HibernateCompositeIdentity extends Property implements HibernatePropertyIdentity {
5858

5959
/**
6060
* The property names that make up the custom identity
@@ -74,7 +74,7 @@ class CompositeIdentity extends Property implements HibernateIdentity {
7474
* @param naturalIdDef The callable
7575
* @return This id
7676
*/
77-
CompositeIdentity naturalId(@DelegatesTo(NaturalId) Closure naturalIdDef) {
77+
HibernateCompositeIdentity naturalId(@DelegatesTo(NaturalId) Closure naturalIdDef) {
7878
this.natural = new NaturalId()
7979
naturalIdDef.setDelegate(this.natural)
8080
naturalIdDef.setResolveStrategy(Closure.DELEGATE_ONLY)

grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/Identity.groovy renamed to grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/HibernateSimpleIdentity.groovy

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ import org.springframework.validation.DataBinder
4242

4343
import org.grails.datastore.mapping.config.Property
4444
import org.grails.orm.hibernate.cfg.domainbinding.generator.GrailsSequenceGeneratorEnum
45-
import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateIdentity
45+
import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernatePropertyIdentity
4646

4747
/**
4848
* Defines the identity generation strategy. In the case of a 'composite' identity the properties
@@ -53,7 +53,7 @@ import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateIdentity
5353
*/
5454
@CompileStatic
5555
@Builder(builderStrategy = SimpleStrategy, prefix = '')
56-
class Identity extends Property implements HibernateIdentity {
56+
class HibernateSimpleIdentity extends Property implements HibernatePropertyIdentity {
5757

5858
/**
5959
* The generator to use
@@ -92,7 +92,7 @@ class Identity extends Property implements HibernateIdentity {
9292
return useSequence ? GrailsSequenceGeneratorEnum.SEQUENCE_IDENTITY.toString() : GrailsSequenceGeneratorEnum.NATIVE.toString()
9393
}
9494

95-
static String determineGeneratorName(Identity mappedId, boolean useSequence) {
95+
static String determineGeneratorName(HibernateSimpleIdentity mappedId, boolean useSequence) {
9696
if (mappedId != null) {
9797
return mappedId.determineGeneratorName(useSequence)
9898
}
@@ -104,7 +104,7 @@ class Identity extends Property implements HibernateIdentity {
104104
* @param naturalIdDef The callable
105105
* @return This id
106106
*/
107-
Identity naturalId(@DelegatesTo(NaturalId) Closure naturalIdDef) {
107+
HibernateSimpleIdentity naturalId(@DelegatesTo(NaturalId) Closure naturalIdDef) {
108108
this.natural = new NaturalId()
109109
naturalIdDef.setDelegate(this.natural)
110110
naturalIdDef.setResolveStrategy(Closure.DELEGATE_ONLY)
@@ -120,8 +120,8 @@ class Identity extends Property implements HibernateIdentity {
120120
* @param config The configuration
121121
* @return The new instance
122122
*/
123-
static Identity configureNew(@DelegatesTo(Identity) Closure config) {
124-
Identity property = new Identity()
123+
static HibernateSimpleIdentity configureNew(@DelegatesTo(HibernateSimpleIdentity) Closure config) {
124+
HibernateSimpleIdentity property = new HibernateSimpleIdentity()
125125
return configureExisting(property, config)
126126
}
127127

@@ -131,7 +131,7 @@ class Identity extends Property implements HibernateIdentity {
131131
* @param config The configuration
132132
* @return The new instance
133133
*/
134-
static Identity configureExisting(Identity property, Map config) {
134+
static HibernateSimpleIdentity configureExisting(HibernateSimpleIdentity property, Map config) {
135135
DataBinder dataBinder = new DataBinder(property)
136136
dataBinder.bind(new MutablePropertyValues(config))
137137
return property
@@ -142,7 +142,7 @@ class Identity extends Property implements HibernateIdentity {
142142
* @param config The configuration
143143
* @return The new instance
144144
*/
145-
static Identity configureExisting(Identity property, @DelegatesTo(Identity) Closure config) {
145+
static HibernateSimpleIdentity configureExisting(HibernateSimpleIdentity property, @DelegatesTo(HibernateSimpleIdentity) Closure config) {
146146
config.setDelegate(property)
147147
config.setResolveStrategy(Closure.DELEGATE_ONLY)
148148
config.call()

0 commit comments

Comments
 (0)