diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index b13790bf310..4f01b4da71e 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -235,15 +235,17 @@ jobs: path: grails-forge/tmp1/cli/**/* if-no-files-found: 'error' functional: - name: "Functional Tests (Java ${{ matrix.java }}, indy=${{ matrix.indy }})" + name: "Functional Tests (Java ${{ matrix.java }}, Hibernate ${{ matrix.hibernate-version }}, indy=${{ matrix.indy }})" if: ${{ !contains(github.event.head_commit.message, '[skip tests]') }} strategy: fail-fast: false matrix: java: [ 21, 25 ] + hibernate-version: [ '5', '7' ] indy: [ false ] include: - java: 21 + hibernate-version: '5' indy: true runs-on: ubuntu-24.04 steps: @@ -264,6 +266,8 @@ jobs: - name: "πŸ” Setup TestLens" uses: testlens-app/setup-testlens@d96a555133c275a00949d2cc77b70fe9a4242ebf # v1.9.2 - name: "πŸƒ Run Functional Tests" + env: + GITHUB_MAVEN_PASSWORD: ${{ secrets.GITHUB_TOKEN }} run: > ./gradlew bootJar check --continue @@ -276,6 +280,8 @@ jobs: -PskipHibernate5Tests -PskipHibernate7Tests -PskipMongodbTests + -PhibernateVersion=${{ matrix.hibernate-version }} + -PskipHibernate${{ matrix.hibernate-version == '5' && '7' || '5' }}Tests mongodbFunctional: if: ${{ !contains(github.event.head_commit.message, '[skip tests]') }} name: "Mongodb Functional Tests (Java ${{ matrix.java }}, MongoDB ${{ matrix.mongodb-version }}, indy=${{ matrix.indy }})" @@ -436,7 +442,7 @@ jobs: name: grails-gradle-artifacts.txt path: grails-gradle/build/grails-gradle-artifacts.txt publish: - needs: [ publishGradle, build, functional, hibernate5Functional, mongodbFunctional ] + needs: [ publishGradle, build, functional, hibernate5Functional, hibernate7Functional, mongodbFunctional ] if: >- ${{ always() && github.repository_owner == 'apache' && @@ -444,7 +450,8 @@ jobs: needs.publishGradle.result == 'success' && (needs.build.result == 'success' || needs.build.result == 'skipped') && (needs.functional.result == 'success' || needs.functional.result == 'skipped') && - (needs.hibernate5Functional.result == 'success' || needs.hibernate5Functional.result == 'skipped') && + (needs.hibernate5Functional.result == 'success' || needs.hibernate5Functional.result == 'skipped') && + (needs.hibernate7Functional.result == 'success' || needs.hibernate7Functional.result == 'skipped') && (needs.mongodbFunctional.result == 'success' || needs.mongodbFunctional.result == 'skipped') }} runs-on: ubuntu-24.04 diff --git a/H7_GORM_BUG_REPORT.md b/H7_GORM_BUG_REPORT.md new file mode 100644 index 00000000000..9bb00a663f6 --- /dev/null +++ b/H7_GORM_BUG_REPORT.md @@ -0,0 +1,99 @@ + + +## Historical H7 `gorm` Functional Test Failures - Bug Report + +This historical report captured the `grails-test-examples-gorm` failures that drove the Hibernate 7 fixes in this branch. Some entries below have since been fixed by this branch, so treat this file as triage rationale rather than current expected test status. + +Before those fixes, running `grails-test-examples-gorm` with `-PhibernateVersion=7` produced 13 failures across 4 specs. Below are the 5 distinct root causes. + +--- + +### Bug 1 (Intentional) - `executeQuery` / `executeUpdate` plain String blocked + +| | | +|---|---| +| **Tests** | `test basic HQL query`, `test HQL aggregate functions`, `test HQL group by`, `test executeUpdate for bulk operations` | +| **Spec** | `GormCriteriaQueriesSpec` | +| **Error** | `UnsupportedOperationException: executeQuery(CharSequence) only accepts a Groovy GString with interpolated parameters` | + +**Description:** H7 intentionally rejects `executeQuery("from Book where inStock = true")` when no parameters are passed. The same tightening was already applied to `executeUpdate`. Callers must use `executeQuery('...', [:])` or a GString with interpolated params. + +> This is by design. The test bodies need to adopt the parameterized form - not a GORM bug. + +--- + +### Bug 2 (Fixed) - `DetachedCriteria.get()` throws `NonUniqueResultException` instead of returning first result + +| | | +|---|---| +| **Test** | `test detached criteria as reusable query` | +| **Spec** | `GormCriteriaQueriesSpec:454` | +| **Error** | `jakarta.persistence.NonUniqueResultException: Query did not return a unique result: 2 results were returned` | + +**Description:** H5 `DetachedCriteria.get()` returned the first matching row when multiple rows existed. H7's `AbstractSelectionQuery.getSingleResult()` is now strict and throws if the result is not unique. + +**Expected fix:** `HibernateQueryExecutor.singleResult()` should apply `setMaxResults(1)` before calling `getSingleResult()`, or switch to `getResultList().stream().findFirst()`. + +> **Status:** Fixed. Caught exceptions are updated to check for `jakarta.persistence.NonUniqueResultException` to return the first result, and the tests pass. + +--- + +### Bug 3 (Fixed) - `Found two representations of same collection: gorm.Author.books` + +| | | +|---|---| +| **Tests** | `test saving child with belongsTo saves parent reference`, `test dirty checking with associations`, `test belongsTo allows orphan removal`, `test updating multiple children`, `test addTo creates bidirectional link` | +| **Spec** | `GormCascadeOperationsSpec` | +| **Error** | `HibernateSystemException: Found two representations of same collection: gorm.Author.books` | + +**Description:** H7 enforces stricter collection identity. After `author.addToBooks(book); author.save(flush: true)`, the session contains two references to the same `Author.books` collection, causing a `HibernateException` on flush. H5 tolerated this. + +**Expected fix:** GORM's `addTo*` / cascade-flush path in `grails-data-hibernate7` must synchronize both sides of the bidirectional association and merge/evict stale collection snapshots before flushing. + +> **Status:** Fixed. Cascade flush issues and collection representations are now resolved, and `GormCascadeOperationsSpec` passes 100%. + +--- + +### Bug 4 (Fixed) - `@Query` aggregate functions fail with type mismatch + +| | | +|---|---| +| **Tests** | `test findAveragePrice`, `test findMaxPageCount` | +| **Spec** | `GormDataServicesSpec` | +| **Errors** | `Incorrect query result type: query produces 'java.lang.Double' but type 'java.lang.Long' was given` / `query produces 'java.lang.Integer' but type 'java.lang.Long' was given` | + +**Description:** `HibernateHqlQuery.buildQuery()` always calls `session.createQuery(hql, ctx.targetClass())`. For aggregate HQL (`select avg(b.price) ...`, `select max(b.pageCount) ...`), the query does not return an entity, but `ctx.targetClass()` returns the entity class (e.g., `Book`). H7's `SqmQueryImpl` enforces strict result-type alignment: `avg()` produces `Double`, `max(pageCount)` produces `Integer`, neither is coercible to the bound entity type. + +**Expected fix:** `HibernateHqlQuery.buildQuery()` must detect non-entity HQL (aggregates / projections) and call the untyped `session.createQuery(hql)` in those cases, letting GORM handle result casting downstream. + +> **Status:** Fixed. Aggregate return types are now dynamically resolved, matching the expected Java type of the aggregate function itself. + +--- + +### Bug 5 (Fixed) - `where { pageCount > price * 10 }` fails with `CoercionException` + +| | | +|---|---| +| **Test** | `test where query comparing two properties` | +| **Spec** | `GormWhereQueryAdvancedSpec:175` | +| **Error** | `org.hibernate.type.descriptor.java.CoercionException: Error coercing value` | + +**Description:** A where-DSL closure comparing an `Integer` property (`pageCount`) to an arithmetic expression involving a `BigDecimal` property (`price * 10`) worked in H5. H7's SQM type system no longer allows implicit coercion between `Integer` and `BigDecimal` in a comparison predicate. + +**Expected fix:** The GORM where-query-to-SQM translator should emit an explicit `CAST` in the SQM tree when the two operands of a comparison have different numeric types. + +> **Status:** Fixed. Type coercion in comparison predicates is now handled, and `GormWhereQueryAdvancedSpec` passes 100%. diff --git a/dependencies.gradle b/dependencies.gradle index 572636c436a..295c491b7e8 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -220,8 +220,11 @@ ext { customBomVersions = [ 'cache-ri-impl.version' : '1.1.1', 'derby.version' : '10.17.1.0', - 'hibernate-models.version' : '1.0.1', - 'hibernate.version' : '7.2.5.Final', + // Spring Boot 4.1 can select Hibernate ORM 7.4.x through Spring Dependency + // Management, and Hibernate ORM 7.4.x requires hibernate-models 1.1.x. + 'hibernate-models.version' : '1.1.1', + 'hibernate-tools.version' : '7.3.8.Final', + 'hibernate.version' : '7.4.1.Final', 'jandex.version' : '3.2.3', 'liquibase-hibernate.version' : '4.27.0', 'liquibase-test-harness.version': '1.0.11', @@ -239,8 +242,8 @@ ext { 'hibernate-envers' : "org.hibernate.orm:hibernate-envers:${combinedVersions['hibernate.version']}", 'hibernate-jcache' : "org.hibernate.orm:hibernate-jcache:${combinedVersions['hibernate.version']}", 'hibernate-models' : "org.hibernate.models:hibernate-models:${combinedVersions['hibernate-models.version']}", - 'hibernate-tools-orm' : "org.hibernate.tool:hibernate-tools-orm:${combinedVersions['hibernate.version']}", - 'hibernate-tools-utils' : "org.hibernate.tool:hibernate-tools-utils:${combinedVersions['hibernate.version']}", + 'hibernate-tools-orm' : "org.hibernate.tool:hibernate-tools-orm:${combinedVersions['hibernate-tools.version']}", + 'hibernate-tools-utils' : "org.hibernate.tool:hibernate-tools-utils:${combinedVersions['hibernate-tools.version']}", 'jandex' : "io.smallrye:jandex:${combinedVersions['jandex.version']}", 'liquibase' : "org.liquibase:liquibase:${combinedVersions['liquibase.version']}", 'liquibase-cdi' : "org.liquibase:liquibase-cdi:${combinedVersions['liquibase.version']}", @@ -313,8 +316,9 @@ ext { customBomVersions = [ 'cache-ri-impl.version' : '1.1.1', 'groovy.version' : '5.0.5', - 'hibernate-models.version' : '1.0.1', - 'hibernate.version' : '7.2.5.Final', + 'hibernate-models.version' : '1.1.1', + 'hibernate-tools.version' : '7.3.8.Final', + 'hibernate.version' : '7.4.1.Final', 'jandex.version' : '3.2.3', 'liquibase-hibernate.version' : '4.27.0', 'liquibase-test-harness.version': '1.0.11', @@ -360,8 +364,8 @@ ext { 'hibernate-envers' : "org.hibernate.orm:hibernate-envers:${combinedVersions['hibernate.version']}", 'hibernate-jcache' : "org.hibernate.orm:hibernate-jcache:${combinedVersions['hibernate.version']}", 'hibernate-models' : "org.hibernate.models:hibernate-models:${combinedVersions['hibernate-models.version']}", - 'hibernate-tools-orm' : "org.hibernate.tool:hibernate-tools-orm:${combinedVersions['hibernate.version']}", - 'hibernate-tools-utils' : "org.hibernate.tool:hibernate-tools-utils:${combinedVersions['hibernate.version']}", + 'hibernate-tools-orm' : "org.hibernate.tool:hibernate-tools-orm:${combinedVersions['hibernate-tools.version']}", + 'hibernate-tools-utils' : "org.hibernate.tool:hibernate-tools-utils:${combinedVersions['hibernate-tools.version']}", 'jandex' : "io.smallrye:jandex:${combinedVersions['jandex.version']}", 'liquibase' : "org.liquibase:liquibase:${combinedVersions['liquibase.version']}", 'liquibase-cdi' : "org.liquibase:liquibase-cdi:${combinedVersions['liquibase.version']}", diff --git a/gradle/functional-test-config.gradle b/gradle/functional-test-config.gradle index 7f7e708a2a4..b4a4ec5aeaa 100644 --- a/gradle/functional-test-config.gradle +++ b/gradle/functional-test-config.gradle @@ -21,6 +21,46 @@ rootProject.subprojects .findAll { !(it.name in testProjects) && !(it.name in docProjects) && !(it.name in cliProjects) } .each { project.evaluationDependsOn(it.path) } +// Determine which Hibernate version to use for general functional tests. +// Pass -PhibernateVersion=7 to run general functional tests against Hibernate 7 instead of 5. +// Only '5' and '7' are supported; any other value fails the build fast to catch typos. +def targetHibernateVersion = (project.findProperty('hibernateVersion') ?: '5').toString() +if (!(targetHibernateVersion in ['5', '7'])) { + throw new GradleException( + "Unsupported hibernateVersion '${targetHibernateVersion}'. Expected '5' or '7'.") +} +boolean isHibernate5LabeledProject = project.name.startsWith('grails-test-examples-hibernate5') +boolean isHibernate7LabeledProject = project.name.startsWith('grails-test-examples-hibernate7') +boolean isHibernateSpecificProject = isHibernate5LabeledProject || isHibernate7LabeledProject +boolean isMongoProject = project.name.startsWith('grails-test-examples-mongodb') +boolean isGeneralFunctionalTest = !isHibernateSpecificProject && !isMongoProject + +// General functional test projects that cannot run under Hibernate 7 via dependency substitution. +// The datasources project has partial H7-compatible coverage in +// grails-test-examples/hibernate7/grails-multiple-datasources, but still includes H5-only +// coverage that needs explicit H7 duplicates before the general project can be enabled. +// The app1 and GraphQL functional projects currently fail under H7 substitution and need +// dedicated H7 follow-up coverage before they can be enabled in this lane. +// The views and scaffolding projects boot with H7 substitution, but their functional specs +// currently fail on association rendering and unique-constraint behavior under Hibernate 7. +List h7IncompatibleProjects = [ + 'grails-test-examples-app1', + 'grails-test-examples-datasources', + 'grails-test-examples-graphql-grails-docs-app', + 'grails-test-examples-graphql-grails-multi-datastore-app', + 'grails-test-examples-graphql-grails-tenant-app', + 'grails-test-examples-graphql-grails-test-app', + 'grails-test-examples-graphql-spring-boot-app', + 'grails-test-examples-views-functional-tests', + 'grails-test-examples-scaffolding-fields', +] + +// Redirect the default grails-bom to grails-hibernate7-bom for general functional tests when +// running with -PhibernateVersion=7, so consumers get the Hibernate 7 version constraints +// (hibernate.version=7.2.7.Final and related). The default grails-bom and grails-hibernate5-bom +// ship the Hibernate 5 constraints. +def redirectBomToH7 = isGeneralFunctionalTest && targetHibernateVersion == '7' && !(project.name in h7IncompatibleProjects) + configurations.configureEach { resolutionStrategy.dependencySubstitution { // Test projects will often include dependencies from local projects. This will ensure any dependencies @@ -32,7 +72,8 @@ configurations.configureEach { //TODO: This does not handle libraries that are both test fixtures & a libraries like grails-data-mongodb, // see grails-test-examples-mongodb-base, & grails-test-examples-mongodb-hibernate5 for project() workaround if (possibleProject.name == 'grails-bom') { - substitute module(substitutedArtifact) using platform(project(':grails-bom')) + def targetBom = redirectBomToH7 ? ':grails-hibernate7-bom' : ':grails-bom' + substitute module(substitutedArtifact) using platform(project(targetBom)) } else if(possibleProject.name == 'grails-geb') { def selector = it.variant(module(substitutedArtifact)) { VariantSelectionDetails details -> @@ -51,6 +92,44 @@ configurations.configureEach { } } } + + // For general (non-hibernate-labeled) functional test projects, redirect Hibernate 5 dependencies + // to Hibernate 7 projects when -PhibernateVersion=7 is set. These rules are added after the loop + // so they override the default substitutions for the h5 modules. + // Projects in h7IncompatibleProjects are excluded since they use H5-specific GORM APIs. + if (redirectBomToH7) { + substitute module('org.apache.grails:grails-data-hibernate5') using project(':grails-data-hibernate7') + substitute module('org.apache.grails:grails-data-hibernate5-spring-boot') using project(':grails-data-hibernate7-spring-boot') + } + } + + // Exclude Hibernate 5-specific runtime dependencies when testing general projects with Hibernate 7. + // These libraries have no Hibernate 7 equivalent and would cause classpath conflicts. + if (isGeneralFunctionalTest && targetHibernateVersion == '7' && !(project.name in h7IncompatibleProjects)) { + exclude group: 'org.hibernate', module: 'hibernate-ehcache' + exclude group: 'org.jboss.spec.javax.transaction', module: 'jboss-transaction-api_1.3_spec' + } +} + +// For general functional test projects running against Hibernate 7, attach the Hibernate 7 BOM as a +// platform on the dependency buckets the test projects use. The test project's own build.gradle +// imports platform(project(':grails-bom')) which lacks Hibernate 7 version constraints +// (hibernate-models, jandex, hibernate-tools-orm, etc.). Attaching the H7 BOM here means we don't +// have to edit each test project's build.gradle, and the H7 constraints take precedence because the +// dependency-substitution above already swaps grails-data-hibernate5 -> grails-data-hibernate7 in +// the same path. +if (isGeneralFunctionalTest && targetHibernateVersion == '7' && !(project.name in h7IncompatibleProjects)) { + def addH7Platform = { String confName -> + def conf = configurations.findByName(confName) + if (conf != null) { + conf.dependencies.add(dependencies.platform(dependencies.project(path: ':grails-hibernate7-bom'))) + } + } + plugins.withId('java') { + ['implementation', 'compileOnly', 'runtimeOnly', + 'testImplementation', 'testCompileOnly', 'testRuntimeOnly', + 'integrationTestImplementation', 'integrationTestRuntimeOnly', + 'testAndDevelopmentOnly'].each(addH7Platform) } } @@ -147,4 +226,4 @@ tasks.withType(Test).configureEach { Test task -> tasks.named('groovydoc').configure { // We don't need to generate docs for test projects enabled = false -} \ No newline at end of file +} diff --git a/grails-data-hibernate5/core/src/test/groovy/grails/gorm/tests/CompositeIdWithJoinTableSpec.groovy b/grails-data-hibernate5/core/src/test/groovy/grails/gorm/tests/CompositeIdWithJoinTableSpec.groovy index 4845fa59c37..306b0974d08 100644 --- a/grails-data-hibernate5/core/src/test/groovy/grails/gorm/tests/CompositeIdWithJoinTableSpec.groovy +++ b/grails-data-hibernate5/core/src/test/groovy/grails/gorm/tests/CompositeIdWithJoinTableSpec.groovy @@ -19,9 +19,10 @@ package grails.gorm.tests -import org.jetbrains.annotations.NotNull - import grails.gorm.annotation.Entity +import grails.gorm.specs.HibernateGormDatastoreSpec + +import jakarta.annotation.Nonnull import static grails.gorm.hibernate.mapping.MappingBuilder.define @@ -70,7 +71,7 @@ class CompositeIdParent implements Serializable, Comparable { } @Override - int compareTo(@NotNull CompositeIdParent o) { + int compareTo(@Nonnull CompositeIdParent o) { this.name <=> o.name ?: this.last <=> o.last } } @@ -90,4 +91,4 @@ class CompositeIdChild implements Comparable { int compareTo(CompositeIdChild other) { foo <=> other.foo } -} \ No newline at end of file +} diff --git a/grails-data-hibernate5/core/src/test/groovy/grails/gorm/tests/CompositeIdWithManyToOneAndSequenceSpec.groovy b/grails-data-hibernate5/core/src/test/groovy/grails/gorm/tests/CompositeIdWithManyToOneAndSequenceSpec.groovy index 960cacc7af6..f8a2b5a6213 100644 --- a/grails-data-hibernate5/core/src/test/groovy/grails/gorm/tests/CompositeIdWithManyToOneAndSequenceSpec.groovy +++ b/grails-data-hibernate5/core/src/test/groovy/grails/gorm/tests/CompositeIdWithManyToOneAndSequenceSpec.groovy @@ -20,10 +20,10 @@ package grails.gorm.tests import grails.gorm.annotation.Entity +import grails.gorm.specs.HibernateGormDatastoreSpec import grails.gorm.transactions.Rollback import jakarta.annotation.Nonnull -//import org.jetbrains.annotations.NotNull import spock.lang.Issue /** @@ -116,4 +116,4 @@ class ToothDisease implements Serializable, Comparable { result = 31 * result + nrVersion.hashCode() return result } -} \ No newline at end of file +} diff --git a/grails-data-hibernate5/core/src/test/groovy/grails/gorm/tests/IdentityEnumTypeSpec.groovy b/grails-data-hibernate5/core/src/test/groovy/grails/gorm/tests/IdentityEnumTypeSpec.groovy index 78ce9601875..ef28b4b38b5 100644 --- a/grails-data-hibernate5/core/src/test/groovy/grails/gorm/tests/IdentityEnumTypeSpec.groovy +++ b/grails-data-hibernate5/core/src/test/groovy/grails/gorm/tests/IdentityEnumTypeSpec.groovy @@ -19,19 +19,20 @@ package grails.gorm.tests import grails.gorm.annotation.Entity +import grails.gorm.specs.HibernateGormDatastoreSpec import grails.gorm.transactions.Rollback import jakarta.persistence.Enumerated import jakarta.persistence.EnumType import org.grails.orm.hibernate.cfg.IdentityEnumType import org.hibernate.HibernateException import org.hibernate.MappingException -import org.hibernate.type.descriptor.WrapperOptions +import org.hibernate.engine.spi.SharedSessionContractImplementor import javax.sql.DataSource import java.sql.ResultSet /** - * Tests for IdentityEnumType in Hibernate 7. + * Tests for IdentityEnumType in Hibernate 5. */ class IdentityEnumTypeSpec extends HibernateGormDatastoreSpec { @@ -65,8 +66,6 @@ class IdentityEnumTypeSpec extends HibernateGormDatastoreSpec { FooWithEnum.first().mySuperValue == XEnum.X__TWO } - // ── Direct unit tests for IdentityEnumType ──────────────────────────────── - def "setParameterValues initializes enumClass"() { given: def type = new IdentityEnumType() @@ -78,7 +77,7 @@ class IdentityEnumTypeSpec extends HibernateGormDatastoreSpec { then: type.returnedClass() == IdentityStatusEnum - type.getSqlType() != 0 + type.sqlTypes()[0] != 0 } def "setParameterValues throws MappingException for enum without getId method"() { @@ -160,13 +159,14 @@ class IdentityEnumTypeSpec extends HibernateGormDatastoreSpec { props.setProperty(IdentityEnumType.PARAM_ENUM_CLASS, IdentityStatusEnum.name) type.setParameterValues(props) def rs = Mock(java.sql.ResultSet) - def options = Mock(WrapperOptions) + def session = manager.sessionFactory.currentSession as SharedSessionContractImplementor when: - def res = type.nullSafeGet(rs, 1, options) + def res = type.nullSafeGet(rs, ['status'] as String[], session, null) then: - 1 * rs.getString(1) >> null + 1 * rs.getString('status') >> null + 1 * rs.wasNull() >> true res == null } @@ -177,14 +177,14 @@ class IdentityEnumTypeSpec extends HibernateGormDatastoreSpec { props.setProperty(IdentityEnumType.PARAM_ENUM_CLASS, IdentityStatusEnum.name) type.setParameterValues(props) def rs = Mock(java.sql.ResultSet) - def options = Mock(WrapperOptions) + def session = manager.sessionFactory.currentSession as SharedSessionContractImplementor when: - def res = type.nullSafeGet(rs, 1, options) + def res = type.nullSafeGet(rs, ['status'] as String[], session, null) then: - 1 * rs.getString(1) >> "A" - (0..1) * rs.wasNull() >> false + 1 * rs.getString('status') >> "A" + 2 * rs.wasNull() >> false res == IdentityStatusEnum.ACTIVE } @@ -195,10 +195,10 @@ class IdentityEnumTypeSpec extends HibernateGormDatastoreSpec { props.setProperty(IdentityEnumType.PARAM_ENUM_CLASS, IdentityStatusEnum.name) type.setParameterValues(props) def st = Mock(java.sql.PreparedStatement) - def options = Mock(WrapperOptions) + def session = manager.sessionFactory.currentSession as SharedSessionContractImplementor when: - type.nullSafeSet(st, null, 1, options) + type.nullSafeSet(st, null, 1, session) then: 1 * st.setNull(1, _) @@ -211,10 +211,10 @@ class IdentityEnumTypeSpec extends HibernateGormDatastoreSpec { props.setProperty(IdentityEnumType.PARAM_ENUM_CLASS, IdentityStatusEnum.name) type.setParameterValues(props) def st = Mock(java.sql.PreparedStatement) - def options = Mock(WrapperOptions) + def session = manager.sessionFactory.currentSession as SharedSessionContractImplementor when: - type.nullSafeSet(st, IdentityStatusEnum.INACTIVE, 1, options) + type.nullSafeSet(st, IdentityStatusEnum.INACTIVE, 1, session) then: 1 * st.setString(1, "I") diff --git a/grails-data-hibernate5/core/src/test/groovy/grails/gorm/tests/validation/SaveWithInvalidEntitySpec.groovy b/grails-data-hibernate5/core/src/test/groovy/grails/gorm/tests/validation/SaveWithInvalidEntitySpec.groovy index a4cc516df7b..b0b50d455b5 100644 --- a/grails-data-hibernate5/core/src/test/groovy/grails/gorm/tests/validation/SaveWithInvalidEntitySpec.groovy +++ b/grails-data-hibernate5/core/src/test/groovy/grails/gorm/tests/validation/SaveWithInvalidEntitySpec.groovy @@ -52,8 +52,7 @@ class SaveWithInvalidEntitySpec extends Specification { then: Exception e = thrown() - // In Hibernate 7, a veto results in EntityActionVetoException (translated to HibernateSystemException) - e.getClass().simpleName in ['HibernateSystemException', 'IllegalStateException'] + e.getClass().simpleName in ['EntityActionVetoException', 'HibernateSystemException', 'IllegalStateException'] b.hasErrors() b.errors.hasFieldErrors('field1') } diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateAttachSupport.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateAttachSupport.java new file mode 100644 index 00000000000..ef69625ee9a --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateAttachSupport.java @@ -0,0 +1,80 @@ +/* + * 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.hibernate.LockMode; +import org.hibernate.Session; +import org.hibernate.TransientObjectException; +import org.hibernate.engine.internal.ForeignKeys; +import org.hibernate.engine.internal.Versioning; +import org.hibernate.engine.spi.EntityKey; +import org.hibernate.engine.spi.PersistenceContext; +import org.hibernate.engine.spi.SessionImplementor; +import org.hibernate.engine.spi.Status; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.type.TypeHelper; + +final class HibernateAttachSupport { + + private HibernateAttachSupport() { + } + + static void attach(Object entity, Session session) { + if (session.contains(entity)) { + return; + } + + SessionImplementor sessionImplementor = (SessionImplementor) session; + PersistenceContext persistenceContext = sessionImplementor.getPersistenceContextInternal(); + Object target = persistenceContext.unproxyAndReassociate(entity); + if (persistenceContext.getEntry(target) != null) { + return; + } + + EntityPersister persister = sessionImplementor.getEntityPersister(null, target); + Object identifier = persister.getIdentifier(target, sessionImplementor); + if (!ForeignKeys.isNotTransient(persister.getEntityName(), target, Boolean.FALSE, sessionImplementor)) { + throw new TransientObjectException("cannot attach an unsaved transient instance: " + persister.getEntityName()); + } + + EntityKey entityKey = sessionImplementor.generateEntityKey(identifier, persister); + persistenceContext.checkUniqueness(entityKey, target); + + Object[] loadedState = persister.getPropertyValues(target); + TypeHelper.deepCopy( + loadedState, + persister.getPropertyTypes(), + persister.getPropertyUpdateability(), + loadedState, + sessionImplementor); + Object version = Versioning.getVersion(loadedState, persister); + + persistenceContext.addEntity( + target, + persister.isMutable() ? Status.MANAGED : Status.READ_ONLY, + loadedState, + entityKey, + version, + LockMode.NONE, + true, + persister, + false); + persister.afterReassociate(target, sessionImplementor); + } +} 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 f838a8fed3d..4034b5c1d96 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 @@ -226,9 +226,10 @@ class HibernateGormInstanceApi extends GormInstanceApi { @Override D attach(D instance) { - return (D) hibernateTemplate.execute { Session session -> - return session.merge(instance) + hibernateTemplate.execute { Session session -> + HibernateAttachSupport.attach(instance, session) } + return instance } @Override 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 4a47afc391e..a2039c4173d 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 @@ -52,6 +52,7 @@ import org.grails.datastore.gorm.finders.FinderMethod 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.PersistentProperty import org.grails.datastore.mapping.query.api.BuildableCriteria as GrailsCriteria import org.grails.datastore.mapping.query.event.PostQueryEvent import org.grails.datastore.mapping.query.event.PreQueryEvent @@ -60,6 +61,7 @@ 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.support.HibernateRuntimeUtils @@ -351,24 +353,59 @@ class HibernateGormStaticApi extends GormStaticApi { @Override D findWhere(Map queryMap, Map args) { if (!queryMap) return null - Map coercedMap = queryMap.collectEntries { k, v -> [k.toString(), v] } - String hql = buildWhereHql(coercedMap) - doSingleInternal(hql, coercedMap, [], args, false) + executeSingleHqlQuery(prepareWhereHqlQuery(queryMap, buildFindWhereArgs(args))) } @Override List findAllWhere(Map queryMap, Map args) { if (!queryMap) return null - Map coercedMap = queryMap.collectEntries { k, v -> [k.toString(), v] } - String hql = buildWhereHql(coercedMap) - doListInternal(hql, coercedMap, [], args, false) + executeListHqlQuery(prepareWhereHqlQuery(queryMap, buildQuerySettings(args))) + } + + private GormQuery prepareWhereHqlQuery(Map queryMap, Map querySettings) { + Map coercedMap = buildQuerySettings(queryMap) + return prepareHqlQuery( + buildWhereHql(coercedMap), + false, + false, + buildWhereParams(coercedMap), + Collections.emptyList(), + querySettings + ) } - private String buildWhereHql(Map queryMap) { - String whereClause = queryMap.keySet().collect { Object key -> "$key = :$key" }.join(' and ') + private String buildWhereHql(Map queryMap) { + 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" } + private String validateWherePropertyName(String propertyName) { + PersistentProperty property = persistentEntity.getPropertyByName(propertyName) + if (property == null || property.name != propertyName) { + throw new IllegalArgumentException("Property [$propertyName] is not a valid property of ${persistentEntity.name}") + } + return propertyName + } + + private static Map buildWhereParams(Map queryMap) { + queryMap.findAll { String key, Object value -> value != null } as Map + } + + private static Map buildFindWhereArgs(Map args) { + Map queryArgs = buildQuerySettings(args) + queryArgs[HibernateQueryArgument.MAX.value()] = 1 + return queryArgs + } + + private static Map buildQuerySettings(Map args) { + Map queryArgs = new LinkedHashMap<>() + args?.each { Object key, Object value -> queryArgs[key.toString()] = value } + return queryArgs + } + @Override List executeQuery(CharSequence query, Map namedParams, Map args) { doListInternal(query, namedParams, [], args, false) @@ -392,7 +429,7 @@ class HibernateGormStaticApi extends GormStaticApi { List convertedIds = ids.collect { HibernateRuntimeUtils.convertValueToType(it, idType, conversionService) } List results = doListInternal("from $entity where $idName in (:ids)" as String, [ids: convertedIds], [], [:], false) Map byId = results.collectEntries { [(it[idName]): it] } - ids.collect { byId[it] } + convertedIds.collect { byId[it] } } @Override @@ -404,8 +441,12 @@ class HibernateGormStaticApi extends GormStaticApi { Map namedParams, Collection positionalParams, Map args - , boolean isNative) { - def hqlQuery = prepareHqlQuery(hql, isNative, false, namedParams, positionalParams, args) + , boolean isNative) { + GormQuery hqlQuery = prepareHqlQuery(hql, isNative, false, namedParams, positionalParams, args) + executeListHqlQuery(hqlQuery) + } + + protected List executeListHqlQuery(GormQuery hqlQuery) { firePreQueryEvent() def ds = (List) hqlQuery.list() firePostQueryEvent(ds) @@ -418,7 +459,12 @@ class HibernateGormStaticApi extends GormStaticApi { Collection positionalParams, Map args, Map hints = [:], boolean isNative ) { - def hqlQuery = prepareHqlQuery(hql, isNative, false, namedParams, positionalParams, args) + GormQuery hqlQuery = prepareHqlQuery(hql, isNative, false, namedParams, positionalParams, args) + executeSingleHqlQuery(hqlQuery) + } + + @SuppressWarnings('GroovyAssignabilityCheck') + private D executeSingleHqlQuery(GormQuery hqlQuery) { firePreQueryEvent() def sm = hqlQuery.singleResult() firePostQueryEvent(sm) 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 c43d7a191c5..08b45e0836f 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 @@ -155,7 +155,10 @@ public void refresh(Object o) { @Override public void attach(Object o) { - hibernateTemplate.lock(o, LockMode.NONE); + ((GrailsHibernateTemplate) hibernateTemplate).execute(session -> { + HibernateAttachSupport.attach(o, session); + return null; + }); } @Override diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/CollectionBinder.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/CollectionBinder.java index d78797ac64e..1f353fbdf28 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/CollectionBinder.java +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/CollectionBinder.java @@ -163,7 +163,7 @@ private void bindCollectionTable(HibernateToManyProperty property, Collection co String catalogName = tableForManyCalculator.getJoinTableCatalog(property); collection.setCollectionTable( - mappings.addTable(schemaName, catalogName, tableName, null, false, metadataBuildingContext)); + mappings.addTable(schemaName, catalogName, tableName, null, false, metadataBuildingContext, false)); collection.setInverse(property.isBidirectional() && !property.isOwningSide()); } diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/CompositeIdentifierToManyToOneBinder.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/CompositeIdentifierToManyToOneBinder.java index ee120ffecff..5b2ed19d4b2 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/CompositeIdentifierToManyToOneBinder.java +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/CompositeIdentifierToManyToOneBinder.java @@ -27,6 +27,7 @@ import org.hibernate.boot.spi.MetadataBuildingContext; import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; +import org.hibernate.mapping.Column; import org.hibernate.mapping.SimpleValue; import org.grails.orm.hibernate.cfg.ColumnConfig; @@ -102,6 +103,13 @@ public void bindCompositeIdentifierToManyToOne( .forEach(columns::add); } simpleValueBinder.bindSimpleValue(property, null, value, path); + refDomainClass.sortOrIndexForeignKeyColumns(value); + List referencedColumns = refDomainClass.getReferencedIdentifierColumns(propertyNames); + if (!referencedColumns.isEmpty() && + value.createForeignKeyOfEntity(refDomainClass.getName(), referencedColumns) != null) { + value.disableForeignKey(); + } + property.markValueSorted(value); } /** diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/JoinedSubClassBinder.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/JoinedSubClassBinder.java index e09b4803bf4..85ce3ea8031 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/JoinedSubClassBinder.java +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/JoinedSubClassBinder.java @@ -85,7 +85,8 @@ public JoinedSubclass bindJoinedSubClass(GrailsHibernatePersistentEntity sub, Pe getJoinedSubClassTableName(sub, joinedSubclass), null, false, - metadataBuildingContext); + metadataBuildingContext, + false); joinedSubclass.setTable(mytable); if (LOG.isInfoEnabled()) { diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/RootPersistentClassCommonValuesBinder.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/RootPersistentClassCommonValuesBinder.java index dd56e38cbe1..e9aa851c580 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/RootPersistentClassCommonValuesBinder.java +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/RootPersistentClassCommonValuesBinder.java @@ -91,7 +91,8 @@ public RootClass bindRoot(@Nonnull HibernatePersistentEntity hibernatePersistent hibernatePersistentEntity.getTableName(namingStrategy), null, hibernatePersistentEntity.isTableAbstract(), - metadataBuildingContext); + metadataBuildingContext, + false); root.setTable(table); if (LOG.isDebugEnabled()) { LOG.debug("[GrailsDomainBinder] Mapping Grails domain class: {} -> {}", hibernatePersistentEntity.getName(), root.getTable().getName()); diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/SimpleIdBinder.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/SimpleIdBinder.java index 261cf1ece17..3f9885f1407 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/SimpleIdBinder.java +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/SimpleIdBinder.java @@ -23,10 +23,8 @@ import org.hibernate.MappingException; import org.hibernate.boot.spi.MetadataBuildingContext; import org.hibernate.mapping.BasicValue; -import org.hibernate.mapping.PrimaryKey; import org.hibernate.mapping.Property; import org.hibernate.mapping.RootClass; -import org.hibernate.mapping.Table; import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernatePersistentEntity; import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateSimpleIdentityProperty; @@ -75,8 +73,6 @@ public void bindSimpleId(@Nonnull HibernatePersistentEntity persistentEntity) { // set identifier property rootClass.setIdentifierProperty(prop); - Table pkTable = id.getTable(); - pkTable.setPrimaryKey(new PrimaryKey(pkTable)); return; } throw new MappingException("Invalid simple id binding for entity [" + persistentEntity.getName() + "]"); diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/GrailsHibernatePersistentEntity.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/GrailsHibernatePersistentEntity.java index c2f94508f6c..570097983a8 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/GrailsHibernatePersistentEntity.java +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/GrailsHibernatePersistentEntity.java @@ -18,6 +18,8 @@ */ package org.grails.orm.hibernate.cfg.domainbinding.hibernate; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Objects; @@ -30,7 +32,11 @@ import org.hibernate.FetchMode; import org.hibernate.boot.spi.InFlightMetadataCollector; +import org.hibernate.mapping.Column; +import org.hibernate.mapping.Component; +import org.hibernate.mapping.KeyValue; import org.hibernate.mapping.PersistentClass; +import org.hibernate.mapping.SimpleValue; import org.grails.datastore.mapping.model.PersistentEntity; import org.grails.datastore.mapping.model.PersistentProperty; @@ -366,4 +372,62 @@ default boolean isLazy(HibernatePersistentProperty property) { }) .orElseGet(() -> property instanceof HibernateAssociation); } + + /** + * Sorts or indexes the columns of {@code value} to align with this entity's composite + * identifier order. When the identifier is a {@link Component} with an established sort order, + * delegates to {@link SimpleValue#sortColumns(int[])}. Otherwise assigns sequential + * {@link Column#setTypeIndex(int)} values so Hibernate can correlate them. + * + * @param value the foreign-key {@link SimpleValue} whose columns should be aligned + */ + default void sortOrIndexForeignKeyColumns(SimpleValue value) { + PersistentClass pc = getPersistentClass(); + KeyValue identifier = pc != null ? pc.getIdentifier() : null; + int[] originalOrder = identifier instanceof Component c ? c.sortProperties() : null; + if (originalOrder != null) { + value.sortColumns(originalOrder); + } else { + List cols = value.getColumns(); + for (int i = 0; i < cols.size(); i++) { + cols.get(i).setTypeIndex(i); + } + } + } + + /** + * Returns the identifier columns for the given {@code propertyNames} in the order that aligns + * with the sorted foreign-key layout produced by {@link #sortOrIndexForeignKeyColumns}. + *

+ * When the identifier is a {@link Component}, columns are gathered per property name and then + * reordered according to the same permutation used during {@link Component#sortProperties()}. + * When the identifier is a plain {@link KeyValue}, its columns are returned directly. + * + * @param propertyNames composite identity property names in the caller's declared order + * @return identifier columns aligned with the foreign-key column layout, or an empty list if + * this entity has no persistent class or identifier + */ + default List getReferencedIdentifierColumns(String[] propertyNames) { + PersistentClass pc = getPersistentClass(); + KeyValue identifier = pc != null ? pc.getIdentifier() : null; + if (identifier == null) { + return List.of(); + } + if (!(identifier instanceof Component component)) { + return identifier.getColumns(); + } + int[] originalOrder = component.sortProperties(); + List referencedColumns = Arrays.stream(propertyNames) + .flatMap(name -> component.getProperty(name).getValue().getColumns().stream()) + .collect(Collectors.toCollection(ArrayList::new)); + return originalOrder != null ? sortedByPermutation(referencedColumns, originalOrder) : referencedColumns; + } + + private static List sortedByPermutation(List columns, int[] permutation) { + List result = new ArrayList<>(columns); + for (int i = 0; i < permutation.length; i++) { + result.set(permutation[i], columns.get(i)); + } + return result; + } } diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateCompositeIdentityProperty.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateCompositeIdentityProperty.java index d742accc40b..3577fa615ce 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateCompositeIdentityProperty.java +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateCompositeIdentityProperty.java @@ -48,3 +48,4 @@ public HibernatePersistentProperty[] getParts() { return parts; } } + diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernatePersistentProperty.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernatePersistentProperty.java index a6011131dba..051f5e05b93 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernatePersistentProperty.java +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernatePersistentProperty.java @@ -18,7 +18,10 @@ */ package org.grails.orm.hibernate.cfg.domainbinding.hibernate; +import java.util.LinkedHashMap; +import java.util.Map; import java.util.Optional; +import java.util.Properties; import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.mapping.DependantValue; @@ -26,12 +29,14 @@ import org.hibernate.mapping.Property; import org.hibernate.mapping.SimpleValue; import org.hibernate.mapping.Table; +import org.hibernate.mapping.ToOne; import org.hibernate.usertype.UserCollectionType; import org.grails.datastore.mapping.model.PersistentProperty; import org.grails.datastore.mapping.model.types.Association; import org.grails.datastore.mapping.model.types.Embedded; import org.grails.orm.hibernate.cfg.ColumnConfig; +import org.grails.orm.hibernate.cfg.HibernateSimpleIdentity; import org.grails.orm.hibernate.cfg.Mapping; import org.grails.orm.hibernate.cfg.PropertyConfig; @@ -266,4 +271,47 @@ default String getNameForPropertyAndPath(String path) { } return getName(); } + + /** + * Builds a {@link HibernateSimpleIdentity} from this property's own mapped form, for use + * when the owning entity has no explicit simple identity configured. Returns + * {@link Optional#empty()} when the mapped form is absent, {@code typeParams} is {@code null}, + * or {@code typeParams} is empty. + * + * @return an {@link Optional} containing the constructed identity, or empty if the property + * carries no generator type parameters + */ + default Optional buildPropertyIdentity() { + PropertyConfig mappedForm = getHibernateMappedForm(); + Properties typeParams = mappedForm != null ? mappedForm.getTypeParams() : null; + if (typeParams == null || typeParams.isEmpty()) { + return Optional.empty(); + } + Map params = new LinkedHashMap<>(); + typeParams.forEach((key, value) -> params.put(key.toString(), value.toString())); + HibernateSimpleIdentity identity = new HibernateSimpleIdentity(); + identity.setName(getName()); + identity.setType(getType()); + identity.setParams(params); + return Optional.of(identity); + } + + /** + * Marks {@code value} as sorted when the Hibernate value type requires it. Called after binding + * so that column ordering aligns with the referenced composite identifier. + *

+ * The default handles both {@link ToOne} and {@link DependantValue} β€” the two value types that + * require sorted columns for composite foreign keys β€” because these can be produced by different + * property types (e.g. a {@link HibernateToManyProperty} can produce a {@link DependantValue} + * as its collection key). Subtypes may override to add property-specific behaviour. + * + * @param value the Hibernate {@link SimpleValue} produced for this property + */ + default void markValueSorted(SimpleValue value) { + if (value instanceof ToOne toOne) { + toOne.setSorted(true); + } else if (value instanceof DependantValue dv) { + dv.setSorted(true); + } + } } diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateToOneProperty.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateToOneProperty.java index 8bab1c953ae..6de594dd8c4 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateToOneProperty.java +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateToOneProperty.java @@ -23,3 +23,4 @@ * HibernateOneToOneProperty}). Parallel to {@link HibernateToManyProperty}. */ public interface HibernateToOneProperty extends HibernateAssociation {} + diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/BidirectionalOneToManyLinker.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/BidirectionalOneToManyLinker.java index 57c2a2f0506..c929e04e5c0 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/BidirectionalOneToManyLinker.java +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/BidirectionalOneToManyLinker.java @@ -58,5 +58,6 @@ public void link( key.addColumn(mappingColumn); key.getTable().addColumn(mappingColumn); } + key.sortProperties(); } } diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/PrimaryKeyValueCreator.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/PrimaryKeyValueCreator.java index 0cdab8c00ef..285eec80541 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/PrimaryKeyValueCreator.java +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/PrimaryKeyValueCreator.java @@ -20,7 +20,6 @@ import org.hibernate.boot.spi.MetadataBuildingContext; import org.hibernate.mapping.Collection; -import org.hibernate.mapping.Component; import org.hibernate.mapping.DependantValue; import org.hibernate.mapping.KeyValue; @@ -48,8 +47,7 @@ public DependantValue createPrimaryKeyValue(Collection collection) { key.setNullable(true); key.setUpdateable(true); - // JPA now requires to check for sorting - key.setSorted(collection.isSorted() || (keyValue instanceof Component)); + key.setSorted(collection.isSorted()); return key; } } diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/util/BasicValueCreator.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/util/BasicValueCreator.java index 9938b76d269..35c8b52adce 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/util/BasicValueCreator.java +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/util/BasicValueCreator.java @@ -69,17 +69,22 @@ public BasicValue bindBasicValue(HibernatePersistentProperty property) { BasicValue basicValue = new BasicValue(metadataBuildingContext, property.getTable()); Optional.ofNullable(property.getGeneratorName()).ifPresent(generator -> basicValue.setCustomIdGeneratorCreator(context -> createGenerator( + property, property.getHibernateOwner(), - context.getValue() == null ? new GeneratorCreationContextWrapper(context, basicValue) : context, + new GeneratorCreationContextWrapper(context, basicValue), generator))); return basicValue; } private Generator createGenerator( + HibernatePersistentProperty property, GrailsHibernatePersistentEntity domainClass, GeneratorCreationContext context, String generatorName) { HibernateSimpleIdentity mappedId = domainClass.getHibernateIdentity() instanceof HibernateSimpleIdentity id ? id : null; + if (mappedId == null) { + mappedId = property.buildPropertyIdentity().orElse(null); + } return grailsSequenceWrapper.getGenerator( generatorName, context, mappedId, domainClass, jdbcEnvironment, namingStrategy); } diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/proxy/ByteBuddyGroovyInterceptor.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/proxy/ByteBuddyGroovyInterceptor.java index 95a1f629d0e..8d913d745f6 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/proxy/ByteBuddyGroovyInterceptor.java +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/proxy/ByteBuddyGroovyInterceptor.java @@ -18,15 +18,12 @@ */ package org.grails.orm.hibernate.proxy; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor; import org.hibernate.type.CompositeType; -import static org.hibernate.internal.util.ReflectHelper.isPublic; - /** * A ByteBuddy interceptor that avoids initializing the proxy for Groovy-specific methods. * @@ -84,31 +81,6 @@ public Object intercept(Object proxy, Method method, Object[] args) throws Throw } } - final Object result = this.invoke(method, args, proxy); - if (result != INVOKE_IMPLEMENTATION) { // NOPMD: sentinel comparison - return result; - } - - if (GroovyProxyInterceptorLogic.isGroovyMethod(methodName)) { - if (isUninitialized()) { - // If we reach here, it's a Groovy method but handleUninitialized didn't catch it. - // We should still avoid getImplementation() if uninitialized. - Object uninitializedResult = GroovyProxyInterceptorLogic.handleUninitialized(state, methodName, args); - if (uninitializedResult != GroovyProxyInterceptorLogic.INVOKE_IMPLEMENTATION) { - return uninitializedResult; - } - } - - final Object target = getImplementation(); - try { - if (!isPublic(getPersistentClass(), method)) { - method.setAccessible(true); // NOPMD: accessibility alteration - } - return method.invoke(target, args); - } catch (InvocationTargetException ite) { - throw ite.getTargetException(); - } - } - return super.intercept(proxy, method, args); + return this.invoke(method, args, proxy); } } diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HqlQueryMethods.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HqlQueryMethods.java index 3f3559fad22..9d80213d417 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HqlQueryMethods.java +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HqlQueryMethods.java @@ -25,6 +25,8 @@ import java.util.Map; import java.util.Set; +import jakarta.persistence.LockModeType; + public interface HqlQueryMethods { Set INTERNAL_SETTINGS = Set.of( @@ -34,7 +36,8 @@ public interface HqlQueryMethods { HibernateQueryArgument.READ_ONLY.value(), HibernateQueryArgument.FETCH_SIZE.value(), HibernateQueryArgument.MAX.value(), - HibernateQueryArgument.OFFSET.value() + HibernateQueryArgument.OFFSET.value(), + HibernateQueryArgument.LOCK.value() ); default void populateQuerySettings(HqlQueryDelegate d, Map args) { @@ -43,14 +46,40 @@ default void populateQuerySettings(HqlQueryDelegate d, Map args) d.setQueryFlushMode(GrailsQueryFlushMode.mapToHibernateQueryFlushMode(args.get(HibernateQueryArgument.FLUSH_MODE.value()))); } if (args.containsKey(HibernateQueryArgument.MAX.value())) { - d.setMaxResults((Integer) args.get(HibernateQueryArgument.MAX.value())); + d.setMaxResults(toInteger(args.get(HibernateQueryArgument.MAX.value()))); } if (args.containsKey(HibernateQueryArgument.OFFSET.value())) { - d.setFirstResult((Integer) args.get(HibernateQueryArgument.OFFSET.value())); + d.setFirstResult(toInteger(args.get(HibernateQueryArgument.OFFSET.value()))); + } + if (args.containsKey(HibernateQueryArgument.FETCH_SIZE.value())) { + d.setFetchSize(toInteger(args.get(HibernateQueryArgument.FETCH_SIZE.value()))); + } + if (args.containsKey(HibernateQueryArgument.TIMEOUT.value())) { + d.setTimeout(toInteger(args.get(HibernateQueryArgument.TIMEOUT.value()))); } if (args.containsKey(HibernateQueryArgument.READ_ONLY.value())) { - d.setReadOnly((Boolean) args.get(HibernateQueryArgument.READ_ONLY.value())); + d.setReadOnly(toBoolean(args.get(HibernateQueryArgument.READ_ONLY.value()))); + } + if (toBoolean(args.get(HibernateQueryArgument.LOCK.value()))) { + d.setLockMode(LockModeType.PESSIMISTIC_WRITE); + d.setCacheable(false); + } else if (args.containsKey(HibernateQueryArgument.CACHE.value())) { + d.setCacheable(toBoolean(args.get(HibernateQueryArgument.CACHE.value()))); + } + } + + static int toInteger(Object value) { + if (value instanceof Number number) { + return number.intValue(); + } + return Integer.parseInt(value.toString()); + } + + static boolean toBoolean(Object value) { + if (value instanceof Boolean bool) { + return bool; } + return value != null && Boolean.parseBoolean(value.toString()); } default void populateHints(HqlQueryDelegate d, Map hints) { diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/PredicateGenerator.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/PredicateGenerator.java index 157d8b6271f..fd5bfb6ffae 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/PredicateGenerator.java +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/PredicateGenerator.java @@ -288,10 +288,7 @@ private Predicate handleJunction( } else if (junction instanceof Query.Disjunction) { return criteriaBuilder.or(predicates); } else if (junction instanceof Query.Negation) { - if (predicates.length > 1) { - throw new IllegalArgumentException("Negation does not support multiple predicates in this context. Use conjunction or disjunction within negation."); - } - return criteriaBuilder.not(criteriaBuilder.and(predicates)); + return criteriaBuilder.not(criteriaBuilder.or(predicates)); } throw new IllegalArgumentException("Unsupported junction: " + junction); } 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 eadc7c8071e..76f27fcd654 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 @@ -96,7 +96,7 @@ public Integer getMax() { return max; } Object m = queryContext.querySettings().get(HibernateQueryArgument.MAX.value()); - return m instanceof Number n ? n.intValue() : -1; + return m == null ? -1 : HqlQueryMethods.toInteger(m); } @Override @@ -105,6 +105,6 @@ public Integer getOffset() { return offset; } Object o = queryContext.querySettings().get(HibernateQueryArgument.OFFSET.value()); - return o instanceof Number n ? n.intValue() : 0; + return o == null ? 0 : HqlQueryMethods.toInteger(o); } } diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/CompositeIdWithManyToOneAndSequenceSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/CompositeIdWithManyToOneAndSequenceSpec.groovy index 7d7890fbbec..ce2971070df 100644 --- a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/CompositeIdWithManyToOneAndSequenceSpec.groovy +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/CompositeIdWithManyToOneAndSequenceSpec.groovy @@ -58,7 +58,7 @@ class Tooth { ToothDisease toothDisease static mapping = { table name: 'AK_TOOTH' - id generator: 'sequence', params: [sequence: 'SEQ_AK_TOOTH'] + id generator: 'sequence', params: [sequence_name: 'SEQ_AK_TOOTH'] toothDisease { column name: 'FK_AK_TOOTH_ID' column name: 'FK_AK_TOOTH_NR_VERSION' @@ -72,8 +72,8 @@ class ToothDisease implements Serializable { Integer nrVersion static mapping = { table name: 'AK_TOOTH_DISEASE' - idColumn column: 'ID', generator: 'sequence', params: [sequence: 'SEQ_AK_TOOTH_DISEASE'] + idColumn column: 'ID', generator: 'sequence', params: [sequence_name: 'SEQ_AK_TOOTH_DISEASE'] nrVersion column: 'NR_VERSION' id composite: ['idColumn', 'nrVersion'] } -} \ No newline at end of file +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/Hibernate7OptimisticLockingSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/Hibernate7OptimisticLockingSpec.groovy index 88d1e22bdb7..c07e077b94d 100644 --- a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/Hibernate7OptimisticLockingSpec.groovy +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/Hibernate7OptimisticLockingSpec.groovy @@ -18,6 +18,7 @@ */ package grails.gorm.tests +import grails.gorm.specs.HibernateGormDatastoreSpec import org.apache.grails.data.testing.tck.domains.OptLockNotVersioned import org.apache.grails.data.testing.tck.domains.OptLockVersioned import org.springframework.dao.OptimisticLockingFailureException @@ -27,10 +28,6 @@ import org.springframework.dao.OptimisticLockingFailureException */ class Hibernate7OptimisticLockingSpec extends HibernateGormDatastoreSpec { - def setupSpec() { - manager.addAllDomainClasses([OptLockVersioned, OptLockNotVersioned]) - } - void "Test versioning"() { given: def o = new OptLockVersioned(name: 'locked') diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/SequenceIdSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/SequenceIdSpec.groovy index 10e822d857c..22ac7543366 100644 --- a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/SequenceIdSpec.groovy +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/SequenceIdSpec.groovy @@ -45,9 +45,12 @@ class SequenceIdSpec extends Specification { then:"The entity was saved" BookWithSequence.first() - ((SessionImplementor)datastore.sessionFactory.currentSession).connection().prepareStatement("call NEXT VALUE FOR book_seq;") - .executeQuery() - .next() + SessionImplementor sessionImplementor = datastore.sessionFactory.currentSession as SessionImplementor + sessionImplementor.doReturningWork { connection -> + connection.prepareStatement("call NEXT VALUE FOR book_seq;") + .executeQuery() + .next() + } } } @Entity @@ -62,4 +65,4 @@ class BookWithSequence { id generator:'sequence', params:[sequence:'book_seq'] id index:'book_id_idx' } -} \ No newline at end of file +} diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/TablePerSubClassAndEmbeddedSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/TablePerSubClassAndEmbeddedSpec.groovy index 18df3004907..ec5d052b953 100644 --- a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/TablePerSubClassAndEmbeddedSpec.groovy +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/TablePerSubClassAndEmbeddedSpec.groovy @@ -18,12 +18,14 @@ */ package grails.gorm.tests +import groovy.lang.GroovyClassLoader + import grails.gorm.DetachedCriteria import grails.gorm.annotation.Entity +import grails.gorm.specs.HibernateGormDatastoreSpec import grails.gorm.transactions.Rollback import org.grails.datastore.gorm.query.criteria.DetachedAssociationCriteria import org.grails.datastore.gorm.query.transform.ApplyDetachedCriteriaTransform -import spock.lang.Ignore /** * Created by graemerocher on 04/11/16. diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/UniqueWithMultipleDataSourcesSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/UniqueWithMultipleDataSourcesSpec.groovy index 46d1c653981..5dfdf7589d3 100644 --- a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/UniqueWithMultipleDataSourcesSpec.groovy +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/UniqueWithMultipleDataSourcesSpec.groovy @@ -19,11 +19,11 @@ package grails.gorm.tests import grails.gorm.annotation.Entity +import grails.gorm.specs.HibernateGormDatastoreSpec import grails.gorm.transactions.Rollback -import org.grails.datastore.mapping.core.Session import org.grails.datastore.mapping.core.connections.ConnectionSource import org.hibernate.dialect.H2Dialect -import spock.lang.* +import spock.lang.Issue /** * Created by graemerocher on 17/02/2017. 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 58fc8936b01..d1e665b7676 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 @@ -85,6 +85,9 @@ class HibernateUpdateFromListenerSpec extends Specification { if (event.entityObject instanceof Person) { Person person = (Person) event.entityObject person.occupation = person.occupation + " listener" + if (event.getEntityAccess() != null) { + event.getEntityAccess().setProperty("occupation", person.occupation) + } } isExecuted = true } diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/hasmany/Something.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/hasmany/Something.groovy index 3512510941a..cc06d86bfc0 100644 --- a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/hasmany/Something.groovy +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/hasmany/Something.groovy @@ -19,6 +19,8 @@ package grails.gorm.specs.hasmany +import grails.gorm.tests.hasmany.Book + class Something { public static void main(String[] args) { diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/hibernatequery/PredicateGeneratorSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/hibernatequery/PredicateGeneratorSpec.groovy index 369642c3944..a0748a74904 100644 --- a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/hibernatequery/PredicateGeneratorSpec.groovy +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/hibernatequery/PredicateGeneratorSpec.groovy @@ -338,7 +338,7 @@ class PredicateGeneratorSpec extends HibernateGormDatastoreSpec { predicates.length == 1 } - def "test getPredicates with Negation throws when multiple predicates"() { + def "test getPredicates with Negation supports multiple predicates"() { given: def negation = new Query.Negation() negation.add(new Query.Equals("firstName", "Alice")) @@ -346,10 +346,10 @@ class PredicateGeneratorSpec extends HibernateGormDatastoreSpec { List criteria = [negation] when: - predicateGenerator.getPredicates(query, root, criteria, fromProvider, personEntity) + def predicates = predicateGenerator.getPredicates(query, root, criteria, fromProvider, personEntity) then: - thrown(RuntimeException) + predicates.length == 1 } def "test getPredicates with invalid property throws ConfigurationException"() { diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/multitenancy/MultiTenancyBidirectionalManyToManySpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/multitenancy/MultiTenancyBidirectionalManyToManySpec.groovy index 4b79f42e664..ea7c9759ca5 100644 --- a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/multitenancy/MultiTenancyBidirectionalManyToManySpec.groovy +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/multitenancy/MultiTenancyBidirectionalManyToManySpec.groovy @@ -20,6 +20,10 @@ package grails.gorm.tests.multitenancy import grails.gorm.transactions.Rollback +import grails.gorm.specs.multitenancy.Department +import grails.gorm.specs.multitenancy.DepartmentService +import grails.gorm.specs.multitenancy.User +import grails.gorm.specs.multitenancy.UserService import org.grails.datastore.mapping.core.DatastoreUtils import org.grails.datastore.mapping.multitenancy.MultiTenancySettings import org.grails.datastore.mapping.multitenancy.resolvers.SystemPropertyTenantResolver @@ -57,7 +61,7 @@ class MultiTenancyBidirectionalManyToManySpec extends Specification { void setup() { System.setProperty(SystemPropertyTenantResolver.PROPERTY_NAME, "oci") - datastore = new HibernateDatastore(DatastoreUtils.createPropertyResolver(config), getClass().getPackage()) + datastore = new HibernateDatastore(DatastoreUtils.createPropertyResolver(config), Department.getPackage()) departmentService = datastore.getService(DepartmentService) userService = datastore.getService(UserService) } @@ -86,4 +90,4 @@ class MultiTenancyBidirectionalManyToManySpec extends Specification { department.users.size() } -} \ No newline at end of file +} 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 d0f1afa6879..e7cbe911652 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 @@ -186,6 +186,53 @@ class HibernateGormInstanceApiSpec extends HibernateGormDatastoreSpec { found.name == 'Fred' } + @Rollback + def "attach() does not overwrite a newer version of the row (optimistic lock preserved)"() { + given: "a versioned person is saved" + def person = new PersonInstanceApi(name: 'Original', age: 30) + person.save(flush: true) + def id = person.id + + and: "the row is updated to a newer version while a stale detached copy is held" + manager.session.clear() + def fresh = PersonInstanceApi.get(id) + fresh.name = 'Updated by someone else' + fresh.save(flush: true) + manager.session.clear() + + when: "the stale detached instance is attached and the session is flushed" + person.attach() + manager.session.flush() + manager.session.clear() + + then: "attach() did not push the stale state over the newer row" + def reloaded = PersonInstanceApi.get(id) + reloaded.name == 'Updated by someone else' + reloaded.version == 1 + } + + @Rollback + def "attach() on a deleted detached instance does not resurrect the row"() { + given: "a saved person whose row is then deleted" + def person = new PersonInstanceApi(name: 'Ghost', age: 50) + person.save(flush: true) + def id = person.id + person.delete(flush: true) + manager.session.clear() + + expect: "the row is gone" + PersonInstanceApi.get(id) == null + + when: "the detached instance is attached and the session is flushed" + person.attach() + manager.session.flush() + manager.session.clear() + + then: "no row was resurrected in the database" + PersonInstanceApi.get(id) == null + PersonInstanceApi.count() == 0 + } + @Rollback def "merge on new instance assigns id and sets version to 0"() { given: diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormStaticApiFindWhereSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormStaticApiFindWhereSpec.groovy new file mode 100644 index 00000000000..228d0dde644 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormStaticApiFindWhereSpec.groovy @@ -0,0 +1,82 @@ +/* + * 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 java.util.Locale + +import grails.gorm.annotation.Entity +import grails.gorm.transactions.Rollback +import org.grails.datastore.mapping.core.DatastoreUtils +import org.grails.orm.hibernate.cfg.Settings +import org.hibernate.resource.jdbc.spi.StatementInspector +import org.springframework.transaction.PlatformTransactionManager +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification + +class HibernateGormStaticApiFindWhereSpec extends Specification { + + @Shared SqlCapture sqlCapture = new SqlCapture() + + @Shared @AutoCleanup HibernateDatastore hibernateDatastore = new HibernateDatastore( + DatastoreUtils.createPropertyResolver( + (Settings.SETTING_DB_CREATE): 'create-drop', + 'hibernate.session_factory.statement_inspector': sqlCapture + ), + FindWhereLimitEntity + ) + @Shared PlatformTransactionManager transactionManager = hibernateDatastore.getTransactionManager() + + @Rollback + void 'findWhere limits duplicate matches to one row'() { + given: + new FindWhereLimitEntity(name: 'duplicate').save(flush: true, failOnError: true) + new FindWhereLimitEntity(name: 'duplicate').save(flush: true, failOnError: true) + sqlCapture.clear() + + when: + FindWhereLimitEntity result = FindWhereLimitEntity.findWhere(name: 'duplicate') + + then: + result.name == 'duplicate' + sqlCapture.statements.any { String sql -> + String normalized = sql.toLowerCase(Locale.ENGLISH) + normalized.contains('where') && normalized.contains('fetch first') && normalized.contains('rows only') + } + } + + static class SqlCapture implements StatementInspector { + final List statements = Collections.synchronizedList(new ArrayList()) + + @Override + String inspect(String sql) { + statements.add(sql) + return sql + } + + void clear() { + statements.clear() + } + } +} + +@Entity +class FindWhereLimitEntity { + String name +} 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 3c3dc32db94..4331fe6baa7 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 @@ -22,12 +22,13 @@ package org.grails.orm.hibernate import grails.gorm.specs.HibernateGormDatastoreSpec import grails.gorm.annotation.Entity -import grails.gorm.specs.entities.Club +import grails.gorm.tests.entities.Club +import org.hibernate.jpa.AvailableHints class HibernateGormStaticApiSpec extends HibernateGormDatastoreSpec { void setupSpec() { - manager.addAllDomainClasses([HibernateGormStaticApiEntity, Club, HibernateGormStaticApiMultiTenantEntity]) + manager.addAllDomainClasses([HibernateGormStaticApiEntity, HibernateGormStaticApiMappedPropertyEntity, Club, HibernateGormStaticApiMultiTenantEntity]) } void "Test that HibernateGormStaticApi uses the shared template from the datastore"() { @@ -185,6 +186,78 @@ class HibernateGormStaticApiSpec extends HibernateGormDatastoreSpec { instances.size() == 2 } + void "Test findWhere matches null values"() { + given: + new HibernateGormStaticApiEntity(name: "null-test", nullableName: null).save(failOnError: true) + new HibernateGormStaticApiEntity(name: "other", nullableName: "present").save(flush: true, failOnError: true) + + when: + def instance = HibernateGormStaticApiEntity.findWhere(nullableName: null) + + then: + instance.name == 'null-test' + } + + void "Test findAllWhere matches null values"() { + given: + new HibernateGormStaticApiEntity(name: "null-test-1", nullableName: null).save(failOnError: true) + new HibernateGormStaticApiEntity(name: "null-test-2", nullableName: null).save(failOnError: true) + new HibernateGormStaticApiEntity(name: "other", nullableName: "present").save(flush: true, failOnError: true) + + when: + def instances = HibernateGormStaticApiEntity.findAllWhere(nullableName: null) + + then: + instances.size() == 2 + instances*.name.containsAll(['null-test-1', 'null-test-2']) + } + + void "Test findWhere rejects unsafe property names"() { + when: + HibernateGormStaticApiEntity.findWhere(['name) or 1=1 or (name': 'test']) + + then: + def e = thrown(IllegalArgumentException) + e.message.contains('not a valid property') + } + + void "Test findAllWhere rejects unsafe null-valued property names"() { + when: + HibernateGormStaticApiEntity.findAllWhere(['nullableName) is null or 1=1 or (nullableName': null]) + + then: + def e = thrown(IllegalArgumentException) + e.message.contains('not a valid property') + } + + void "Test findWhere rejects mapped column names"() { + when: + HibernateGormStaticApiMappedPropertyEntity.findWhere(name_col: 'test') + + then: + def e = thrown(IllegalArgumentException) + e.message.contains('not a valid property') + } + + void "Test findWhere applies JPA hints from args"() { + given: + def entity = new HibernateGormStaticApiEntity(name: "hint-test").save(flush: true, failOnError: true) + def entityId = entity.id + session.clear() + + when: + def instance = HibernateGormStaticApiEntity.findWhere([name: 'hint-test'], [(AvailableHints.HINT_READ_ONLY): true]) + instance.name = "modified" + session.flush() + + and: "the instance is reloaded from the database" + session.clear() + def reloadedInstance = HibernateGormStaticApiEntity.get(entityId) + + then: + reloadedInstance.name == "hint-test" + } + void "Test findAll with HQL using named params"() { given: new HibernateGormStaticApiEntity(name: "test1").save(failOnError: true) @@ -519,6 +592,22 @@ class HibernateGormStaticApiSpec extends HibernateGormDatastoreSpec { instances[2].id == e2.id } + void "Test getAll preserves input order for convertible ids"() { + given: + def e1 = new HibernateGormStaticApiEntity(name: "first").save(failOnError: true) + def e2 = new HibernateGormStaticApiEntity(name: "second").save(failOnError: true) + def e3 = new HibernateGormStaticApiEntity(name: "third").save(flush: true, failOnError: true) + + when: "ids are supplied as strings in reverse order" + def instances = HibernateGormStaticApiEntity.getAll([e3.id.toString(), e1.id.toString(), e2.id.toString()]) + + then: "results are ordered by the converted requested ids" + instances.size() == 3 + instances[0].id == e3.id + instances[1].id == e1.id + instances[2].id == e2.id + } + void "Test getAll returns null in position for non-existent ids"() { given: def e1 = new HibernateGormStaticApiEntity(name: "exists").save(flush: true, failOnError: true) @@ -877,10 +966,23 @@ class HibernateGormStaticApiSpec extends HibernateGormDatastoreSpec { @Entity class HibernateGormStaticApiEntity { String name + String nullableName + + static constraints = { + nullableName nullable: true + } +} + +@Entity +class HibernateGormStaticApiMappedPropertyEntity { + String name + + static mapping = { + name column: 'name_col' + } } @Entity class HibernateGormStaticApiMultiTenantEntity implements grails.gorm.MultiTenant { String name } - diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/GrailsHibernatePersistentPropertySpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/GrailsHibernatePersistentPropertySpec.groovy index 705b8212823..4aeb80fce43 100644 --- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/GrailsHibernatePersistentPropertySpec.groovy +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/GrailsHibernatePersistentPropertySpec.groovy @@ -234,6 +234,34 @@ class GrailsHibernatePersistentPropertySpec extends HibernateGormDatastoreSpec { then: thrown(org.hibernate.MappingException) } + + void "test buildPropertyIdentity on real property with params"() { + given: + PersistentEntity entity = createPersistentEntity(TestEntityWithParams) + HibernatePersistentProperty property = (HibernatePersistentProperty) entity.getPropertyByName("myProperty") + + when: + def optionalIdentity = property.buildPropertyIdentity() + + then: + optionalIdentity.isPresent() + def identity = optionalIdentity.get() + identity.name == "myProperty" + identity.type == String.class + identity.params == [param1: "value1"] + } + + void "test buildPropertyIdentity on real property without params"() { + given: + PersistentEntity entity = createPersistentEntity(TestEntityWithTypeName) + HibernatePersistentProperty property = (HibernatePersistentProperty) entity.getPropertyByName("name") + + when: + def optionalIdentity = property.buildPropertyIdentity() + + then: + !optionalIdentity.isPresent() + } } @@ -386,3 +414,12 @@ class BMTOWLMAuthor { static hasMany = [books: BMTOWLMBook] } + +@Entity +class TestEntityWithParams { + Long id + String myProperty + static mapping = { + myProperty type: 'string', params: [param1: 'value1'] + } +} diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/BasicValueCreatorSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/BasicValueCreatorSpec.groovy index efa31c231d7..99a637bc3fc 100644 --- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/BasicValueCreatorSpec.groovy +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/BasicValueCreatorSpec.groovy @@ -181,4 +181,54 @@ class BasicValueCreatorSpec extends HibernateGormDatastoreSpec { then: 1 * grailsSequenceWrapper.getGenerator("custom", _, mappedId, domainClass, jdbcEnvironment, namingStrategy) >> Mock(Generator) } + + def "should use buildPropertyIdentity when domainClass identity is null"() { + given: + def mockGenerator = Mock(Generator) + def domainClass = Mock(HibernatePersistentEntity) + domainClass.getHibernateIdentity() >> null + + HibernateSimpleIdentity propertyIdentity = new HibernateSimpleIdentity() + propertyIdentity.setGenerator("custom") + + def identityProperty = Mock(HibernateSimpleIdentityProperty) + identityProperty.getGeneratorName() >> "custom" + identityProperty.getHibernateOwner() >> domainClass + identityProperty.getTable() >> table + identityProperty.buildPropertyIdentity() >> Optional.of(propertyIdentity) + def context = Mock(GeneratorCreationContext) + + when: + BasicValue id = creator.bindBasicValue(identityProperty) + def generatorCreator = id.getCustomIdGeneratorCreator() + Generator generator = generatorCreator.createGenerator(context) + + then: + 1 * grailsSequenceWrapper.getGenerator("custom", _, propertyIdentity, domainClass, jdbcEnvironment, namingStrategy) >> mockGenerator + generator == mockGenerator + } + + def "should handle empty buildPropertyIdentity when domainClass identity is null"() { + given: + def mockGenerator = Mock(Generator) + def domainClass = Mock(HibernatePersistentEntity) + domainClass.getHibernateIdentity() >> null + + def identityProperty = Mock(HibernateSimpleIdentityProperty) + identityProperty.getGeneratorName() >> "custom" + identityProperty.getHibernateOwner() >> domainClass + identityProperty.getTable() >> table + identityProperty.buildPropertyIdentity() >> Optional.empty() + def context = Mock(GeneratorCreationContext) + + when: + BasicValue id = creator.bindBasicValue(identityProperty) + def generatorCreator = id.getCustomIdGeneratorCreator() + Generator generator = generatorCreator.createGenerator(context) + + then: + 1 * grailsSequenceWrapper.getGenerator("custom", _, null, domainClass, jdbcEnvironment, namingStrategy) >> mockGenerator + generator == mockGenerator + } } + diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/CollectionBinderSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/CollectionBinderSpec.groovy index e7df64fe24c..de6a3d018e8 100644 --- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/CollectionBinderSpec.groovy +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/CollectionBinderSpec.groovy @@ -54,7 +54,7 @@ class CollectionBinderSpec extends HibernateGormDatastoreSpec { getMetadataBuildingOptions() >> mbc.getMetadataCollector().getMetadataBuildingOptions() getBootstrapContext() >> mbc.getMetadataCollector().getBootstrapContext() getDatabase() >> mbc.getMetadataCollector().getDatabase() - addTable(_, _, _, _, _, _) >> { schema, catalog, name, sub, isAbstract, context -> + addTable(_, _, _, _, _, _, _) >> { schema, catalog, name, sub, isAbstract, context, isView -> return new Table("test", name).with { setSchema(schema) setCatalog(catalog) diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/CompositeIdentifierToManyToOneBinderSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/CompositeIdentifierToManyToOneBinderSpec.groovy index 1a66a4178d5..8fefa23fe14 100644 --- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/CompositeIdentifierToManyToOneBinderSpec.groovy +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/CompositeIdentifierToManyToOneBinderSpec.groovy @@ -27,6 +27,9 @@ import org.grails.orm.hibernate.cfg.domainbinding.hibernate.GrailsHibernatePersi import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernatePersistentProperty import org.grails.orm.hibernate.cfg.PersistentEntityNamingStrategy import org.grails.orm.hibernate.cfg.PropertyConfig +import org.hibernate.mapping.Column +import org.hibernate.mapping.KeyValue +import org.hibernate.mapping.RootClass import org.hibernate.mapping.SimpleValue import spock.lang.Specification @@ -55,6 +58,8 @@ class CompositeIdentifierToManyToOneBinderSpec extends Specification { def association = Mock(HibernatePersistentProperty) def value = Mock(SimpleValue) def refDomainClass = Mock(GrailsHibernatePersistentEntity) + def persistentClass = new RootClass(metadataBuildingContext) + def identifier = Mock(KeyValue) def path = "/test" // Use a real CompositeIdentity object to avoid final method mocking issues @@ -89,6 +94,16 @@ class CompositeIdentifierToManyToOneBinderSpec extends Specification { // Make backticks remover pass through the values for simplicity backticksRemover.apply(_) >> { String s -> s } + refDomainClass.getPersistentClass() >> persistentClass + refDomainClass.getName() >> "RefDomain" + persistentClass.setIdentifier(identifier) + identifier.getColumns() >> [new Column("part_a_col"), new Column("part_b_col")] + value.getColumns() >> [new Column("ref_table_nested_entity_col_part_a_col"), new Column("ref_table_nested_entity_col_part_b_col")] + + // sortOrIndexForeignKeyColumns and getReferencedIdentifierColumns are now on the entity mock + refDomainClass.sortOrIndexForeignKeyColumns(value) >> {} + refDomainClass.getReferencedIdentifierColumns(propertyNames) >> [new Column("part_a_col"), new Column("part_b_col")] + value.createForeignKeyOfEntity("RefDomain", _ as List) >> null when: binder.bindCompositeIdentifierToManyToOne(association as HibernatePersistentProperty, value, compositeId, refDomainClass, path) @@ -122,6 +137,8 @@ class CompositeIdentifierToManyToOneBinderSpec extends Specification { def compositeId = new HibernateCompositeIdentity() compositeId.setPropertyNames(["prop1", "prop2"] as String[]) def refDomainClass = Mock(GrailsHibernatePersistentEntity) + def persistentClass = new RootClass(metadataBuildingContext) + def identifier = Mock(KeyValue) def path = "/test" // 3. Set up the "match" condition @@ -133,6 +150,15 @@ class CompositeIdentifierToManyToOneBinderSpec extends Specification { // The calculated length is the same as the number of columns already in the config calculator.calculateForeignKeyColumnCount(refDomainClass, _ as String[]) >> 2 + refDomainClass.getPersistentClass() >> persistentClass + refDomainClass.getName() >> "RefDomain" + persistentClass.setIdentifier(identifier) + identifier.getColumns() >> [new Column("prop1"), new Column("prop2")] + value.getColumns() >> [new Column("prop1"), new Column("prop2")] + + refDomainClass.sortOrIndexForeignKeyColumns(value) >> {} + refDomainClass.getReferencedIdentifierColumns(_ as String[]) >> [new Column("prop1"), new Column("prop2")] + value.createForeignKeyOfEntity("RefDomain", _ as List) >> null when: binder.bindCompositeIdentifierToManyToOne(association as HibernatePersistentProperty, value, compositeId, refDomainClass, path) diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/GrailsIdentityGeneratorSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/GrailsIdentityGeneratorSpec.groovy index 71f68ca3433..51893efb00e 100644 --- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/GrailsIdentityGeneratorSpec.groovy +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/GrailsIdentityGeneratorSpec.groovy @@ -26,6 +26,7 @@ import org.hibernate.mapping.BasicValue import org.hibernate.mapping.Column import org.hibernate.mapping.Property import org.hibernate.mapping.Table +import org.hibernate.type.Type import spock.lang.Subject import org.grails.orm.hibernate.cfg.domainbinding.generator.GrailsIdentityGenerator @@ -44,8 +45,11 @@ class GrailsIdentityGeneratorSpec extends HibernateGormDatastoreSpec { def column = new Column("test_id") value.addColumn(column) hibernateProperty.setValue(value) - + context.getProperty() >> hibernateProperty + context.getType() >> Stub(Type) { + getReturnedClass() >> Long + } when: @Subject @@ -66,8 +70,11 @@ class GrailsIdentityGeneratorSpec extends HibernateGormDatastoreSpec { def column = new Column("test_id2") value.addColumn(column) hibernateProperty.setValue(value) - + context.getProperty() >> hibernateProperty + context.getType() >> Stub(Type) { + getReturnedClass() >> Long + } when: @Subject diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/SimpleIdBinderSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/SimpleIdBinderSpec.groovy index 3522d860f2e..f51db9c1782 100644 --- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/SimpleIdBinderSpec.groovy +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/SimpleIdBinderSpec.groovy @@ -26,6 +26,7 @@ import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateSimpleIdent import org.hibernate.boot.spi.MetadataBuildingContext import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment import org.hibernate.mapping.BasicValue +import org.hibernate.mapping.Column import org.hibernate.mapping.PrimaryKey import org.hibernate.mapping.RootClass import org.hibernate.mapping.Table @@ -82,6 +83,7 @@ class SimpleIdBinderSpec extends HibernateGormDatastoreSpec { } def rootClass = new RootClass(metadataBuildingContext) currentTable = new Table("TEST_TABLE") + currentTable.setName("TEST_TABLE") rootClass.setTable(currentTable) def domainClass = Mock(HibernatePersistentEntity) { getMappedForm() >> mapping @@ -95,13 +97,22 @@ class SimpleIdBinderSpec extends HibernateGormDatastoreSpec { simpleIdBinder.bindSimpleId(domainClass) then: - 1 * simpleValueBinder.bindSimpleValue(testProperty, null, _, "") + 1 * simpleValueBinder.bindSimpleValue(testProperty, null, _, "") >> { HibernatePersistentProperty property, HibernatePersistentProperty parentProperty, BasicValue value, String path -> + bindIdColumn(value) + } 1 * propertyBinder.bindProperty(testProperty, _) rootClass.identifier instanceof BasicValue rootClass.declaredIdentifierProperty != null rootClass.identifierProperty != null + rootClass.table.primaryKey == null + + when: "the root class creates the table primary key" + rootClass.createPrimaryKey() + + then: rootClass.table.primaryKey instanceof PrimaryKey + rootClass.table.primaryKey.columnSpan == 1 } def "bindSimpleId with sequence generator"() { @@ -114,6 +125,7 @@ class SimpleIdBinderSpec extends HibernateGormDatastoreSpec { } def rootClass = new RootClass(metadataBuildingContext) currentTable = new Table("TEST_TABLE") + currentTable.setName("TEST_TABLE") rootClass.setTable(currentTable) def domainClass = Mock(HibernatePersistentEntity) { getMappedForm() >> mapping @@ -127,13 +139,22 @@ class SimpleIdBinderSpec extends HibernateGormDatastoreSpec { simpleIdBinder.bindSimpleId(domainClass) then: - 1 * simpleValueBinder.bindSimpleValue(testProperty, null, _, "") + 1 * simpleValueBinder.bindSimpleValue(testProperty, null, _, "") >> { HibernatePersistentProperty property, HibernatePersistentProperty parentProperty, BasicValue value, String path -> + bindIdColumn(value) + } 1 * propertyBinder.bindProperty(testProperty, _) rootClass.identifier instanceof BasicValue rootClass.declaredIdentifierProperty != null rootClass.identifierProperty != null + rootClass.table.primaryKey == null + + when: "the root class creates the table primary key" + rootClass.createPrimaryKey() + + then: rootClass.table.primaryKey instanceof PrimaryKey + rootClass.table.primaryKey.columnSpan == 1 } def "bindSimpleId with synthetic identifier property"() { @@ -144,6 +165,7 @@ class SimpleIdBinderSpec extends HibernateGormDatastoreSpec { def reflector = Mock(EntityReflector) def rootClass = new RootClass(metadataBuildingContext) currentTable = new Table("TEST_TABLE") + currentTable.setName("TEST_TABLE") rootClass.setTable(currentTable) def domainClass = Mock(HibernatePersistentEntity) { getMappedForm() >> mapping @@ -160,13 +182,22 @@ class SimpleIdBinderSpec extends HibernateGormDatastoreSpec { simpleIdBinder.bindSimpleId(domainClass) then: - 1 * simpleValueBinder.bindSimpleValue(_, null, _, "") + 1 * simpleValueBinder.bindSimpleValue(_, null, _, "") >> { HibernatePersistentProperty property, HibernatePersistentProperty parentProperty, BasicValue value, String path -> + bindIdColumn(value) + } 1 * propertyBinder.bindProperty(_, _) rootClass.identifier instanceof BasicValue rootClass.declaredIdentifierProperty != null rootClass.identifierProperty != null + rootClass.table.primaryKey == null + + when: "the root class creates the table primary key" + rootClass.createPrimaryKey() + + then: rootClass.table.primaryKey instanceof PrimaryKey + rootClass.table.primaryKey.columnSpan == 1 } def "bindSimpleId throws MappingException when identity property is not a HibernateSimpleIdentityProperty"() { @@ -188,4 +219,12 @@ class SimpleIdBinderSpec extends HibernateGormDatastoreSpec { expect: simpleIdBinder.getMetadataBuildingContext() == metadataBuildingContext } + + private static BasicValue bindIdColumn(BasicValue value) { + def column = new Column("id") + column.setValue(value) + value.table.addColumn(column) + value.addColumn(column) + value + } } diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/TableForManyCalculatorSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/TableForManyCalculatorSpec.groovy index 1675991497a..f74046b724a 100644 --- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/TableForManyCalculatorSpec.groovy +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/TableForManyCalculatorSpec.groovy @@ -48,7 +48,7 @@ class TableForManyCalculatorSpec extends HibernateGormDatastoreSpec { def namingStrategy = getGrailsDomainBinder().getNamingStrategy() def backticksRemover = new BackticksRemover() def collector = Mock(InFlightMetadataCollector) - collector.addTable(_, _, _, _, _, _) >> { schema, catalog, name, sub, isAbstract, context -> + collector.addTable(_, _, _, _, _, _, _) >> { schema, catalog, name, sub, isAbstract, context, isView -> return new org.hibernate.mapping.Table("test", name) } @@ -186,7 +186,7 @@ class TableForManyCalculatorSpec extends HibernateGormDatastoreSpec { def namingStrategy = getGrailsDomainBinder().getNamingStrategy() def backticksRemover = new BackticksRemover() def collector = Mock(InFlightMetadataCollector) - collector.addTable(_, _, _, _, _, _) >> { a, b, name, d, e, f -> new org.hibernate.mapping.Table("test", name) } + collector.addTable(_, _, _, _, _, _, _) >> { a, b, name, d, e, f, isView -> new org.hibernate.mapping.Table("test", name) } def calculator = new TableForManyCalculator(namingStrategy, collector, backticksRemover) def ownerEntity = Mock(GrailsHibernatePersistentEntity) @@ -214,7 +214,7 @@ class TableForManyCalculatorSpec extends HibernateGormDatastoreSpec { def namingStrategy = getGrailsDomainBinder().getNamingStrategy() def backticksRemover = new BackticksRemover() def collector = Mock(InFlightMetadataCollector) - collector.addTable(_, _, _, _, _, _) >> { a, b, name, d, e, f -> new org.hibernate.mapping.Table("test", name) } + collector.addTable(_, _, _, _, _, _, _) >> { a, b, name, d, e, f, isView -> new org.hibernate.mapping.Table("test", name) } def calculator = new TableForManyCalculator(namingStrategy, collector, backticksRemover) def ownerEntity = Mock(GrailsHibernatePersistentEntity) @@ -241,7 +241,7 @@ class TableForManyCalculatorSpec extends HibernateGormDatastoreSpec { def namingStrategy = getGrailsDomainBinder().getNamingStrategy() def backticksRemover = new BackticksRemover() def collector = Mock(InFlightMetadataCollector) - collector.addTable(_, _, _, _, _, _) >> { a, b, name, d, e, f -> new org.hibernate.mapping.Table("test", name) } + collector.addTable(_, _, _, _, _, _, _) >> { a, b, name, d, e, f, isView -> new org.hibernate.mapping.Table("test", name) } def calculator = new TableForManyCalculator(namingStrategy, collector, backticksRemover) def ownerEntity = Mock(GrailsHibernatePersistentEntity) diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/ListSecondPassBinderSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/ListSecondPassBinderSpec.groovy index efcf70253f9..ee27378e669 100644 --- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/ListSecondPassBinderSpec.groovy +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/ListSecondPassBinderSpec.groovy @@ -103,7 +103,7 @@ class ListSecondPassBinderSpec extends HibernateGormDatastoreSpec { def rootClass = new RootClass(binder.getMetadataBuildingContext()) rootClass.setEntityName(domainClass.name) rootClass.setJpaEntityName(domainClass.simpleName) - rootClass.setTable(collector.addTable(null, null, domainClass.simpleName.toUpperCase(), null, false, binder.getMetadataBuildingContext())) + rootClass.setTable(collector.addTable(null, null, domainClass.simpleName.toUpperCase(), null, false, binder.getMetadataBuildingContext(), false)) properties.each { propName -> def p = new Property() @@ -177,7 +177,7 @@ class ListSecondPassBinderSpec extends HibernateGormDatastoreSpec { def list = new org.hibernate.mapping.List(binder.getMetadataBuildingContext(), ownerRoot) list.setRole("${LSBManyToManyA.name}.others".toString()) - list.setCollectionTable(collector.addTable(null, null, "JOIN_TABLE", null, false, binder.getMetadataBuildingContext())) + list.setCollectionTable(collector.addTable(null, null, "JOIN_TABLE", null, false, binder.getMetadataBuildingContext(), false)) list.setKey(new DependantValue(binder.getMetadataBuildingContext(), list.getCollectionTable(), null)) list.setElement(new ManyToOne(binder.getMetadataBuildingContext(), list.getCollectionTable())) ((ManyToOne)list.getElement()).setReferencedEntityName(LSBManyToManyB.name) diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/MapSecondPassBinderSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/MapSecondPassBinderSpec.groovy index cc49729bdc6..0658d8e4611 100644 --- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/MapSecondPassBinderSpec.groovy +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/MapSecondPassBinderSpec.groovy @@ -201,14 +201,14 @@ class MapSecondPassBinderSpec extends HibernateGormDatastoreSpec { rootClass.setEntityName(authorEntity.name) rootClass.setClassName(authorEntity.name) rootClass.setJpaEntityName(authorEntity.name) - rootClass.setTable(collector.addTable(null, null, "MAPSPB_AUTHOR", null, false, metadataBuildingContext)) + rootClass.setTable(collector.addTable(null, null, "MAPSPB_AUTHOR", null, false, metadataBuildingContext, false)) collector.addEntityBinding(rootClass) def bookRootClass = new RootClass(metadataBuildingContext) bookRootClass.setEntityName(bookEntity.name) bookRootClass.setClassName(bookEntity.name) bookRootClass.setJpaEntityName(bookEntity.name) - bookRootClass.setTable(collector.addTable(null, null, "MAPSPB_BOOK", null, false, metadataBuildingContext)) + bookRootClass.setTable(collector.addTable(null, null, "MAPSPB_BOOK", null, false, metadataBuildingContext, false)) collector.addEntityBinding(bookRootClass) def persistentClasses = [ @@ -251,14 +251,14 @@ class MapSecondPassBinderSpec extends HibernateGormDatastoreSpec { rootClass.setEntityName(authorEntity.name) rootClass.setClassName(authorEntity.name) rootClass.setJpaEntityName(authorEntity.name) - rootClass.setTable(collector.addTable(null, null, "MAPSPB_AUTHOR", null, false, metadataBuildingContext)) + rootClass.setTable(collector.addTable(null, null, "MAPSPB_AUTHOR", null, false, metadataBuildingContext, false)) collector.addEntityBinding(rootClass) def bookRootClass = new RootClass(metadataBuildingContext) bookRootClass.setEntityName(bookEntity.name) bookRootClass.setClassName(bookEntity.name) bookRootClass.setJpaEntityName(bookEntity.name) - bookRootClass.setTable(collector.addTable(null, null, "MAPSPB_BOOK", null, false, metadataBuildingContext)) + bookRootClass.setTable(collector.addTable(null, null, "MAPSPB_BOOK", null, false, metadataBuildingContext, false)) collector.addEntityBinding(bookRootClass) def persistentClasses = [ @@ -302,7 +302,7 @@ class MapSecondPassBinderSpec extends HibernateGormDatastoreSpec { rootClass.setEntityName(ownerEntity.name) rootClass.setClassName(ownerEntity.name) rootClass.setJpaEntityName(ownerEntity.name) - rootClass.setTable(collector.addTable(null, null, "MAPSPB_OWNER", null, false, metadataBuildingContext)) + rootClass.setTable(collector.addTable(null, null, "MAPSPB_OWNER", null, false, metadataBuildingContext, false)) collector.addEntityBinding(rootClass) def map = new org.hibernate.mapping.Map(metadataBuildingContext, rootClass) @@ -339,7 +339,7 @@ class MapSecondPassBinderSpec extends HibernateGormDatastoreSpec { rootClass.setEntityName(ownerEntity.name) rootClass.setClassName(ownerEntity.name) rootClass.setJpaEntityName(ownerEntity.name) - rootClass.setTable(collector.addTable(null, null, "MAPSPB_OWNER2", null, false, metadataBuildingContext)) + rootClass.setTable(collector.addTable(null, null, "MAPSPB_OWNER2", null, false, metadataBuildingContext, false)) collector.addEntityBinding(rootClass) def map = new org.hibernate.mapping.Map(metadataBuildingContext, rootClass) @@ -415,7 +415,7 @@ class MapSecondPassBinderSpec extends HibernateGormDatastoreSpec { rootClass.setEntityName(ownerEntity.name) rootClass.setClassName(ownerEntity.name) rootClass.setJpaEntityName(ownerEntity.name) - rootClass.setTable(collector.addTable(null, null, "MAPSPB_OWNER3", null, false, metadataBuildingContext)) + rootClass.setTable(collector.addTable(null, null, "MAPSPB_OWNER3", null, false, metadataBuildingContext, false)) collector.addEntityBinding(rootClass) def map = new org.hibernate.mapping.Map(metadataBuildingContext, rootClass) diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/event/listener/HibernateEventListenerSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/event/listener/HibernateEventListenerSpec.groovy index 004e52f6c10..9228d215280 100644 --- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/event/listener/HibernateEventListenerSpec.groovy +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/event/listener/HibernateEventListenerSpec.groovy @@ -395,6 +395,8 @@ class HibernateEventListenerSpec extends HibernateGormDatastoreSpec { def mockSessionFactory = Mock(org.hibernate.engine.spi.SessionFactoryImplementor) def mockPersister = Mock(org.hibernate.persister.entity.EntityPersister) mockPersister.getFactory() >> mockSessionFactory + mockEventSource.asEventSource() >> mockEventSource + mockEventSource.getFactory() >> mockSessionFactory mockEventSource.getSessionFactory() >> mockSessionFactory when: "Calling onPersistEvent" diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/query/HqlQueryMethodsSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/query/HqlQueryMethodsSpec.groovy index a6ad612e1da..4dbd386ea0e 100644 --- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/query/HqlQueryMethodsSpec.groovy +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/query/HqlQueryMethodsSpec.groovy @@ -19,6 +19,7 @@ package org.grails.orm.hibernate.query import org.grails.orm.hibernate.query.HibernateQueryArgument +import jakarta.persistence.LockModeType import spock.lang.Specification import org.hibernate.query.QueryFlushMode @@ -64,9 +65,12 @@ class HqlQueryMethodsSpec extends Specification { def delegate = Mock(HqlQueryDelegate) def settings = [ (HibernateQueryArgument.FLUSH_MODE.value()): "COMMIT", - (HibernateQueryArgument.MAX.value()): 10, - (HibernateQueryArgument.OFFSET.value()): 5, - (HibernateQueryArgument.READ_ONLY.value()): true + (HibernateQueryArgument.MAX.value()): '10', + (HibernateQueryArgument.OFFSET.value()): '5', + (HibernateQueryArgument.FETCH_SIZE.value()): '50', + (HibernateQueryArgument.TIMEOUT.value()): '30', + (HibernateQueryArgument.READ_ONLY.value()): 'true', + (HibernateQueryArgument.CACHE.value()): 'true' ] when: @@ -76,7 +80,27 @@ class HqlQueryMethodsSpec extends Specification { 1 * delegate.setQueryFlushMode(QueryFlushMode.NO_FLUSH) 1 * delegate.setMaxResults(10) 1 * delegate.setFirstResult(5) + 1 * delegate.setFetchSize(50) + 1 * delegate.setTimeout(30) 1 * delegate.setReadOnly(true) + 1 * delegate.setCacheable(true) + } + + void "test populateQuerySettings applies lock and disables cache"() { + given: + def delegate = Mock(HqlQueryDelegate) + def settings = [ + (HibernateQueryArgument.LOCK.value()): 'true', + (HibernateQueryArgument.CACHE.value()): 'true' + ] + + when: + queryMethods.populateQuerySettings(delegate, settings) + + then: + 1 * delegate.setLockMode(LockModeType.PESSIMISTIC_WRITE) + 1 * delegate.setCacheable(false) + 0 * delegate.setCacheable(true) } void "test populateParameters with named parameters"() { @@ -96,13 +120,14 @@ class HqlQueryMethodsSpec extends Specification { void "test populateParameters filters internal settings"() { given: def delegate = Mock(HqlQueryDelegate) - def ctx = new HqlQueryContext("hql", Object, [(HibernateQueryArgument.MAX.value()): 10, title: "GORM"], [], [:], [:], false, false) + def ctx = new HqlQueryContext("hql", Object, [(HibernateQueryArgument.MAX.value()): 10, (HibernateQueryArgument.LOCK.value()): true, title: "GORM"], [], [:], [:], false, false) when: HqlQueryMethods.populateParameters(delegate, ctx) then: 0 * delegate.setParameter(HibernateQueryArgument.MAX.value(), _) + 0 * delegate.setParameter(HibernateQueryArgument.LOCK.value(), _) 1 * delegate.setParameter("title", "GORM") } diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/query/SelectHqlQuerySpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/query/SelectHqlQuerySpec.groovy index 8c947936bff..3642ef198bd 100644 --- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/query/SelectHqlQuerySpec.groovy +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/query/SelectHqlQuerySpec.groovy @@ -141,6 +141,17 @@ class SelectHqlQuerySpec extends HibernateGormDatastoreSpec { results.size() == 1 } + void "createHqlQuery exposes converted max and offset args"() { + when: + def query = buildHqlQuery("from SelectHqlQuerySpecBook order by title", [:], null, [max: '2', offset: '1']) + def results = query.list() + + then: + query.max == 2 + query.offset == 1 + results.size() == 2 + } + void "createHqlQuery with empty query string defaults to full entity query"() { when: def results = buildHqlQuery("").list() diff --git a/grails-data-hibernate7/dbmigration-core/src/main/java/liquibase/ext/hibernate/database/HibernateSpringPackageDatabase.java b/grails-data-hibernate7/dbmigration-core/src/main/java/liquibase/ext/hibernate/database/HibernateSpringPackageDatabase.java index 81ab92b1b4c..2c4712d0fc3 100644 --- a/grails-data-hibernate7/dbmigration-core/src/main/java/liquibase/ext/hibernate/database/HibernateSpringPackageDatabase.java +++ b/grails-data-hibernate7/dbmigration-core/src/main/java/liquibase/ext/hibernate/database/HibernateSpringPackageDatabase.java @@ -113,7 +113,7 @@ protected EntityManagerFactoryBuilderImpl createEntityManagerFactoryBuilder() { .setPersistenceProviderPackageName(jpaVendorAdapter.getPersistenceProviderRootPackage()); } - Map map = new HashMap<>(); + Map map = new HashMap<>(); map.put(AvailableSettings.DIALECT, getProperty(AvailableSettings.DIALECT)); map.put(HibernateDatabase.HIBERNATE_TEMP_USE_JDBC_METADATA_DEFAULTS, Boolean.FALSE.toString()); map.put(AvailableSettings.USE_SECOND_LEVEL_CACHE, Boolean.FALSE.toString()); diff --git a/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/liquibase/ChangelogXml2GroovySpec.groovy b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/liquibase/ChangelogXml2GroovySpec.groovy index eb082d3838a..0bc06dd572c 100644 --- a/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/liquibase/ChangelogXml2GroovySpec.groovy +++ b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/liquibase/ChangelogXml2GroovySpec.groovy @@ -96,6 +96,6 @@ class ChangelogXml2GroovySpec extends Specification { String groovy = ChangelogXml2Groovy.convert(xml) then: - groovy.trim() == "databaseChangeLog = {\n}" + groovy.trim().replace('\r\n', '\n').replace('\r', '\n') == "databaseChangeLog = {\n}" } } diff --git a/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/liquibase/GormDatabaseSpec.groovy b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/liquibase/GormDatabaseSpec.groovy index 7a5b153708e..2279a47dac6 100644 --- a/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/liquibase/GormDatabaseSpec.groovy +++ b/grails-data-hibernate7/dbmigration/src/test/groovy/org/grails/plugins/databasemigration/liquibase/GormDatabaseSpec.groovy @@ -18,6 +18,7 @@ */ package org.grails.plugins.databasemigration.liquibase +import grails.gorm.annotation.Entity import liquibase.database.DatabaseConnection import liquibase.database.jvm.JdbcConnection import liquibase.snapshot.DatabaseSnapshot @@ -25,52 +26,36 @@ import liquibase.snapshot.JdbcDatabaseSnapshot import liquibase.structure.DatabaseObject import org.grails.orm.hibernate.HibernateDatastore -import org.hibernate.boot.Metadata -import org.hibernate.boot.MetadataSources -import org.hibernate.boot.internal.MetadataBuilderImpl -import org.hibernate.boot.registry.StandardServiceRegistryBuilder import org.hibernate.dialect.H2Dialect import spock.lang.Specification class GormDatabaseSpec extends Specification { - protected Metadata createRealMetadata() { - def serviceRegistry = new StandardServiceRegistryBuilder() - .applySetting("hibernate.dialect", H2Dialect.class.getName()) - .build() - return new MetadataBuilderImpl( - new MetadataSources(serviceRegistry) - ).build() - } - def "test GormDatabase initialization and properties"() { given: def dialect = new H2Dialect() - Metadata metadata = createRealMetadata() - HibernateDatastore datastore = Mock { - getMetadata() >> metadata - } + HibernateDatastore datastore = new HibernateDatastore(GormDatabaseSpecBook) when: - GormDatabase gormDb = Spy(GormDatabase, constructorArgs: [dialect, datastore]) - gormDb.getMetadata() >> metadata + GormDatabase gormDb = new GormDatabase(dialect, datastore) then: gormDb.getDialect().getClass() == dialect.getClass() - gormDb.getMetadata() == metadata + gormDb.getMetadata() == datastore.getMetadata() gormDb.getGormDatastore() == datastore gormDb.getShortName() == 'GORM' gormDb.getDefaultDatabaseProductName() == 'getDefaultDatabaseProductName' gormDb.supportsAutoIncrement() !gormDb.isCorrectDatabaseImplementation(Mock(DatabaseConnection)) + + cleanup: + datastore.destroy() } def "test GormDatabase connection and snapshot"() { given: def dialect = new H2Dialect() - Metadata metadata = createRealMetadata() - HibernateDatastore datastore = Mock() - datastore.getMetadata() >> metadata + HibernateDatastore datastore = new HibernateDatastore(GormDatabaseSpecBook) when: GormDatabase gormDb = new GormDatabase(dialect, datastore) @@ -85,5 +70,13 @@ class GormDatabaseSpec extends Specification { then: "it returns a DatabaseSnapshot" snapshot instanceof DatabaseSnapshot snapshot.database == gormDb + + cleanup: + datastore.destroy() } } + +@Entity +class GormDatabaseSpecBook { + String title +} diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/gettingStarted/hibernateVersions.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/gettingStarted/hibernateVersions.adoc index 101666876fc..7ca3d869fa4 100644 --- a/grails-data-hibernate7/docs/src/docs/asciidoc/gettingStarted/hibernateVersions.adoc +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/gettingStarted/hibernateVersions.adoc @@ -47,17 +47,127 @@ dependencies { } ---- -=== Key Changes from Hibernate 5 +=== Hibernate 7.4 support line -If you are migrating from Hibernate 5, review the following areas: +Grails Hibernate 7 targets Hibernate ORM 7.4, the current stable Hibernate 7 line. Hibernate ORM 7.3 and 7.2 are limited-support lines, so new Hibernate 7 applications should validate against the Hibernate 7.4 behavior described below. -* **Session API** β€” `Session.save()`, `update()`, `delete()`, `load()`, and `get()` were removed. GORM's own `save()`, `delete()`, `get()`, and `load()` methods are unaffected; only direct Hibernate `Session` usage inside `withSession` blocks is impacted. -* **`CascadeType.SAVE_UPDATE` removed** β€” Use `CascadeType.ALL` or `CascadeType.PERSIST` + `CascadeType.MERGE`. The GORM ORM DSL `cascade: 'save-update'` string continues to work. -* **`@Where` renamed** β€” `@org.hibernate.annotations.Where` was replaced by `@org.hibernate.annotations.SQLRestriction`. -* **`@Proxy` removed** β€” Proxy configuration via annotation is no longer supported. -* **`@LazyCollection` removed** β€” Use `fetch = FetchType.LAZY` / `EAGER` directly on the association annotation. -* **Native query temporal types** β€” Native SQL queries now return `java.time` types instead of `java.sql` types. Set `hibernate.query.native.prefer_jdbc_datetime_types=true` to restore legacy behaviour during migration. -* **DDL changes** β€” `char`/`Character` now maps to `varchar(1)` instead of `char(1)`. Oracle `float`/`double` map to `binary_float`/`binary_double`. Validate your schema before running `dbCreate=update` in production. -* **Jakarta EE 10** β€” `javax.*` was already replaced by `jakarta.*` in Grails 7. Hibernate 7 requires Jakarta Persistence 3.2. +=== Migration checklist from Hibernate 5 -See the https://docs.hibernate.org/orm/7.0/migration-guide/[Hibernate ORM 7.0 Migration Guide] and https://docs.hibernate.org/orm/6.0/migration-guide/[Hibernate ORM 6.0 Migration Guide] for the full list of changes. +If you are migrating an application from Hibernate 5 or the Grails Hibernate 5 plugin, review these remaining application-level changes. GORM compatibility issues fixed by this release, such as `findWhere` null handling and `getAll` ordering with converted ids, are documented in the relevant GORM API reference pages and do not require separate migration steps when using the fixed GORM APIs. + +[cols="1,2,2", options="header"] +|=== +| Area +| Hibernate 7.4 change +| Application migration action + +| Hibernate artifacts +| Hibernate 7 applications must resolve the Hibernate 7 Grails plugin, BOM, and Hibernate ORM artifacts together. +| Use `grails-hibernate7-bom` and `grails-hibernate7` consistently. Do not mix Hibernate 5 and Hibernate 7 GORM or Hibernate artifacts in one runtime. + +| Jakarta Persistence +| Hibernate 7 requires Jakarta Persistence 3.2 and Jakarta EE packages. +| Replace any remaining `javax.persistence.*` or other `javax.*` imports with the matching `jakarta.*` APIs. + +| Direct Hibernate `Session` API +| `Session.save()`, `update()`, `delete()`, and `load()` were removed. `Session.get()` remains available but is deprecated in favor of `find()`. +| GORM domain methods such as `save()`, `delete()`, `get()`, and `load()` are unaffected. Direct `Session` code inside `withSession` blocks should use `persist()`, `merge()`, `remove()`, and `getReference()` for removed methods, and prefer `find()` over deprecated direct `Session.get()` usage. + +| Hibernate annotations +| Several Hibernate 5 annotations or enum constants were removed or renamed. +| Replace direct uses of `CascadeType.SAVE_UPDATE`, `@org.hibernate.annotations.Where`, `@Proxy`, and `@LazyCollection`. The GORM ORM DSL `cascade: 'save-update'` string continues to work. + +| HQL string safety +| Single-argument GORM HQL overloads are reserved for Groovy `GString` values so interpolated values can be bound safely. +| Pass a `GString` with interpolated values, or use the named-parameter or positional-parameter overload with an explicit params map or list. See <>. + +| H5-specific cache setup +| Hibernate 5 cache integrations and region-factory classes are not valid Hibernate 7 cache configuration. +| Remove H5 cache dependencies and region-factory settings before booting on Hibernate 7. Reintroduce caching with a Hibernate 7 compatible provider such as JCache and test locked queries without relying on query cache entries. + +| Direct single-result queries +| Hibernate 7.3 and later strictly throw when `getSingleResult()` or `getSingleResultOrNull()` sees more than one result. +| GORM single-result helpers fixed by this release preserve GORM first-row behavior. Direct Hibernate or JPA queries should add `setMaxResults(1)`, query for a list and choose the first row intentionally, or use a result-list transformer when duplicate collapse is required. + +| Read-only entity collections +| Collections owned by entities loaded in read-only mode are now read-only too. +| Do not mutate associations on read-only entities. Reload the entity in a writable session before changing its collections. + +| Timeout exceptions +| Query or lock timeouts may now throw `PersistenceException` when the database marks the transaction for rollback. +| Catch `PersistenceException` around direct Hibernate or JPA timeout-sensitive code and inspect the cause when you need to distinguish query timeout, lock timeout, and transaction rollback behavior. + +| Native SQL temporal values +| Native SQL queries return `java.time` types instead of `java.sql` temporal types by default. +| Update result handling to use `java.time` types, or set `hibernate.query.native.prefer_jdbc_datetime_types=true` during migration if legacy JDBC temporal values are required. + +| Direct aggregate HQL +| Hibernate 7 validates projection result types strictly. +| Use scalar-compatible return types for aggregate projections. For example, `avg` generally returns `Double`, `count` returns `Long`, and `max` or `min` follow the selected expression type. + +| Numeric expression typing +| Hibernate 7 SQM typing is stricter about comparing unlike numeric expression types. +| GORM where-query numeric comparison fixes are included in this release. In direct HQL, align domain numeric types or cast explicitly when comparing expressions of different numeric types. + +| Managed collection identity +| Hibernate 7 detects duplicate managed collection wrappers more aggressively. +| Prefer mutating managed collection instances with GORM `addTo*` and `removeFrom*` helpers instead of replacing collection objects. Be careful when merging detached graphs into an existing session. + +| Bidirectional associations +| Hibernate 7 is less tolerant of inconsistent managed association state at flush time. +| Keep both sides of bidirectional associations synchronized. Prefer GORM association helpers where possible. + +| Query locks and query cache +| Locked queries are not query-cacheable. +| Do not expect `lock: true` queries to use the query cache. Treat pessimistically locked queries as database reads that require fresh row state. + +| Programmatic datastore setup +| Hibernate 7 test and programmatic datastore setups expose wrong package scanning more readily. +| When constructing a datastore manually, scan the package that contains the domain and service classes, not only the caller or spec package. + +| Multiple datasources and OSIV +| Hibernate 7 multiple-datasource and Open Session in View behavior should be validated explicitly for each application. +| Run targeted migration tests for datasource switching, transaction routing, and GSP rendering under OSIV before migrating production applications with multiple datasources. + +| Views and scaffolding +| Generated views and scaffolding can expose Hibernate 7 differences in association rendering and unique-constraint behavior. +| Regression-test generated and custom association rendering, fields rendering, and unique constraints before enabling Hibernate 7 in applications that rely on scaffolding. + +| Version-column DDL +| Hibernate 7.3 declares `@Version` columns `not null` by default. +| Validate generated DDL against existing schemas before using `dbCreate=update`, especially for tables with legacy nullable version columns. + +| General DDL changes +| Hibernate 7 changes generated DDL for several mappings, including `char`/`Character`, Oracle floating-point types, `@ElementCollection` sets, `@CreationTimestamp`, `@UpdateTimestamp`, and Oracle 23c LOB columns. +| Prefer explicit database migrations for production schemas. Compare generated DDL in a staging database before using schema update tooling. + +| Schema actions and `import.sql` +| Hibernate 7.3 and later run schema actions even when no entities are mapped. +| Remove accidental `import.sql` files from the runtime classpath, or ensure schema-generation settings are intentional for persistence units with no mapped entities. + +| MySQL fetch depth +| Hibernate 7.4 removes the MySQL dialect-specific default `hibernate.max_fetch_depth=2`. +| Set `hibernate.max_fetch_depth` explicitly if the application relied on that implicit MySQL default. + +| Fetch joins with limits +| Hibernate 7.4 applies pagination or limits with collection fetch joins in SQL instead of doing the limit in memory. +| Review queries that combine `max`/`offset` or limits with collection fetch joins. Set the query hint `org.hibernate.limitInMemory` only when the previous in-memory behavior is required. + +| Oracle HQL date expressions +| On Oracle, HQL `current date` and `local date` now translate to `trunc(current_date)`. +| Review Oracle queries that depended on a time component from those date expressions. + +| Eager `@Any` mappings +| Hibernate 7.4 join-fetches eager `@Any` associations when loading an entity by id. +| Review direct Hibernate `@Any` mappings for SQL shape and row-width changes. + +| Spanner PostgreSQL dialect +| `SpannerPostgreSQLDialect` moved from `hibernate-community-dialects` to `hibernate-core` and package `org.hibernate.dialect`. +| Update explicit dialect configuration to `org.hibernate.dialect.SpannerPostgreSQLDialect`, or rely on automatic dialect resolution. + +| Envers audited associations +| Hibernate 7.3 respects `RelationTargetAuditMode.NOT_AUDITED` for audited associations. +| If you use Envers, verify historic reads that previously expected the associated target entity to come from its audit table despite `NOT_AUDITED`. +|=== + +See the https://docs.hibernate.org/orm/7.4/migration-guide/[Hibernate ORM 7.4 Migration Guide], https://docs.hibernate.org/orm/7.3/migration-guide/[Hibernate ORM 7.3 Migration Guide], https://docs.hibernate.org/orm/7.0/migration-guide/[Hibernate ORM 7.0 Migration Guide], and https://docs.hibernate.org/orm/6.0/migration-guide/[Hibernate ORM 6.0 Migration Guide] for the full list of Hibernate changes. diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/querying/hql.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/querying/hql.adoc index 5d6a9c09c18..10678b813cf 100644 --- a/grails-data-hibernate7/docs/src/docs/asciidoc/querying/hql.adoc +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/querying/hql.adoc @@ -85,9 +85,9 @@ List results = Book.executeQuery( ['%Groovy%', 'Tech']) ---- -=== Pagination and Sorting +=== Query Settings -All `findAll` and `executeQuery` overloads accept a settings map as the last argument: +`find`, `findAll`, and `executeQuery` overloads accept a settings map as the last argument. Use it for pagination and Hibernate query settings such as `cache`, `readOnly`, `fetchSize`, `timeout`, `flushMode`, and `lock`: [source,groovy] ---- @@ -99,8 +99,15 @@ List results = Book.executeQuery( "from Book b where b.genre = :genre", [genre: 'Tech'], [max: 5, offset: 0, cache: true]) + +List locked = Book.findAll( + "from Book b where b.genre = :genre", + [genre: 'Tech'], + [lock: true]) ---- +When `lock: true` is used, GORM requests a pessimistic write lock and disables query caching for that query. + === Bulk Updates and Deletes [source,groovy] diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/quickStartGuide/basicCRUD.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/quickStartGuide/basicCRUD.adoc index 1f78dce296a..ca1b0e1d8ea 100644 --- a/grails-data-hibernate7/docs/src/docs/asciidoc/quickStartGuide/basicCRUD.adoc +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/quickStartGuide/basicCRUD.adoc @@ -46,6 +46,9 @@ def book = Book.get(999) // null // Get multiple by IDs def books = Book.getAll(1, 2, 3) +// The returned list preserves the supplied id order +def books = Book.getAll(3, 1, 2) + // Load a proxy (no immediate SELECT) def book = Book.load(1) @@ -59,6 +62,10 @@ def books = Book.list(max: 10, offset: 0, sort: 'title', order: 'asc') def book = Book.findByTitle('Groovy in Action') def books = Book.findAllByAuthor('Dierk KΓΆnig') def count = Book.countByAuthor('Dierk KΓΆnig') + +// Property-map queries; null values match null properties +def unreleased = Book.findWhere(releaseDate: null) +def matching = Book.findAllWhere(author: 'Dierk KΓΆnig', releaseDate: null) ---- === Update diff --git a/grails-datamapping-tck/src/main/groovy/org/apache/grails/data/testing/tck/tests/DirtyCheckingAfterListenerSpec.groovy b/grails-datamapping-tck/src/main/groovy/org/apache/grails/data/testing/tck/tests/DirtyCheckingAfterListenerSpec.groovy index aa94842be63..53788734400 100644 --- a/grails-datamapping-tck/src/main/groovy/org/apache/grails/data/testing/tck/tests/DirtyCheckingAfterListenerSpec.groovy +++ b/grails-datamapping-tck/src/main/groovy/org/apache/grails/data/testing/tck/tests/DirtyCheckingAfterListenerSpec.groovy @@ -87,6 +87,9 @@ class TestSaveOrUpdateEventListener extends AbstractPersistenceEventListener { protected void onPersistenceEvent(AbstractPersistenceEvent event) { TestPlayer player = (TestPlayer) event.entityObject player.attributes = ['test0', 'test1', 'test2'] + if (event.getEntityAccess() != null) { + event.getEntityAccess().setProperty('attributes', player.attributes) + } isExecuted = true } diff --git a/grails-datamapping-tck/src/main/groovy/org/apache/grails/data/testing/tck/tests/OneToOneSpec.groovy b/grails-datamapping-tck/src/main/groovy/org/apache/grails/data/testing/tck/tests/OneToOneSpec.groovy index 460e5f8e956..43de314e380 100644 --- a/grails-datamapping-tck/src/main/groovy/org/apache/grails/data/testing/tck/tests/OneToOneSpec.groovy +++ b/grails-datamapping-tck/src/main/groovy/org/apache/grails/data/testing/tck/tests/OneToOneSpec.groovy @@ -102,6 +102,7 @@ class OneToOneSpec extends GrailsDataTckSpec { @Entity class OwnerEntity { + } @Entity diff --git a/grails-doc/src/en/ref/Domain Classes/executeQuery.adoc b/grails-doc/src/en/ref/Domain Classes/executeQuery.adoc index f6c1cb1286e..74e6d4f0c99 100644 --- a/grails-doc/src/en/ref/Domain Classes/executeQuery.adoc +++ b/grails-doc/src/en/ref/Domain Classes/executeQuery.adoc @@ -76,7 +76,15 @@ Account.executeQuery("select distinct a.number from Account a", // modify the FlushMode of the Query (default is `FlushMode.AUTO`) Account.executeQuery("select distinct a.number from Account a", - null, [flushMode: FlushMode.MANUAL]) + null, [flushMode: FlushMode.MANUAL]) + +// enable the query cache when Hibernate query caching is configured +Account.executeQuery("select distinct a.number from Account a", + null, [cache: true]) + +// request a pessimistic write lock; locked queries are not query-cacheable +Account.executeQuery("from Account a where a.branch = :branch", + [branch: 'London'], [lock: true]) ---- @@ -99,4 +107,4 @@ Parameters: * `query` - An HQL query * `positionalParams` - A `List` of parameters for a positional parameterized query * `namedParams` - A `Map` of named parameters for a named parameterized query -* `metaParams` - A `Map` of pagination parameters `max` or/and `offset`, as well as Hibernate query parameters `readOnly`, `fetchSize`, `timeout`, and `flushMode` +* `metaParams` - A `Map` of pagination parameters `max` and/or `offset`, as well as Hibernate query parameters `readOnly`, `fetchSize`, `timeout`, `flushMode`, `cache`, and `lock` diff --git a/grails-doc/src/en/ref/Domain Classes/find.adoc b/grails-doc/src/en/ref/Domain Classes/find.adoc index da7192da4df..220065de3b4 100644 --- a/grails-doc/src/en/ref/Domain Classes/find.adoc +++ b/grails-doc/src/en/ref/Domain Classes/find.adoc @@ -78,5 +78,5 @@ Parameters: * `query` - An HQL query * `positionalParams` - A `List` of parameters for a positional parametrized HQL query * `namedParams` - A `Map` of named parameters a HQL query -* `queryParams` - A `Map` of query parameters. Currently, only `cache` is supported +* `queryParams` - A `Map` of query parameters such as `cache`, `readOnly`, `fetchSize`, `timeout`, `flushMode`, and `lock` * `example` - An instance of the domain class for query by example diff --git a/grails-doc/src/en/ref/Domain Classes/findAll.adoc b/grails-doc/src/en/ref/Domain Classes/findAll.adoc index 0e9ae24d449..7d7724fbda8 100644 --- a/grails-doc/src/en/ref/Domain Classes/findAll.adoc +++ b/grails-doc/src/en/ref/Domain Classes/findAll.adoc @@ -119,9 +119,10 @@ Parameters: * `query` - An HQL query * `positionalParams` - A `List` of parameters for a positional parametrized HQL query * `namedParams` - A `Map` of named parameters a HQL query -* `queryParams` - A `Map` containing parameters 'max', and/or 'offset' and/or 'cache' +* `queryParams` - A `Map` containing parameters `max`, `offset`, `cache`, and other supported query settings * `example` - An instance of the domain class for query by example * `readOnly` - `true` if returned objects should not be automatically dirty-checked (simlar to `read()`) * `fetchSize` - number of rows fetched by the underlying JDBC driver per round trip * `flushMode` - Hibernate `FlushMode` override, defaults to `FlushMode.AUTO` * `timeout` - query timeout in seconds +* `lock` - `true` to request a pessimistic write lock; locked queries are not query-cacheable diff --git a/grails-doc/src/en/ref/Domain Classes/findAllWhere.adoc b/grails-doc/src/en/ref/Domain Classes/findAllWhere.adoc index 2ca643b29c5..8ce1c9380f3 100644 --- a/grails-doc/src/en/ref/Domain Classes/findAllWhere.adoc +++ b/grails-doc/src/en/ref/Domain Classes/findAllWhere.adoc @@ -60,6 +60,11 @@ def unreleasedBooks = Book.findAllWhere(releaseDate: null) === Description +`findAllWhere` returns all matching instances. A `null` value in the argument map matches rows where that property is `null`. + +Only domain class property names may be used as keys in the argument map. Invalid property names are rejected instead of being interpolated into the generated query. + Parameters: * `queryParams` - A Map of key/value pairs to be used in the query +* `args` - Optional query arguments such as `max`, `offset`, `cache`, `readOnly`, `fetchSize`, `timeout`, `flushMode`, and `lock` diff --git a/grails-doc/src/en/ref/Domain Classes/findWhere.adoc b/grails-doc/src/en/ref/Domain Classes/findWhere.adoc index 591bd0ab915..5dce3895c10 100644 --- a/grails-doc/src/en/ref/Domain Classes/findWhere.adoc +++ b/grails-doc/src/en/ref/Domain Classes/findWhere.adoc @@ -62,6 +62,11 @@ boolean isReleased = Book.findWhere(author: "Stephen King", === Description +`findWhere` returns the first matching instance, or `null` when no instance matches. A `null` value in the argument map matches rows where that property is `null`. + +Only domain class property names may be used as keys in the argument map. Invalid property names are rejected instead of being interpolated into the generated query. + Parameters: * `queryParams` - A `Map` of key/value pairs to be used in the query +* `args` - Optional query arguments such as `offset`, `cache`, `readOnly`, `fetchSize`, `timeout`, `flushMode`, and `lock`. `findWhere` always limits the query to one result. diff --git a/grails-doc/src/en/ref/Domain Classes/getAll.adoc b/grails-doc/src/en/ref/Domain Classes/getAll.adoc index bd8d3fc1e15..e9fb04f576e 100644 --- a/grails-doc/src/en/ref/Domain Classes/getAll.adoc +++ b/grails-doc/src/en/ref/Domain Classes/getAll.adoc @@ -48,6 +48,8 @@ def bookList = Book.getAll() === Description +The returned list preserves the order of the ids supplied by the caller. If an id is `null` or does not exist, the corresponding position in the returned list is `null`. + Parameters: * `varargs`* - A variable argument list of ids diff --git a/grails-forge/gradle.properties b/grails-forge/gradle.properties index e182da08b06..6a59372d664 100644 --- a/grails-forge/gradle.properties +++ b/grails-forge/gradle.properties @@ -49,6 +49,7 @@ reflectionsVersion=0.10.2 rockerVersion=2.2.1 shadowVersion=8.3.6 spotlessVersion=6.25.0 +testcontainersVersion=1.20.4 testRetryVersion=1.6.2 typesafeConfigVersion=1.4.3 diff --git a/grails-forge/grails-forge-analytics-postgres/build.gradle b/grails-forge/grails-forge-analytics-postgres/build.gradle index 92b19450fa7..04fe266cf81 100644 --- a/grails-forge/grails-forge-analytics-postgres/build.gradle +++ b/grails-forge/grails-forge-analytics-postgres/build.gradle @@ -28,9 +28,11 @@ group = 'org.apache.grails.forge' dependencies { + annotationProcessor platform("io.micronaut.platform:micronaut-platform:$micronautVersion") annotationProcessor 'io.micronaut:micronaut-graal' annotationProcessor 'io.micronaut.data:micronaut-data-processor' + implementation platform("io.micronaut.platform:micronaut-platform:$micronautVersion") implementation project(':grails-forge-core') implementation "com.google.cloud.sql:postgres-socket-factory:$postgresSocketFactoryVersion" implementation 'io.micronaut.data:micronaut-data-jdbc' diff --git a/grails-test-examples/gorm/grails-app/conf/application.yml b/grails-test-examples/gorm/grails-app/conf/application.yml index 1d72d9b2d19..e7afc511afd 100644 --- a/grails-test-examples/gorm/grails-app/conf/application.yml +++ b/grails-test-examples/gorm/grails-app/conf/application.yml @@ -60,10 +60,8 @@ grails: --- hibernate: cache: - use_second_level_cache: true - provider_class: net.sf.ehcache.hibernate.EhCacheProvider - region: - factory_class: org.hibernate.cache.ehcache.EhCacheRegionFactory + use_second_level_cache: false + use_query_cache: false dataSource: pooled: true jmxExport: true diff --git a/grails-test-examples/hyphenated/grails-app/conf/application.yml b/grails-test-examples/hyphenated/grails-app/conf/application.yml index be778a1b977..e809a364af6 100644 --- a/grails-test-examples/hyphenated/grails-app/conf/application.yml +++ b/grails-test-examples/hyphenated/grails-app/conf/application.yml @@ -62,9 +62,13 @@ grails: hibernate: cache: queries: false - use_second_level_cache: true + use_second_level_cache: false use_query_cache: false - region.factory_class: 'org.hibernate.cache.ehcache.EhCacheRegionFactory' + +endpoints: + jmx: + unique-names: true + dataSource: pooled: true jmxExport: true