diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/.github/ci-prerequisites-atlas.sh b/.github/ci-prerequisites-atlas.sh new file mode 100755 index 000000000000..b0a16f22b1e8 --- /dev/null +++ b/.github/ci-prerequisites-atlas.sh @@ -0,0 +1,6 @@ +# Reclaims disk space and sanitizes user home on Atlas infrastructure + +# We use the GitHub cache for the relevant parts of these directories. +# Also, we do not want to keep things like ~/.gradle/build-scan-data. +rm -rf ~/.gradle/ +rm -rf ~/.m2/ diff --git a/.github/workflows/atlas.yml b/.github/workflows/atlas.yml deleted file mode 100644 index afd8892e7cf8..000000000000 --- a/.github/workflows/atlas.yml +++ /dev/null @@ -1,118 +0,0 @@ -# The main CI of Hibernate ORM is https://ci.hibernate.org/job/hibernate-orm-pipeline/. -# However, Hibernate ORM builds run on GitHub actions regularly -# to check that it still works and can be used in GitHub forks. -# See https://docs.github.com/en/free-pro-team@latest/actions -# for more information about GitHub actions. - -name: Hibernate ORM build-Atlas - -on: - push: - branches: - - 'main' - # WARNING: Using pull_request_target to access secrets, but we check out the PR head commit. - # See checkout action for details. - pull_request_target: - branches: - - 'main' - -permissions: {} # none - -# See https://github.com/hibernate/hibernate-orm/pull/4615 for a description of the behavior we're getting. -concurrency: - # Consider that two builds are in the same concurrency group (cannot run concurrently) - # if they use the same workflow and are about the same branch ("ref") or pull request. - group: "workflow = ${{ github.workflow }}, ref = ${{ github.event.ref }}, pr = ${{ github.event.pull_request.id }}" - # Cancel previous builds in the same concurrency group even if they are in process - # for pull requests or pushes to forks (not the upstream repository). - cancel-in-progress: ${{ github.event_name == 'pull_request_target' || github.repository != 'hibernate/hibernate-orm' }} - -jobs: - build: - permissions: - contents: read - name: Java 11 - # runs-on: ubuntu-latest - runs-on: [self-hosted, Linux, X64, OCI] - strategy: - fail-fast: false - matrix: - include: - - rdbms: oracle_atps - - rdbms: oracle_db19c - - rdbms: oracle_db21c - - rdbms: oracle_db23c - steps: - - name: Check out commit already pushed to branch - if: "! github.event.pull_request.number" - uses: actions/checkout@v4 - with: - persist-credentials: false - - name: Check out PR head - uses: actions/checkout@v4 - if: github.event.pull_request.number - with: - # WARNING: This is potentially dangerous since we're checking out unreviewed code, - # and since we're using the pull_request_target event we can use secrets. - # Thus, we must be extra careful to never expose secrets to steps that execute this code, - # and to strictly limit our of secrets to those that only pose minor security threats. - # This means in particular we won't expose Develocity credentials to the main gradle executions, - # but instead will execute gradle a second time just to push build scans to Develocity; - # see below. - ref: "refs/pull/${{ github.event.pull_request.number }}/head" - persist-credentials: false - - name: Reclaim Disk Space - run: .github/ci-prerequisites.sh - - name: Start database - env: - RDBMS: ${{ matrix.rdbms }} - RUNID: ${{ github.run_number }} - run: ci/database-start.sh - - name: Set up Java 11 - uses: graalvm/setup-graalvm@v1 - with: - distribution: 'graalvm' - java-version: '21' - - name: Get year/month for cache key - id: get-date - run: echo "yearmonth=$(/bin/date -u "+%Y-%m")" >> $GITHUB_OUTPUT - shell: bash - - name: Cache Maven local repository - uses: actions/cache@v4 - id: cache-maven - with: - path: | - ~/.m2/repository - ~/.gradle/caches/ - ~/.gradle/wrapper/ - # refresh cache every month to avoid unlimited growth - key: maven-localrepo-${{ steps.get-date.outputs.yearmonth }} - - name: Run build script - env: - RDBMS: ${{ matrix.rdbms }} - RUNID: ${{ github.run_number }} - # WARNING: exposes secrets, so must only be passed to a step that doesn't run unapproved code. - # WARNING: As this runs on untrusted nodes, we use the same access key as for PRs: - # it has limited access, essentially it can only push build scans. - DEVELOCITY_ACCESS_KEY: "${{ github.event_name == 'push' && secrets.GRADLE_ENTERPRISE_ACCESS_KEY_PR || '' }}" - run: ./ci/build-github.sh - shell: bash - - name: Publish Develocity build scan for previous build - # Don't fail a build if publishing fails - continue-on-error: true - if: "${{ !cancelled() && github.event_name == 'pull_request_target' && github.repository == 'hibernate/hibernate-orm' }}" - run: | - ./gradlew buildScanPublishPrevious - env: - # WARNING: exposes secrets, so must only be passed to a step that doesn't run unapproved code. - DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY_PR }} - - name: Upload test reports (if Gradle failed) - uses: actions/upload-artifact@v4 - if: failure() - with: - name: test-reports-java11-${{ matrix.rdbms }} - path: | - ./**/target/reports/tests/ - ./**/target/reports/checkstyle/ - - name: Omit produced artifacts from build cache - run: ./ci/before-cache.sh \ No newline at end of file diff --git a/.github/workflows/ci-report.yml b/.github/workflows/ci-report.yml new file mode 100644 index 000000000000..73ce74cd3b96 --- /dev/null +++ b/.github/workflows/ci-report.yml @@ -0,0 +1,78 @@ +name: GH Actions CI reporting + +on: + workflow_run: + workflows: [ "GH Actions CI" ] + types: [ completed ] + +defaults: + run: + shell: bash + +jobs: + publish-build-scans: + name: Publish Develocity build scans + if: github.repository == 'hibernate/hibernate-orm' && github.event.workflow_run.conclusion != 'cancelled' + runs-on: ubuntu-latest + steps: + # Checkout target branch which has trusted code + - name: Check out target branch + uses: actions/checkout@v4 + with: + persist-credentials: false + ref: ${{ github.ref }} + - name: Set up Java 11 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '11' + + - name: Generate cache key + id: cache-key + run: | + CURRENT_BRANCH="${{ github.repository != 'hibernate/hibernate-orm' && 'fork' || github.base_ref || github.ref_name }}" + CURRENT_MONTH=$(/bin/date -u "+%Y-%m") + CURRENT_DAY=$(/bin/date -u "+%d") + ROOT_CACHE_KEY="buildtool-cache" + echo "buildtool-monthly-cache-key=${ROOT_CACHE_KEY}-${CURRENT_MONTH}" >> $GITHUB_OUTPUT + echo "buildtool-monthly-branch-cache-key=${ROOT_CACHE_KEY}-${CURRENT_MONTH}-${CURRENT_BRANCH}" >> $GITHUB_OUTPUT + echo "buildtool-cache-key=${ROOT_CACHE_KEY}-${CURRENT_MONTH}-${CURRENT_BRANCH}-${CURRENT_DAY}" >> $GITHUB_OUTPUT + - name: Restore Maven/Gradle Dependency/Dist Caches + uses: actions/cache/restore@v4 + with: + path: | + ~/.m2/repository/ + ~/.m2/wrapper/ + ~/.gradle/caches/modules-2 + ~/.gradle/wrapper/ + key: ${{ steps.cache-key.outputs.buildtool-cache-key }} + restore-keys: | + ${{ steps.cache-key.outputs.buildtool-monthly-branch-cache-key }}- + ${{ steps.cache-key.outputs.buildtool-monthly-cache-key }}- + + - name: Download GitHub Actions artifacts for the Develocity build scans + id: downloadBuildScan + uses: actions/download-artifact@v4 + with: + pattern: build-scan-data-* + github-token: ${{ github.token }} + repository: ${{ github.repository }} + run-id: ${{ github.event.workflow_run.id }} + path: /tmp/downloaded-build-scan-data/ + # Don't fail the build if there are no matching artifacts + continue-on-error: true + - name: Publish Develocity build scans for previous builds + if: ${{ steps.downloadBuildScan.outcome != 'failure'}} + run: | + shopt -s nullglob # Don't run the loop below if there are no artifacts + status=0 + mkdir -p ~/.gradle/ + for build_scan_data_directory in /tmp/downloaded-build-scan-data/* + do + rm -rf ~/.gradle/build-scan-data + mv "$build_scan_data_directory" ~/.gradle/build-scan-data \ + && ./gradlew --no-build-cache buildScanPublishPrevious || status=1 + done + exit $status + env: + DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY_PR }} \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000000..ea263c9b627d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,236 @@ +name: GH Actions CI + +on: + pull_request: + branches: + - '6.6' + +permissions: {} # none + +# See https://github.com/hibernate/hibernate-orm/pull/4615 for a description of the behavior we're getting. +concurrency: + # Consider that two builds are in the same concurrency group (cannot run concurrently) + # if they use the same workflow and are about the same branch ("ref") or pull request. + group: "workflow = ${{ github.workflow }}, ref = ${{ github.event.ref }}, pr = ${{ github.event.pull_request.id }}" + # Cancel previous builds in the same concurrency group even if they are in progress + # for pull requests or pushes to forks (not the upstream repository). + cancel-in-progress: ${{ github.event_name == 'pull_request' || github.repository != 'hibernate/hibernate-orm' }} + +jobs: + + # Main job for h2/docker DBs. + build: + permissions: + contents: read + name: OpenJDK 11 - ${{matrix.rdbms}} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - rdbms: h2 + - rdbms: hsqldb + - rdbms: derby + - rdbms: mysql + - rdbms: mariadb + - rdbms: postgresql + - rdbms: edb + - rdbms: oracle + - rdbms: db2 + - rdbms: mssql + - rdbms: sybase +# Running with CockroachDB requires at least 2-4 vCPUs, which we don't have on GH Actions runners +# - rdbms: cockroachdb +# Running with HANA requires at least 8GB memory just for the database, which we don't have on GH Actions runners +# - rdbms: hana + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Reclaim Disk Space + run: .github/ci-prerequisites.sh + - name: Start database + env: + RDBMS: ${{ matrix.rdbms }} + run: ci/database-start.sh + - name: Set up Java 11 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '11' + + - name: Generate cache key + id: cache-key + run: | + CURRENT_BRANCH="${{ github.repository != 'hibernate/hibernate-orm' && 'fork' || github.base_ref || github.ref_name }}" + CURRENT_MONTH=$(/bin/date -u "+%Y-%m") + CURRENT_DAY=$(/bin/date -u "+%d") + ROOT_CACHE_KEY="buildtool-cache" + echo "buildtool-monthly-cache-key=${ROOT_CACHE_KEY}-${CURRENT_MONTH}" >> $GITHUB_OUTPUT + echo "buildtool-monthly-branch-cache-key=${ROOT_CACHE_KEY}-${CURRENT_MONTH}-${CURRENT_BRANCH}" >> $GITHUB_OUTPUT + echo "buildtool-cache-key=${ROOT_CACHE_KEY}-${CURRENT_MONTH}-${CURRENT_BRANCH}-${CURRENT_DAY}" >> $GITHUB_OUTPUT + - name: Cache Maven/Gradle Dependency/Dist Caches + id: cache-maven + uses: actions/cache@v4 + # if it's not a pull request, we restore and save the cache + if: github.event_name != 'pull_request' + with: + path: | + ~/.m2/repository/ + ~/.m2/wrapper/ + ~/.gradle/caches/modules-2 + ~/.gradle/wrapper/ + # A new cache will be stored daily. After that first store of the day, cache save actions will fail because the cache is immutable but it's not a problem. + # The whole cache is dropped monthly to prevent unlimited growth. + # The cache is per branch but in case we don't find a branch for a given branch, we will get a cache from another branch. + key: ${{ steps.cache-key.outputs.buildtool-cache-key }} + restore-keys: | + ${{ steps.cache-key.outputs.buildtool-monthly-branch-cache-key }}- + ${{ steps.cache-key.outputs.buildtool-monthly-cache-key }}- + - name: Restore Maven/Gradle Dependency/Dist Caches + uses: actions/cache/restore@v4 + # if it a pull request, we restore the cache but we don't save it + if: github.event_name == 'pull_request' + with: + path: | + ~/.m2/repository/ + ~/.m2/wrapper/ + ~/.gradle/caches/modules-2 + ~/.gradle/wrapper/ + key: ${{ steps.cache-key.outputs.buildtool-cache-key }} + restore-keys: | + ${{ steps.cache-key.outputs.buildtool-monthly-branch-cache-key }}- + ${{ steps.cache-key.outputs.buildtool-monthly-cache-key }}- + + - name: Run build script + run: ./ci/build-github.sh + shell: bash + env: + RDBMS: ${{ matrix.rdbms }} + # For jobs running on 'push', publish build scan and cache immediately. + # This won't work for pull requests, since they don't have access to secrets. + POPULATE_REMOTE_GRADLE_CACHE: ${{ github.event_name == 'push' && github.repository == 'hibernate/hibernate-orm' && 'true' || 'false' }} + DEVELOCITY_ACCESS_KEY: "${{ secrets.DEVELOCITY_ACCESS_KEY }}" + + # For jobs running on 'pull_request', upload build scan data. + # The actual publishing must be done in a separate job (see ci-report.yml). + # We don't write to the remote cache as that would be unsafe. + - name: Upload GitHub Actions artifact for the Develocity build scan + uses: actions/upload-artifact@v4 + if: "${{ github.event_name == 'pull_request' && !cancelled() }}" + with: + name: build-scan-data-${{ matrix.rdbms }} + path: ~/.gradle/build-scan-data + + - name: Upload test reports (if Gradle failed) + uses: actions/upload-artifact@v4 + if: failure() + with: + name: test-reports-java11-${{ matrix.rdbms }} + path: | + ./**/target/reports/tests/ + - name: Omit produced artifacts from build cache + run: ./ci/before-cache.sh + + # Job for builds on Atlas (Oracle) infrastructure. + # This is untrusted, even for pushes, see below. + atlas: + permissions: + contents: read + name: GraalVM 21 - ${{matrix.rdbms}} + # runs-on: ubuntu-latest + runs-on: [self-hosted, Linux, X64, OCI] + strategy: + fail-fast: false + matrix: + include: + - rdbms: oracle_atps + - rdbms: oracle_db19c + - rdbms: oracle_db21c + - rdbms: oracle_db23c + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Reclaim disk space and sanitize user home + run: .github/ci-prerequisites-atlas.sh + - name: Start database + env: + RDBMS: ${{ matrix.rdbms }} + RUNID: ${{ github.run_number }} + run: ci/database-start.sh + - name: Set up Java 21 + uses: graalvm/setup-graalvm@v1 + with: + distribution: 'graalvm' + java-version: '21' + + - name: Generate cache key + id: cache-key + run: | + CURRENT_BRANCH="${{ github.repository != 'hibernate/hibernate-orm' && 'fork' || github.base_ref || github.ref_name }}" + CURRENT_MONTH=$(/bin/date -u "+%Y-%m") + CURRENT_DAY=$(/bin/date -u "+%d") + ROOT_CACHE_KEY="buildtool-cache-atlas" + echo "buildtool-monthly-cache-key=${ROOT_CACHE_KEY}-${CURRENT_MONTH}" >> $GITHUB_OUTPUT + echo "buildtool-monthly-branch-cache-key=${ROOT_CACHE_KEY}-${CURRENT_MONTH}-${CURRENT_BRANCH}" >> $GITHUB_OUTPUT + echo "buildtool-cache-key=${ROOT_CACHE_KEY}-${CURRENT_MONTH}-${CURRENT_BRANCH}-${CURRENT_DAY}" >> $GITHUB_OUTPUT + - name: Cache Maven/Gradle Dependency/Dist Caches + id: cache-maven + uses: actions/cache@v4 + # if it's not a pull request, we restore and save the cache + if: github.event_name != 'pull_request' + with: + path: | + ~/.m2/repository/ + ~/.m2/wrapper/ + ~/.gradle/caches/modules-2 + ~/.gradle/wrapper/ + # A new cache will be stored daily. After that first store of the day, cache save actions will fail because the cache is immutable but it's not a problem. + # The whole cache is dropped monthly to prevent unlimited growth. + # The cache is per branch but in case we don't find a branch for a given branch, we will get a cache from another branch. + key: ${{ steps.cache-key.outputs.buildtool-cache-key }} + restore-keys: | + ${{ steps.cache-key.outputs.buildtool-monthly-branch-cache-key }}- + ${{ steps.cache-key.outputs.buildtool-monthly-cache-key }}- + - name: Restore Maven/Gradle Dependency/Dist Caches + uses: actions/cache/restore@v4 + # if it a pull request, we restore the cache but we don't save it + if: github.event_name == 'pull_request' + with: + path: | + ~/.m2/repository/ + ~/.m2/wrapper/ + ~/.gradle/caches/modules-2 + ~/.gradle/wrapper/ + key: ${{ steps.cache-key.outputs.buildtool-cache-key }} + restore-keys: | + ${{ steps.cache-key.outputs.buildtool-monthly-branch-cache-key }}- + ${{ steps.cache-key.outputs.buildtool-monthly-cache-key }}- + + - name: Run build script + env: + RDBMS: ${{ matrix.rdbms }} + RUNID: ${{ github.run_number }} + run: ./ci/build-github.sh + shell: bash + # Upload build scan data. + # The actual publishing must be done in a separate job (see ci-report.yml). + # We don't write to the remote cache as that would be unsafe. + # That's even on push, because we do not trust Atlas runners to hold secrets: they are shared infrastructure. + - name: Upload GitHub Actions artifact for the Develocity build scan + uses: actions/upload-artifact@v4 + if: "${{ !cancelled() }}" + with: + name: build-scan-data-${{ matrix.rdbms }} + path: ~/.gradle/build-scan-data + - name: Upload test reports (if Gradle failed) + uses: actions/upload-artifact@v4 + if: failure() + with: + name: test-reports-java11-${{ matrix.rdbms }} + path: | + ./**/target/reports/tests/ + ./**/target/reports/checkstyle/ + - name: Omit produced artifacts from build cache + run: ./ci/before-cache.sh \ No newline at end of file diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index a05c7b585b5d..77756582862a 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,11 +1,8 @@ name: "CodeQL" on: - push: - branches: [ 'main' ] pull_request: - # The branches below must be a subset of the branches above - branches: [ 'main' ] + branches: [ '6.6' ] schedule: - cron: '34 11 * * 4' diff --git a/.github/workflows/contributor-build.yml b/.github/workflows/contributor-build.yml deleted file mode 100644 index 8d393ce5cfdb..000000000000 --- a/.github/workflows/contributor-build.yml +++ /dev/null @@ -1,126 +0,0 @@ -# The main CI of Hibernate ORM is https://ci.hibernate.org/job/hibernate-orm-pipeline/. -# However, Hibernate ORM builds run on GitHub actions regularly -# to check that it still works and can be used in GitHub forks. -# See https://docs.github.com/en/free-pro-team@latest/actions -# for more information about GitHub actions. - -name: Hibernate ORM build - -on: - push: - branches: - - 'main' - # WARNING: Using pull_request_target to access secrets, but we check out the PR head commit. - # See checkout action for details. - pull_request_target: - branches: - - 'main' - -permissions: {} # none - -# See https://github.com/hibernate/hibernate-orm/pull/4615 for a description of the behavior we're getting. -concurrency: - # Consider that two builds are in the same concurrency group (cannot run concurrently) - # if they use the same workflow and are about the same branch ("ref") or pull request. - group: "workflow = ${{ github.workflow }}, ref = ${{ github.event.ref }}, pr = ${{ github.event.pull_request.id }}" - # Cancel previous builds in the same concurrency group even if they are in process - # for pull requests or pushes to forks (not the upstream repository). - cancel-in-progress: ${{ github.event_name == 'pull_request_target' || github.repository != 'hibernate/hibernate-orm' }} - -jobs: - build: - permissions: - contents: read - name: Java 11 - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - include: - - rdbms: h2 - - rdbms: hsqldb - - rdbms: derby - - rdbms: mysql - - rdbms: mariadb - - rdbms: postgresql - - rdbms: edb - - rdbms: oracle - - rdbms: db2 - - rdbms: mssql - - rdbms: sybase -# Running with CockroachDB requires at least 2-4 vCPUs, which we don't have on GH Actions runners -# - rdbms: cockroachdb -# Running with HANA requires at least 8GB memory just for the database, which we don't have on GH Actions runners -# - rdbms: hana - steps: - - name: Check out commit already pushed to branch - if: "! github.event.pull_request.number" - uses: actions/checkout@v4 - with: - persist-credentials: false - - name: Check out PR head - uses: actions/checkout@v4 - if: github.event.pull_request.number - with: - # WARNING: This is potentially dangerous since we're checking out unreviewed code, - # and since we're using the pull_request_target event we can use secrets. - # Thus, we must be extra careful to never expose secrets to steps that execute this code, - # and to strictly limit our of secrets to those that only pose minor security threats. - # This means in particular we won't expose Develocity credentials to the main gradle executions, - # but instead will execute gradle a second time just to push build scans to Develocity; - # see below. - ref: "refs/pull/${{ github.event.pull_request.number }}/head" - persist-credentials: false - - name: Reclaim Disk Space - run: .github/ci-prerequisites.sh - - name: Start database - env: - RDBMS: ${{ matrix.rdbms }} - run: ci/database-start.sh - - name: Set up Java 11 - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: '11' - - name: Get year/month for cache key - id: get-date - run: echo "yearmonth=$(/bin/date -u "+%Y-%m")" >> $GITHUB_OUTPUT - shell: bash - - name: Cache Maven local repository - uses: actions/cache@v4 - id: cache-maven - with: - path: | - ~/.m2/repository - ~/.gradle/caches/ - ~/.gradle/wrapper/ - # refresh cache every month to avoid unlimited growth - key: maven-localrepo-${{ steps.get-date.outputs.yearmonth }} - - name: Run build script - env: - RDBMS: ${{ matrix.rdbms }} - # Don't populate Develocity cache in pull requests as that's potentially dangerous - POPULATE_REMOTE_GRADLE_CACHE: "${{ github.event_name == 'push' }}" - # WARNING: exposes secrets, so must only be passed to a step that doesn't run unapproved code. - DEVELOCITY_ACCESS_KEY: "${{ github.event_name == 'push' && secrets.GRADLE_ENTERPRISE_ACCESS_KEY || '' }}" - run: ./ci/build-github.sh - shell: bash - - name: Publish Develocity build scan for previous build (pull request) - # Don't fail a build if publishing fails - continue-on-error: true - if: "${{ !cancelled() && github.event_name == 'pull_request_target' && github.repository == 'hibernate/hibernate-orm' }}" - run: | - ./gradlew buildScanPublishPrevious - env: - # WARNING: exposes secrets, so must only be passed to a step that doesn't run unapproved code. - DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY_PR }} - - name: Upload test reports (if Gradle failed) - uses: actions/upload-artifact@v4 - if: failure() - with: - name: test-reports-java11-${{ matrix.rdbms }} - path: | - ./**/target/reports/tests/ - ./**/target/reports/checkstyle/ - - name: Omit produced artifacts from build cache - run: ./ci/before-cache.sh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b4f6d3a6ab24..57b4caddbd1a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -51,6 +51,7 @@ GitHub there are a few pre-requisite steps to follow: the linked page, this also includes: * [set up your local git install](https://help.github.com/articles/set-up-git) * clone your fork +* Instruct git to ignore certain commits when using `git blame`. From the directory of your local clone, run this: `git config blame.ignoreRevsFile .git-blame-ignore-revs` * See the wiki pages for setting up your IDE, whether you use [IntelliJ IDEA](https://hibernate.org/community/contribute/intellij-idea/) or [Eclipse](https://hibernate.org/community/contribute/eclipse-ide/)(1). diff --git a/Jenkinsfile b/Jenkinsfile index 9b6d025b00c9..9d0a78ccfeb8 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -28,49 +28,22 @@ stage('Configure') { requireApprovalForPullRequest 'hibernate' this.environments = [ -// new BuildEnvironment( dbName: 'h2' ), -// new BuildEnvironment( dbName: 'hsqldb' ), -// new BuildEnvironment( dbName: 'derby' ), -// new BuildEnvironment( dbName: 'mysql' ), -// new BuildEnvironment( dbName: 'mariadb' ), -// new BuildEnvironment( dbName: 'postgresql' ), -// new BuildEnvironment( dbName: 'edb' ), -// new BuildEnvironment( dbName: 'oracle' ), -// new BuildEnvironment( dbName: 'db2' ), -// new BuildEnvironment( dbName: 'mssql' ), -// new BuildEnvironment( dbName: 'sybase' ), -// Don't build with HANA by default, but only do it nightly until we receive a 3rd instance -// new BuildEnvironment( dbName: 'hana_cloud', dbLockableResource: 'hana-cloud', dbLockResourceAsHost: true ), new BuildEnvironment( node: 's390x' ), - new BuildEnvironment( dbName: 'tidb', node: 'tidb', - notificationRecipients: 'tidb_hibernate@pingcap.com' ), + new BuildEnvironment( dbName: 'sybase_jconn' ), new BuildEnvironment( testJdkVersion: '17' ), + new BuildEnvironment( testJdkVersion: '21' ), // We want to enable preview features when testing newer builds of OpenJDK: // even if we don't use these features, just enabling them can cause side effects // and it's useful to test that. - new BuildEnvironment( testJdkVersion: '20', testJdkLauncherArgs: '--enable-preview' ), - new BuildEnvironment( testJdkVersion: '21', testJdkLauncherArgs: '--enable-preview' ), - new BuildEnvironment( testJdkVersion: '22', testJdkLauncherArgs: '--enable-preview' ), + new BuildEnvironment( testJdkVersion: '23', testJdkLauncherArgs: '--enable-preview' ), + new BuildEnvironment( testJdkVersion: '24', testJdkLauncherArgs: '--enable-preview' ), // The following JDKs aren't supported by Hibernate ORM out-of-the box yet: // they require the use of -Dnet.bytebuddy.experimental=true. // Make sure to remove that argument as soon as possible // -- generally that requires upgrading bytebuddy after the JDK goes GA. - new BuildEnvironment( testJdkVersion: '23', testJdkLauncherArgs: '--enable-preview -Dnet.bytebuddy.experimental=true' ), - new BuildEnvironment( testJdkVersion: '24', testJdkLauncherArgs: '--enable-preview -Dnet.bytebuddy.experimental=true' ) + new BuildEnvironment( testJdkVersion: '25', testJdkLauncherArgs: '--enable-preview -Dnet.bytebuddy.experimental=true' ) ]; - if ( env.CHANGE_ID ) { - if ( pullRequest.labels.contains( 'cockroachdb' ) ) { - this.environments.add( new BuildEnvironment( dbName: 'cockroachdb', node: 'cockroachdb', longRunning: true ) ) - } - if ( pullRequest.labels.contains( 'hana' ) ) { - this.environments.add( new BuildEnvironment( dbName: 'hana_cloud', dbLockableResource: 'hana-cloud', dbLockResourceAsHost: true ) ) - } - if ( pullRequest.labels.contains( 'sybase' ) ) { - this.environments.add( new BuildEnvironment( dbName: 'sybase_jconn' ) ) - } - } - helper.configure { file 'job-configuration.yaml' // We don't require the following, but the build helper plugin apparently does @@ -98,15 +71,17 @@ if (currentBuild.getBuildCauses().toString().contains('BranchIndexingCause')) { currentBuild.result = 'NOT_BUILT' return } +// This is a limited maintenance branch, so don't run this on pushes to the branch, only on PRs +if ( !env.CHANGE_ID ) { + print "INFO: Build skipped because this job should only run for pull request, not for branch pushes" + currentBuild.result = 'NOT_BUILT' + return +} stage('Build') { Map executions = [:] Map> state = [:] environments.each { BuildEnvironment buildEnv -> - // Don't build environments for newer JDKs when this is a PR - if ( helper.scmSource.pullRequest && buildEnv.testJdkVersion ) { - return - } state[buildEnv.tag] = [:] executions.put(buildEnv.tag, { runBuildOnNode(buildEnv.node ?: NODE_PATTERN_BASE) { @@ -199,6 +174,9 @@ stage('Build') { } }) } + executions.put('Hibernate Search Update Dependency', { + build job: '/hibernate-search-dependency-update/7.2', propagate: true, parameters: [string(name: 'UPDATE_JOB', value: 'orm6.6'), string(name: 'ORM_REPOSITORY', value: helper.scmSource.remoteUrl), string(name: 'ORM_PULL_REQUEST_ID', value: helper.scmSource.pullRequest.id)] + }) parallel(executions) } @@ -242,7 +220,7 @@ void ciBuild(buildEnv, String args) { // On untrusted nodes, we use the same access key as for PRs: // it has limited access, essentially it can only push build scans. - def develocityCredentialsId = buildEnv.node ? 'ge.hibernate.org-access-key-pr' : 'ge.hibernate.org-access-key' + def develocityCredentialsId = buildEnv.node ? 'develocity.commonhaus.dev-access-key-pr' : 'develocity.commonhaus.dev-access-key' withCredentials([string(credentialsId: develocityCredentialsId, variable: 'DEVELOCITY_ACCESS_KEY')]) { @@ -251,13 +229,13 @@ void ciBuild(buildEnv, String args) { } } } - else if ( buildEnv.node && buildEnv.node != 's390x' ) { // We couldn't get the code below to work on s390x for some reason. + else if ( buildEnv.node != 's390x' ) { // We couldn't get the code below to work on s390x for some reason. // Pull request: we can't pass credentials to the build, since we'd be exposing secrets to e.g. tests. // We do the build first, then publish the build scan separately. tryFinally({ sh "./ci/build.sh $args" }, { // Finally - withCredentials([string(credentialsId: 'ge.hibernate.org-access-key-pr', + withCredentials([string(credentialsId: 'develocity.commonhaus.dev-access-key-pr', variable: 'DEVELOCITY_ACCESS_KEY')]) { withGradle { // withDevelocity, actually: https://plugins.jenkins.io/gradle/#plugin-content-capturing-build-scans-from-jenkins-pipeline // Don't fail a build if publishing fails diff --git a/README.adoc b/README.adoc index 9392eca52045..a1fbc80d7119 100644 --- a/README.adoc +++ b/README.adoc @@ -5,7 +5,7 @@ Hibernate implements JPA, the standard API for object/relational persistence in See https://hibernate.org/orm/[Hibernate.org] for more information. image:https://ci.hibernate.org/job/hibernate-orm-pipeline/job/main/badge/icon[Build Status,link=https://ci.hibernate.org/job/hibernate-orm-pipeline/job/main/] -image:https://img.shields.io/badge/Revved%20up%20by-Develocity-06A0CE?logo=Gradle&labelColor=02303A[link=https://ge.hibernate.org/scans] +image:https://img.shields.io/badge/Revved%20up%20by-Develocity-06A0CE?logo=Gradle&labelColor=02303A[link=https://develocity.commonhaus.dev/scans] == Continuous Integration diff --git a/build.gradle b/build.gradle index ae7a4284a399..6a35b97c163e 100644 --- a/build.gradle +++ b/build.gradle @@ -28,8 +28,6 @@ plugins { id 'org.checkerframework' version '0.6.40' id 'org.hibernate.orm.build.jdks' - id 'io.github.gradle-nexus.publish-plugin' version '1.1.0' - id 'idea' id 'org.jetbrains.gradle.plugin.idea-ext' version '1.0' id 'eclipse' @@ -42,77 +40,29 @@ apply from: file( 'gradle/module.gradle' ) // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Release Task -task release { +tasks.register('release') { description = "The task performed when we are performing a release build. Relies on " + "the fact that subprojects will appropriately define a release task " + "themselves if they have any release-related activities to perform" doFirst { def javaVersionsInUse = jdkVersions.allVersions - if ( javaVersionsInUse != [JavaLanguageVersion.of( 11 )].toSet() ) { - throw new IllegalStateException( "Please use JDK 11 to perform the release. Currently using: ${javaVersionsInUse}" ) + if (javaVersionsInUse != [JavaLanguageVersion.of(11)].toSet()) { + throw new IllegalStateException("Please use JDK 11 to perform the release. Currently using: ${javaVersionsInUse}") } } } -task publish { +tasks.register('publish') { description = "The task performed when we want to just publish maven artifacts. Relies on " + "the fact that subprojects will appropriately define a release task " + "themselves if they have any publish-related activities to perform" } -ext { - if ( project.hasProperty( 'hibernatePublishUsername' ) ) { - if ( ! project.hasProperty( 'hibernatePublishPassword' ) ) { - throw new GradleException( "Should specify both `hibernatePublishUsername` and `hibernatePublishPassword` as project properties" ); - } - } -} - -nexusPublishing { - repositories { - sonatype { - username = project.hasProperty( 'hibernatePublishUsername' ) ? project.property( 'hibernatePublishUsername' ) : null - password = project.hasProperty( 'hibernatePublishPassword' ) ? project.property( 'hibernatePublishPassword' ) : null - } - } -} - -gradle.taskGraph.addTaskExecutionGraphListener( - new TaskExecutionGraphListener() { - @Override - void graphPopulated(TaskExecutionGraph graph) { - String[] tasksToLookFor = [ - 'publish', - 'publishToSonatype', - 'publishAllPublicationsToSonatype', - 'publishPublishedArtifactsPublicationToSonatypeRepository', - 'publishRelocationArtifactsPublicationToSonatypeRepository', - ] - - for ( String taskToLookFor : tasksToLookFor ) { - if ( graph.hasTask( taskToLookFor ) ) { - // trying to publish - make sure the needed credentials are available - - if ( project.property( 'hibernatePublishUsername' ) == null ) { - throw new RuntimeException( "`-PhibernatePublishUsername=...` not found" ) - } - if ( project.property( 'hibernatePublishPassword' ) == null ) { - throw new RuntimeException( "`-PhibernatePublishPassword=...` not found" ) - } - - break; - } - } - } - } -) - - // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // CI Build Task -task ciBuild { +tasks.register('ciBuild') { description = "The task performed when one of the 'main' jobs are triggered on the " + "CI server. Just as above, relies on the fact that subprojects will " + "appropriately define a release task themselves if they have any tasks " + @@ -139,6 +89,3 @@ idea { name = "hibernate-orm" } } - - - diff --git a/changelog.txt b/changelog.txt index 06f70064586b..d9cdb869b204 100644 --- a/changelog.txt +++ b/changelog.txt @@ -3,6 +3,353 @@ Hibernate 6 Changelog Note: Please refer to JIRA to learn more about each issue. +Changes in 6.6.16.Final (May 25, 2025) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/33606 + +** Bug + * [HHH-19477] - ConnectionReleaseMode.AFTER_STATEMENT ineffective due to missing connection release + * [HHH-19473] - Bytecode enhancement incorrectly generates code for Bidirectional Generic Entities + * [HHH-19472] - Native query "SELECT 1" with result type Object[] return singular object + * [HHH-19387] - AssertionError in EntityInitializerImpl data.concreteDescriptor is null + * [HHH-19372] - AccessOptimizer.setPropertyValues() and getPropertyValues() error with entity hierarchy. + * [HHH-19369] - Entity hierarchy ordering error within metamodel building with enhancement + * [HHH-19207] - JPA OrderBy annotated relation not ordered when using entity graph with criteria api + * [HHH-18813] - DML update of secondary table column fails + + +Changes in 6.6.15.Final (May 13, 2025) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/33406 + +** Bug + * [HHH-19458] - problem starting MockSessionFactory from Quarkus dev mode with Reactive + + +Changes in 6.6.14.Final (May 11, 2025) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/33241 + +** Bug + * [HHH-19425] - incorrect class literals in Processor-generated code + * [HHH-19375] - fix check for presence of Quarkus in reactive case + * [HHH-19374] - repositories should always be @Dependent + * [HHH-19320] - Assigned id value is not passed into BeforeExecutionGenerator#generate() method when allowAssignedIdentifiers() is true and id has been assigned + * [HHH-19314] - StackOverflowException when using onConflict with createCriteriaInsertValues and createCriteriaInsertSelect + * [HHH-19306] - Composite generator may not respect the event types of generators it consits of + * [HHH-19036] - ORM Filter are searched for in the wrong order + + +Changes in 6.6.13.Final (April 06, 2025) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/33142 + +** Bug + * [HHH-19314] - StackOverflowException when using onConflict with createCriteriaInsertValues and createCriteriaInsertSelect + + +Changes in 6.6.12.Final (March 30, 2025) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/32945 + +** Bug + * [HHH-19109] - Hibernate Data Repositories are @RequestScoped + * [HHH-19059] - Bytecode enhancement fails when inherited fields are mapped using property access in subclass + * [HHH-19017] - Class Cast Exception for PersistentAttributeInterceptable + * [HHH-18920] - Enum parameters in Jakarta Data repository method return type constructor are not properly matched + * [HHH-18745] - Unnecessary joins when use TREAT operator + * [HHH-14694] - Use stable proxy names to avoid managing proxy state and memory leaks + +** Task + * [HHH-19230] - Ensure that thread local for org.hibernate.bytecode.enhance.internal.bytebuddy.SafeCacheProvider + OverridingClassFileLocator are completely cleared + + +Changes in 6.6.11.Final (March 16, 2025) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/32879 + +** Bug + * [HHH-19246] - Fetch join makes partially covered EntityGraph ineffective + * [HHH-19220] - ClassCastException: class org.hibernate.bytecode.enhance.spi.LazyPropertyInitializer$1 cannot be cast to class java.lang.String + * [HHH-19140] - Enhanced entities with AccessType.PROPERTY does not work well with inheritance + * [HHH-19107] - Entities with @EmbeddedId not supported with CrudRepository + * [HHH-19106] - @Transaction(TxType) not working with Hibernate Data Repositories + * [HHH-19052] - Hibernate 6.6.X regression with join formula + * [HHH-18894] - Hibernate 6.6 enum literal is considered field literal instead + * [HHH-18881] - In MySQL, array of dates are not converted correctly + * [HHH-18858] - array fields and static metamodel + * [HHH-18787] - Custom UserType not recognised for array properties + * [HHH-18570] - Invalid SQL when filter contains identifier named date + + +Changes in 6.6.10.Final (March 09, 2025) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/32647 + +** Bug + * [HHH-19232] - BeanValidationEventListener not called if only associated collection is updated via getter + * [HHH-19206] - Bytecode-enhanced dirty checking ineffective if entity's embedded ID set manually (to same value) + * [HHH-19195] - Embeddable inheritance: discriminator values are not hierarchically ordered + +** Improvement + * [HHH-19219] - Informix Catalog and schema support + + +Changes in 6.6.9.Final (February 23, 2025) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/32614 + +** Bug + * [HHH-19173] - PostgreSQLLegacySqlAstTranslator does not implement visitInArrayPredicates + * [HHH-19116] - Error when using fk() function on left joined many-to-one association and is null predicate + * [HHH-19110] - Flush operation fails with "UnsupportedOperationException: compare() not implemented for EntityType" + * [HHH-17151] - NPE when binding null parameter in native query with explicit TemporalType + + +Changes in 6.6.8.Final (February 16, 2025) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/32516 + +** Bug + * [HHH-19126] - Plural valued paths should be collection-typed instead of element typed + * [HHH-18988] - Embeddable inheritance + default_schema results in NPE at startup + +** Improvement + * [HHH-19098] - Disable implicit loading of the default import script + + +Changes in 6.6.7.Final (February 10, 2025) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/32416 + +** Bug + * [HHH-19104] - Envers is keeping references to classes and thus classloaders + * [HHH-18901] - AnnotationFormatError: Duplicate annotation for class: interface org.hibernate.bytecode.enhance.spi.EnhancementInfo + * [HHH-18069] - NullPointerException when unioning partition results + +** Task + * [HHH-19050] - Allow configuration of EntityManagerFactoryBuilderImpl to override the BytecodeProvider instance + * [HHH-18928] - Consider the default Access Type as per Spec section 2.3.1 and skip enhancement of properties accessor + + +Changes in 6.6.6.Final (February 02, 2025) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/32382 + +** Bug + * [HHH-19079] - ComponentType.replace can cause ArrayIndexOutOfBoundsException when used with embeddable inheritance + * [HHH-19069] - Performance regression for wide inheritance models + * [HHH-19034] - Wrong reuse of a Join + * [HHH-18961] - JtaIsolationDelegate, obtaining connection : NPE when SQLExceptionConversionDelegate#convert returns null + * [HHH-18933] - the ordering of the class declaration in persistence.xml seems to affect the metamodel + +** Task + * [HHH-19078] - Improve release process error message when no issues with corresponding version are found + + +Changes in 6.6.5.Final (January 19, 2025) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/32379 + +** Bug + * [HHH-19011] - @ElementCollection comment overrides class level comment on an Entity + * [HHH-18819] - Error resolving persistent property of @MapperSuperclass if subtype @Embeddable used as @IdClass + * [HHH-17652] - Cannot invoke "org.hibernate.envers.internal.entities.EntityConfiguration.getRelationDescription(String)" because "entCfg" is null + +** Task + * [HHH-18972] - Upgrade to ByteBuddy 1.15.11 + + +Changes in 6.6.4.Final (December 18, 2024) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/32370 + +** Bug + * [HHH-18949] - Hibernate Processor should not insert underscores within uppercase names + * [HHH-18932] - Wrongly using FK column instead of PK when using joined alias + * [HHH-18912] - ORM release process + * [HHH-18904] - Bytecode Enhancement fails with UnsupportedEnhancementStrategy.FAIL for pre-persist method + * [HHH-18903] - Bytecode enhancement fails for entities that contain a method named get + * [HHH-18872] - ConcreteProxy type not restored from 2LC when loading a ManyToOne + * [HHH-18868] - Wrong behaviour of getAttribute method in impl. of ManagedType when scattered id attributes are used in MappedSuperclass + * [HHH-18863] - jpamodelgen 6.6 performance issue in Eclipse IDE + * [HHH-18850] - createCountQuery with Hibernate 6.6.2 + * [HHH-18709] - CriteriaUpdate involving JSON field containing Map results in SemanticException + * [HHH-18689] - 'FULL' query cache sometimes incomplete + * [HHH-18629] - Inconsistent column alias generated while result class is used for placeholder + * [HHH-18583] - Joined + discriminator inheritance treat in where clause not restricting to subtype + * [HHH-18384] - @JoinColumnsOrFormulas broken + * [HHH-18274] - Problems with generics in queries; proposed partial solution + * [HHH-17838] - @OneToOne relationship + @Embeddable keys + FetchType.LAZY fail in most recent version + * [HHH-17612] - DefaultRevisionEntity: Illegal argument on static metamodel field injection + * [HHH-14725] - Using a InputStream with BlobProxy and Envers results in java.sql.SQLException: could not reset reader + * [HHH-13815] - TransientObjectException after merging a bidirectional one-to-many with orphan deletion + * [HHH-13790] - Temporary session not being closed + * [HHH-13377] - Lazy loaded properties of bytecode enhanced entity are left stale after refresh of entity + +** Sub-task + * [HHH-18369] - Support Informix matches() function + * [HHH-18367] - Informix sum on case expression error + * [HHH-18365] - Informix function bit_length() error + * [HHH-18364] - Informix function locate() error + * [HHH-18363] - Informix component nullness check error + * [HHH-18362] - Informix function substring() error + * [HHH-18361] - Informix current_time error + * [HHH-18360] - Informix function str() error + +** Task + * [HHH-18917] - Follow all of the JavaBeans rules in enhance/internal/bytebuddy/EnhancerImpl when checking if a class can be enhanced + * [HHH-18906] - Allow specifying UnsupportedEnhancementStrategy for Hibernate testing + + +Changes in 6.6.3.Final (November 21, 2024) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/32365 + +** Bug + * [HHH-18862] - Group by error due to subselect using foreign key reference instead of primary key in HQL query + * [HHH-18851] - ArrayContainsArgumentTypeResolver wrongly infers array type for needle argument + * [HHH-18842] - Regression: CollectionType.replace() breaks if target is PersistentCollection, but not instance of Collection (e.g. PersistentMap) + * [HHH-18832] - Bytecode enhancement skipped for entities with "compute-only" @Transient properties + * [HHH-18816] - Error when rendering the fk-side of an association in an exists subquery + * [HHH-18703] - JoinedSubclassEntityPersister#getTableNameForColumn KO + * [HHH-18647] - SemanticException when using createCriteriaInsertValues to insert into foreign key column + +** Improvement + * [HHH-18841] - Make `_identifierMapper` property added for a IdClass synthetic + * [HHH-18833] - Configuration to fail bytecode enhancement instead of skipping it on unsupported models + +** Task + * [HHH-18846] - Enable release automation for ORM 6.6 + + +Changes in 6.6.2.Final (November 07, 2024) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/32350 + +** Bug + * [HHH-18773] - Multiple selections of same alias triggers possible non-threadsafe access to the session + * [HHH-18770] - NPE when using the JFR integration with JFR disabled + * [HHH-18764] - Class cast exception when using non basic type as identifier and in an embedded field using a natural ID + * [HHH-18761] - named query method generation for @NamedQuery on entity + * [HHH-18739] - Do not support join queries when using Mysql + * [HHH-18730] - Multi-column association in aggregate component doesn't work + * [HHH-18720] - Type check on select columns in union all gives SemanticException when there is a null column + * [HHH-18719] - Previous row state reuse can provide detached entities to the consumer + * [HHH-18713] - saveOrUpdate changed behaviour with bytecode enhancer + * [HHH-18712] - Warning about attempts to update an immutable entity for normal (not immutable) entity + * [HHH-18702] - Exception using @EmbeddedId with @OneToMany that refers to an alternate key column + * [HHH-18699] - Correctly handle @Id and @Version fields in query validation in Hibernate Processor + * [HHH-18697] - JPA 3.2 spec compliance for uppercasing of names in Hibernate Processor + * [HHH-18696] - @Find method for single @NaturalId field + * [HHH-18692] - Hibernate attempts to close batched statements multiple times + * [HHH-18681] - InterpretationException executing subquery in case-when : o.h.query.sqm.tree.select.SqmSelection.getExpressible() is null + * [HHH-18675] - Self-referencing many-to-many relation on generic entity gives NullPointerException in mapping + * [HHH-18669] - NullPointerException in the AgroalConnectionProvider + * [HHH-18667] - Annotation processor leaks - OOME when used in Eclipse IDE + * [HHH-18658] - Inner join prevents finding an entity instance referencing an empty map + * [HHH-18645] - AssertionError in AbstractBatchEntitySelectFetchInitializer#registerToBatchFetchQueue + * [HHH-18642] - DB2: select from new table with identity column not working when missing read permission + * [HHH-18635] - Avoid using `bigdatetime` column type on Sybase jconn when not necessary + * [HHH-18632] - Concurrency issue with AbstractEntityPersister#nonLazyPropertyLoadPlansByName + * [HHH-18631] - AssertionError when loading an entity after removing another, associated entity + * [HHH-18628] - Regression: Unable to determine TableReference + * [HHH-18617] - Fetching unowned side of bidirectional OneToOne mappings including tenant identifier triggers EntityFilteredException + * [HHH-18614] - TransientObjectException: session.update() does not save new entities in OneToMany relation when using bytecode enhancement + * [HHH-18608] - NPE in EntityInitializerImpl.resolveInstanceSubInitializers + * [HHH-18596] - ValueHandlingMode hack in query pagination + * [HHH-18582] - Mapping array of arrays with @JdbcTypeCode(SqlTypes.ARRAY) causes NPE + * [HHH-18575] - IN predicate with numeric/decimal parameter types leads to Binding is multi-valued; illegal call to #getBindValue + * [HHH-18564] - Literal expressions using AttributeConverters stopped working in hibernate 6 + * [HHH-18551] - Memory leak caused by AbstractArrayJavaType#getRecommendedJdbcType + * [HHH-18515] - Unrecognized discriminator value exception when running native query on entity with discriminator column + * [HHH-18513] - Session Metrics for pre-partial-flushes are wrong + * [HHH-18500] - Gradle plugin crashes on module-info when extended enhancement is set + * [HHH-18494] - UnknownTableReferenceException in native query with placeholder when entity contains a to-one with a join table + * [HHH-18491] - Resuming null transaction in JtaIsolationDelegate + * [HHH-18471] - Since 6.2.2 Dialect SybaseAnywhereDialect does not render table names in the selection query + * [HHH-18450] - Inconsistent "SELECT 1" versus "SELECT ?1" with result type Object[] + * [HHH-18409] - In 6.5.2.Final, byte[] instance variables annotated with @NaturalId cannot be found with a natural ID query (regression from Hibernate 5.6.15.Final) + * [HHH-18389] - `merge()` with `orphanRemoval = true` leads to "HibernateException: A collection with cascade="all-delete-orphan" was no longer referenced by the owning entity instance" + * [HHH-18321] - SqlTreeCreationException when using sum/diff with coalesce on properties of embeddable component + * [HHH-18131] - Composite identifiers with associations stopped working with @IdClass + * [HHH-17739] - unhelpful CCE for field with unsupported collection type + * [HHH-16572] - Skip enhancement for PROPERTY attributes with mismatched field and method names + +** Improvement + * [HHH-18698] - respect @Nonnull annotation applied to parameters of @Find method + * [HHH-18654] - Change setting docs to use an asciidoc section per setting (User Guide) + * [HHH-18640] - Add note to migration guide about @Table and subclasses + + +Changes in 6.6.1.Final (September 17, 2024) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/32340 + +** Bug + * [HHH-18571] - Entities and collections with batch size 1 are treated as batchable + * [HHH-18565] - Bytecode enhancement, assertion error on reloading *toOne entities + * [HHH-18560] - DB2iDialect executes incompatible query in combination with @AuditJoinTable mapping + * [HHH-18558] - Informix UUID type support + * [HHH-18556] - Expressions.nullExpresion() in querydsl result in NPE in SqmExpressible + * [HHH-18531] - Informix timestamp literal error + * [HHH-18524] - @IdClass and composite key generation not working with meta-annotated @IdGenerationType + * [HHH-18518] - MySQLDialect fails to detect version on azure flexible server + * [HHH-18511] - ArrayIndexOutOfBoundsException in ImmutableFetchList + * [HHH-18506] - Flush performance degradation due to itable stubs + * [HHH-18503] - ID select criteria query fails with joined + discriminator inheritance when entity has subtypes + * [HHH-18502] - Type conversion error due to surprising javac method selection in SqmSubQuery#in + * [HHH-18493] - Resolving already initialized collection elements leads to assertion error + * [HHH-18490] - Static metamodel contains wrong type for Embeddable with generic attribute extending from mapped superclass + * [HHH-18489] - Lazy, unowned one-to-one associations get loaded eagerly in queries - even with bytecode enhancement + * [HHH-18487] - Error: Detached entity wiht id X has an uninitialized version value '1'' + * [HHH-18486] - Unable to add Persist the object because of EntityNameResolver being ignored + * [HHH-18484] - Update fails when a many-to-one property on an entity with a generated value is changed + * [HHH-18480] - ClassCastException when updating a Blob with Oracle + * [HHH-18478] - Cannot invoke "org.hibernate.persister.entity.EntityPersister.getSubclassId()" because "data.concreteDescriptor" is null when reloading entity from query cache + * [HHH-18476] - Fetchable scrolling with KeyToOne leads to assertion error + * [HHH-18472] - Fix memory issues for release job + * [HHH-18470] - Duplicate foreign key names generated for inheritance type TABLE_PER_CLASS + * [HHH-18469] - Assertion Error when fetch joining complex entities structure + * [HHH-18466] - Regression from 5.6.15.Final where a mutable natural IDs query cannot find an entity + * [HHH-18447] - cast as boolean broken in Hibernate 6 + * [HHH-18445] - Embeddable as Java Record has wrong order of columns + * [HHH-18439] - NullPointerException when access data from query cache + * [HHH-18436] - Wrong order-by rendering order for nested collections if loaded with entity graph + * [HHH-18414] - Duplicated filter applied to 'find()' method + * [HHH-18400] - AttributeConverter causing type-check failure when comparing Date field to parameter + * [HHH-18353] - ArrayConstructorFunction comparing argument types by reference causes FunctionArgumentException + * [HHH-18337] - SequenceStyleGenerator not respecting physical naming strategy + * [HHH-18282] - generate error sql in case of @DiscriminatorValue("not null") and the entity is superclass of other entity + * [HHH-18174] - Hibernate polymorphic join in complex subquery not using discriminator + * [HHH-18103] - MappedSuperclass handling for Embeddable is broken in metamodel + +** Improvement + * [HHH-18625] - Add Configurable#create(GeneratorCreationContext) + * [HHH-18507] - allow overwriting the default db image with environment variable in docker_db.sh + * [HHH-18459] - Add SingleStore community dialect + * [HHH-17646] - JOINED @Inheritance leads to bad performance in Hibernate 6 + +** Task + * [HHH-18612] - Avoid writing files to classpath in tests to avoid spurious failures + + Changes in 6.6.0.Final (August 08, 2024) ------------------------------------------------------------------------------------------------------------------------ diff --git a/ci/jpa-3.1-tck.Jenkinsfile b/ci/jpa-3.1-tck.Jenkinsfile index 87b18513bb00..f7f9f0e81538 100644 --- a/ci/jpa-3.1-tck.Jenkinsfile +++ b/ci/jpa-3.1-tck.Jenkinsfile @@ -6,106 +6,108 @@ if (currentBuild.getBuildCauses().toString().contains('BranchIndexingCause')) { currentBuild.result = 'NOT_BUILT' return } -def throttleCount -// Don't build the TCK on PRs, unless they use the tck label -if ( env.CHANGE_ID != null ) { - if ( !pullRequest.labels.contains( 'tck' ) ) { - print "INFO: Build skipped because pull request doesn't have 'tck' label" - return - } - throttleCount = 20 -} -else { - throttleCount = 1 +// This is a limited maintenance branch, so don't run this on pushes to the branch, only on PRs +if ( !env.CHANGE_ID ) { + print "INFO: Build skipped because this job should only run for pull request, not for branch pushes" + currentBuild.result = 'NOT_BUILT' + return } pipeline { - agent { - label 'LongDuration' - } + agent none tools { jdk 'OpenJDK 11 Latest' } options { - rateLimitBuilds(throttle: [count: throttleCount, durationName: 'day', userBoost: true]) buildDiscarder(logRotator(numToKeepStr: '3', artifactNumToKeepStr: '3')) disableConcurrentBuilds(abortPrevious: true) } parameters { choice(name: 'IMAGE_JDK', choices: ['jdk11'], description: 'The JDK base image version to use for the TCK image.') - string(name: 'TCK_VERSION', defaultValue: '3.1.5', description: 'The version of the Jakarta JPA TCK i.e. `2.2.0` or `3.0.1`') - string(name: 'TCK_SHA', defaultValue: '01072e6bdf56f0f8818290b8819f492ac95bb83fab14070d36aa7158a4f5eeed', description: 'The SHA256 of the Jakarta JPA TCK that is distributed under https://download.eclipse.org/jakartaee/persistence/3.1/jakarta-persistence-tck-${TCK_VERSION}.zip.sha256') + string(name: 'TCK_VERSION', defaultValue: '3.1.6', description: 'The version of the Jakarta JPA TCK i.e. `2.2.0` or `3.0.1`') + string(name: 'TCK_SHA', defaultValue: '790ca7a2a95ea098cfedafa2689c0d7a379fa62c74fed9505dd23191292f59fe', description: 'The SHA256 of the Jakarta JPA TCK that is distributed under https://download.eclipse.org/jakartaee/persistence/3.1/jakarta-persistence-tck-${TCK_VERSION}.zip.sha256') string(name: 'TCK_URL', defaultValue: '', description: 'The URL from which to download the TCK ZIP file. Only needed for testing staged builds. Ensure the TCK_VERSION variable matches the ZIP file name suffix.') booleanParam(name: 'NO_SLEEP', defaultValue: true, description: 'Whether the NO_SLEEP patch should be applied to speed up the TCK execution') } stages { - stage('Build') { + stage('Checks') { steps { - script { - docker.withRegistry('https://index.docker.io/v1/', 'hibernateci.hub.docker.com') { - docker.image('openjdk:11-jdk').pull() - } - } - dir('hibernate') { - checkout scm - sh './gradlew publishToMavenLocal -PmavenMirror=nexus-load-balancer-c4cf05fd92f43ef8.elb.us-east-1.amazonaws.com -DjakartaJpaVersion=3.1.0' - script { - env.HIBERNATE_VERSION = sh ( - script: "grep hibernateVersion gradle/version.properties|cut -d'=' -f2", - returnStdout: true - ).trim() - } - } - dir('tck') { - checkout changelog: false, poll: false, scm: [$class: 'GitSCM', branches: [[name: '*/main']], extensions: [], userRemoteConfigs: [[url: 'https://github.com/hibernate/jakarta-tck-runner.git']]] - script { - if ( params.TCK_URL == null || params.TCK_URL.isEmpty() ) { - sh "cd jpa-3.1; docker build -f Dockerfile.${params.IMAGE_JDK} -t jakarta-tck-runner --build-arg TCK_VERSION=${params.TCK_VERSION} --build-arg TCK_SHA=${params.TCK_SHA} ." - } - else { - sh "cd jpa-3.1; docker build -f Dockerfile.${params.IMAGE_JDK} -t jakarta-tck-runner --build-arg TCK_VERSION=${params.TCK_VERSION} --build-arg TCK_SHA=${params.TCK_SHA} --build-arg TCK_URL=${params.TCK_URL} ." - } - } - } - } - } - stage('Run TCK') { - steps { - sh """ \ - rm -Rf ./results - docker rm -f tck || true - docker volume rm -f tck-vol || true - docker volume create tck-vol - docker run -v ~/.m2/repository/org/hibernate:/root/.m2/repository/org/hibernate:z -v tck-vol:/tck/persistence-tck/tmp/:z -e NO_SLEEP=${params.NO_SLEEP} -e HIBERNATE_VERSION=$HIBERNATE_VERSION --name tck jakarta-tck-runner - docker cp tck:/tck/persistence-tck/tmp/ ./results - """ - archiveArtifacts artifacts: 'results/**' - script { - failures = sh ( - script: """ \ - set +x - while read line; do - if [[ "\$line" != *"Passed." ]]; then - echo "\$line" - fi - done .*@${env.HIBERNATE_VERSION}@' pom.xml", returnStatus: true) + if ( sedStatus != 0 ) { + throw new IllegalArgumentException( "Unable to replace hibernate version in Quarkus pom. Got exit code $sedStatus" ) + } + } + // Need to override the default maven configuration this way, because there is no other way to do it + sh "sed -i 's/-Xmx5g/-Xmx2048m/' ./.mvn/jvm.config" + sh "echo -e '\\n-XX:MaxMetaspaceSize=1024m'>>./.mvn/jvm.config" + withMaven(mavenLocalRepo: env.WORKSPACE + '/.m2repository', publisherStrategy:'EXPLICIT') { + sh "./mvnw -pl !docs -Dquickly install" + // Need to kill the gradle daemons started during the Maven install run + sh "sudo pkill -f '.*GradleDaemon.*' || true" + // Need to override the default maven configuration this way, because there is no other way to do it + sh "sed -i 's/-Xmx2048m/-Xmx1340m/' ./.mvn/jvm.config" + sh "sed -i 's/MaxMetaspaceSize=1024m/MaxMetaspaceSize=512m/' ./.mvn/jvm.config" + def excludes = "'!integration-tests/kafka-oauth-keycloak,!integration-tests/kafka-sasl-elytron,!integration-tests/hibernate-search-orm-opensearch,!integration-tests/maven,!integration-tests/quartz,!integration-tests/reactive-messaging-kafka,!integration-tests/resteasy-reactive-kotlin/standard,!integration-tests/opentelemetry-reactive-messaging,!integration-tests/virtual-threads/kafka-virtual-threads,!integration-tests/smallrye-jwt-oidc-webapp,!extensions/oidc-db-token-state-manager/deployment,!docs'" + sh "TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED=true ./mvnw -Dinsecure.repositories=WARN -pl :quarkus-hibernate-orm -amd -pl ${excludes} verify -Dstart-containers -Dtest-containers -Dskip.gradle.build" + } + } + } + } + } + } + post { + always { + notifyBuildResult maintainers: "andrea@hibernate.org steve@hibernate.org christian.beikov@gmail.com mbellade@redhat.com" + } + } +} \ No newline at end of file diff --git a/ci/release/Jenkinsfile b/ci/release/Jenkinsfile index 0304265957d2..0e57fec97c02 100644 --- a/ci/release/Jenkinsfile +++ b/ci/release/Jenkinsfile @@ -17,11 +17,10 @@ import org.hibernate.jenkins.pipeline.helpers.version.Version // Global build configuration env.PROJECT = "orm" env.JIRA_KEY = "HHH" -def RELEASE_ON_PUSH = false // Set to `true` *only* on branches where you want a release on each push. +def RELEASE_ON_SCHEDULE = true // Set to `true` *only* on branches where you want a scheduled release. print "INFO: env.PROJECT = ${env.PROJECT}" print "INFO: env.JIRA_KEY = ${env.JIRA_KEY}" -print "INFO: RELEASE_ON_PUSH = ${RELEASE_ON_PUSH}" // -------------------------------------------- // Build conditions @@ -34,10 +33,17 @@ if (currentBuild.getBuildCauses().toString().contains('BranchIndexingCause')) { } def manualRelease = currentBuild.getBuildCauses().toString().contains( 'UserIdCause' ) +def cronRelease = currentBuild.getBuildCauses().toString().contains( 'TimerTriggerCause' ) // Only do automatic release on branches where we opted in -if ( !manualRelease && !RELEASE_ON_PUSH ) { - print "INFO: Build skipped because automated releases are disabled on this branch. See constant RELEASE_ON_PUSH in ci/release/Jenkinsfile" +if ( !manualRelease && !cronRelease ) { + print "INFO: Build skipped because automated releases on push are disabled on this branch." + currentBuild.result = 'NOT_BUILT' + return +} + +if ( !manualRelease && cronRelease && !RELEASE_ON_SCHEDULE ) { + print "INFO: Build skipped because automated releases are disabled on this branch. See constant RELEASE_ON_SCHEDULE in ci/release/Jenkinsfile" currentBuild.result = 'NOT_BUILT' return } @@ -58,14 +64,17 @@ def checkoutReleaseScripts() { pipeline { agent { - label 'Worker&&Containers' + label 'Release' + } + triggers { + // Run every week Sunday midnight + cron('0 0 * * 0') } tools { jdk 'OpenJDK 11 Latest' } options { buildDiscarder logRotator(daysToKeepStr: '30', numToKeepStr: '10') - rateLimitBuilds(throttle: [count: 1, durationName: 'day', userBoost: true]) disableConcurrentBuilds(abortPrevious: false) preserveStashes() } @@ -92,6 +101,10 @@ pipeline { stage('Release check') { steps { script { + print "INFO: params.RELEASE_VERSION = ${params.RELEASE_VERSION}" + print "INFO: params.DEVELOPMENT_VERSION = ${params.DEVELOPMENT_VERSION}" + print "INFO: params.RELEASE_DRY_RUN? = ${params.RELEASE_DRY_RUN}" + checkoutReleaseScripts() def currentVersion = Version.parseDevelopmentVersion( sh( @@ -103,11 +116,20 @@ pipeline { def releaseVersion def developmentVersion + def lastCommitter = sh(script: 'git show -s --format=\'%an\'', returnStdout: true).trim() + def secondLastCommitter = sh(script: 'git show -s --format=\'%an\' HEAD~1', returnStdout: true).trim() + def isCiLastCommiter = lastCommitter == 'Hibernate-CI' && secondLastCommitter == 'Hibernate-CI' + + echo "Last two commits were performed by '${lastCommitter}'/'${secondLastCommitter}'." + echo "Is 'Hibernate-CI' the last commiter: '${isCiLastCommiter}'." + if ( manualRelease ) { echo "Release was requested manually" if ( !params.RELEASE_VERSION ) { - throw new IllegalArgumentException( 'Missing value for parameter RELEASE_VERSION. This parameter must be set explicitly to prevent mistakes.' ) + throw new IllegalArgumentException( + 'Missing value for parameter RELEASE_VERSION. This parameter must be set explicitly to prevent mistakes.' + ) } releaseVersion = Version.parseReleaseVersion( params.RELEASE_VERSION ) @@ -119,11 +141,11 @@ pipeline { echo "Release was triggered automatically" // Avoid doing an automatic release for commits from a release - def lastCommitter = sh(script: 'git show -s --format=\'%an\'', returnStdout: true) - def secondLastCommitter = sh(script: 'git show -s --format=\'%an\' HEAD~1', returnStdout: true) - if (lastCommitter == 'Hibernate-CI' && secondLastCommitter == 'Hibernate-CI') { + + if (isCiLastCommiter) { print "INFO: Automatic release skipped because last commits were for the previous release" - currentBuild.result = 'ABORTED' + currentBuild.getRawBuild().getExecutor().interrupt(Result.NOT_BUILT) + sleep(1) // Interrupt is not blocking and does not take effect immediately. return } @@ -148,6 +170,7 @@ pipeline { env.RELEASE_VERSION = releaseVersion.toString() env.DEVELOPMENT_VERSION = developmentVersion.toString() env.SCRIPT_OPTIONS = params.RELEASE_DRY_RUN ? "-d" : "" + env.JRELEASER_DRY_RUN = params.RELEASE_DRY_RUN // Determine version id to check if Jira version exists sh ".release/scripts/determine-jira-version-id.sh ${env.JIRA_KEY} ${releaseVersion.withoutFinalQualifier}" @@ -163,24 +186,17 @@ pipeline { configFile(fileId: 'release.config.ssh', targetLocation: "${env.HOME}/.ssh/config"), configFile(fileId: 'release.config.ssh.knownhosts', targetLocation: "${env.HOME}/.ssh/known_hosts") ]) { - withCredentials([ - usernamePassword(credentialsId: 'ossrh.sonatype.org', passwordVariable: 'OSSRH_PASSWORD', usernameVariable: 'OSSRH_USER'), - usernamePassword(credentialsId: 'gradle-plugin-portal-api-key', passwordVariable: 'PLUGIN_PORTAL_PASSWORD', usernameVariable: 'PLUGIN_PORTAL_USERNAME'), - file(credentialsId: 'release.gpg.private-key', variable: 'RELEASE_GPG_PRIVATE_KEY_PATH'), - string(credentialsId: 'release.gpg.passphrase', variable: 'RELEASE_GPG_PASSPHRASE') - ]) { - sshagent(['ed25519.Hibernate-CI.github.com', 'hibernate.filemgmt.jboss.org', 'hibernate-ci.frs.sourceforge.net']) { - // set release version - // update changelog from JIRA - // tags the version - // changes the version to the provided development version - withEnv([ - "BRANCH=${env.GIT_BRANCH}", - // Increase the amount of memory for this part since asciidoctor doc rendering consumes a lot of metaspace - "GRADLE_OPTS=-Dorg.gradle.jvmargs='-Dlog4j2.disableJmx -Xmx4g -XX:MaxMetaspaceSize=768m -XX:+HeapDumpOnOutOfMemoryError -Duser.language=en -Duser.country=US -Duser.timezone=UTC -Dfile.encoding=UTF-8'" - ]) { - sh ".release/scripts/prepare-release.sh ${env.PROJECT} ${env.RELEASE_VERSION} ${env.DEVELOPMENT_VERSION}" - } + sshagent(['ed25519.Hibernate-CI.github.com', 'hibernate.filemgmt.jboss.org', 'hibernate-ci.frs.sourceforge.net']) { + // set release version + // update changelog from JIRA + // tags the version + // changes the version to the provided development version + withEnv([ + "DISABLE_REMOTE_GRADLE_CACHE=true", + // Increase the amount of memory for this part since asciidoctor doc rendering consumes a lot of metaspace + "GRADLE_OPTS=-Dorg.gradle.jvmargs='-Dlog4j2.disableJmx -Xmx4g -XX:MaxMetaspaceSize=768m -XX:+HeapDumpOnOutOfMemoryError -Duser.language=en -Duser.country=US -Duser.timezone=UTC -Dfile.encoding=UTF-8'" + ]) { + sh ".release/scripts/prepare-release.sh -j -b ${env.GIT_BRANCH} -v ${env.DEVELOPMENT_VERSION} ${env.PROJECT} ${env.RELEASE_VERSION}" } } } @@ -197,16 +213,30 @@ pipeline { configFile(fileId: 'release.config.ssh.knownhosts', targetLocation: "${env.HOME}/.ssh/known_hosts") ]) { withCredentials([ - usernamePassword(credentialsId: 'ossrh.sonatype.org', passwordVariable: 'OSSRH_PASSWORD', usernameVariable: 'OSSRH_USER'), - usernamePassword(credentialsId: 'gradle-plugin-portal-api-key', passwordVariable: 'PLUGIN_PORTAL_PASSWORD', usernameVariable: 'PLUGIN_PORTAL_USERNAME'), + // https://github.com/gradle-nexus/publish-plugin#publishing-to-maven-central-via-sonatype-ossrh + // TODO: HHH-19309: + // Once we switch to maven-central publishing (from nexus2) we need to add a new credentials + // to use the following env variable names to set the user/password: + // - JRELEASER_MAVENCENTRAL_USERNAME + // - JRELEASER_MAVENCENTRAL_TOKEN + // Also use the new `credentialsId` for Maven Central, e.g.: + // usernamePassword(credentialsId: '???????', passwordVariable: 'JRELEASER_MAVENCENTRAL_TOKEN', usernameVariable: 'JRELEASER_MAVENCENTRAL_USERNAME'), + usernamePassword(credentialsId: 'ossrh.sonatype.org', passwordVariable: 'JRELEASER_NEXUS2_PASSWORD', usernameVariable: 'JRELEASER_NEXUS2_USERNAME'), + // https://docs.gradle.org/current/userguide/publishing_gradle_plugins.html#account_setup + usernamePassword(credentialsId: 'gradle-plugin-portal-api-key', passwordVariable: 'GRADLE_PUBLISH_SECRET', usernameVariable: 'GRADLE_PUBLISH_KEY'), + gitUsernamePassword(credentialsId: 'username-and-token.Hibernate-CI.github.com', gitToolName: 'Default'), file(credentialsId: 'release.gpg.private-key', variable: 'RELEASE_GPG_PRIVATE_KEY_PATH'), - string(credentialsId: 'release.gpg.passphrase', variable: 'RELEASE_GPG_PASSPHRASE'), - gitUsernamePassword(credentialsId: 'username-and-token.Hibernate-CI.github.com', gitToolName: 'Default') + string(credentialsId: 'release.gpg.passphrase', variable: 'JRELEASER_GPG_PASSPHRASE'), + string(credentialsId: 'Hibernate-CI.github.com', variable: 'JRELEASER_GITHUB_TOKEN') ]) { sshagent(['ed25519.Hibernate-CI.github.com', 'hibernate.filemgmt.jboss.org', 'hibernate-ci.frs.sourceforge.net']) { // performs documentation upload and Sonatype release // push to github - sh ".release/scripts/publish.sh ${env.SCRIPT_OPTIONS} ${env.PROJECT} ${env.RELEASE_VERSION} ${env.DEVELOPMENT_VERSION} ${env.GIT_BRANCH}" + withEnv([ + "DISABLE_REMOTE_GRADLE_CACHE=true" + ]) { + sh ".release/scripts/publish.sh -j ${env.SCRIPT_OPTIONS} ${env.PROJECT} ${env.RELEASE_VERSION} ${env.DEVELOPMENT_VERSION} ${env.GIT_BRANCH}" + } } } } @@ -258,4 +288,4 @@ pipeline { } } } -} \ No newline at end of file +} diff --git a/ci/snapshot-publish.Jenkinsfile b/ci/snapshot-publish.Jenkinsfile index 24cd67d92f1b..712111e81e71 100644 --- a/ci/snapshot-publish.Jenkinsfile +++ b/ci/snapshot-publish.Jenkinsfile @@ -10,9 +10,17 @@ if (currentBuild.getBuildCauses().toString().contains('BranchIndexingCause')) { return } +def checkoutReleaseScripts() { + dir('.release/scripts') { + checkout scmGit(branches: [[name: '*/main']], extensions: [], + userRemoteConfigs: [[credentialsId: 'ed25519.Hibernate-CI.github.com', + url: 'https://github.com/hibernate/hibernate-release-scripts.git']]) + } +} + pipeline { agent { - label 'Fedora' + label 'Release' } tools { jdk 'OpenJDK 11 Latest' @@ -30,23 +38,31 @@ pipeline { } stage('Publish') { steps { - withCredentials([ - usernamePassword(credentialsId: 'ossrh.sonatype.org', usernameVariable: 'hibernatePublishUsername', passwordVariable: 'hibernatePublishPassword'), - usernamePassword(credentialsId: 'plugins.gradle.org', usernameVariable: 'hibernatePluginPortalUsername', passwordVariable: 'hibernatePluginPortalPassword'), - string(credentialsId: 'ge.hibernate.org-access-key', variable: 'DEVELOCITY_ACCESS_KEY'), - string(credentialsId: 'release.gpg.passphrase', variable: 'SIGNING_PASS'), - file(credentialsId: 'release.gpg.private-key', variable: 'SIGNING_KEYRING') - ]) { - sh '''./gradlew clean publish \ - -PhibernatePublishUsername=$hibernatePublishUsername \ - -PhibernatePublishPassword=$hibernatePublishPassword \ - -Pgradle.publish.key=$hibernatePluginPortalUsername \ - -Pgradle.publish.secret=$hibernatePluginPortalPassword \ - --no-scan \ - -DsigningPassword=$SIGNING_PASS \ - -DsigningKeyFile=$SIGNING_KEYRING \ - ''' - } + script { + withCredentials([ + // https://github.com/gradle-nexus/publish-plugin#publishing-to-maven-central-via-sonatype-ossrh + // TODO: HHH-19309: + // Once we switch to maven-central publishing (from nexus2) we need to update credentialsId: + // https://docs.gradle.org/current/samples/sample_publishing_credentials.html#:~:text=via%20environment%20variables + usernamePassword(credentialsId: 'ossrh.sonatype.org', passwordVariable: 'ORG_GRADLE_PROJECT_snapshotsPassword', usernameVariable: 'ORG_GRADLE_PROJECT_snapshotsUsername'), + string(credentialsId: 'Hibernate-CI.github.com', variable: 'JRELEASER_GITHUB_TOKEN'), + // https://docs.gradle.org/current/userguide/publishing_gradle_plugins.html#account_setup + usernamePassword(credentialsId: 'gradle-plugin-portal-api-key', passwordVariable: 'GRADLE_PUBLISH_SECRET', usernameVariable: 'GRADLE_PUBLISH_KEY'), + gitUsernamePassword(credentialsId: 'username-and-token.Hibernate-CI.github.com', gitToolName: 'Default') + ]) { + withEnv([ + "DISABLE_REMOTE_GRADLE_CACHE=true" + ]) { + checkoutReleaseScripts() + def version = sh( + script: ".release/scripts/determine-current-version.sh orm", + returnStdout: true + ).trim() + echo "Current version: '${version}'" + sh "bash -xe .release/scripts/snapshot-deploy.sh orm ${version}" + } + } + } } } } @@ -57,4 +73,4 @@ pipeline { } } } -} \ No newline at end of file +} diff --git a/docker_db.sh b/docker_db.sh index 8cdbce7d9be3..85035e7f91b8 100755 --- a/docker_db.sh +++ b/docker_db.sh @@ -21,7 +21,7 @@ mysql() { mysql_8_0() { $CONTAINER_CLI rm -f mysql || true - $CONTAINER_CLI run --name mysql -e MYSQL_USER=hibernate_orm_test -e MYSQL_PASSWORD=hibernate_orm_test -e MYSQL_ROOT_PASSWORD=hibernate_orm_test -e MYSQL_DATABASE=hibernate_orm_test -e MYSQL_ROOT_PASSWORD=hibernate_orm_test -p3306:3306 -d docker.io/mysql:8.0.31 --character-set-server=utf8mb4 --collation-server=utf8mb4_0900_as_cs --skip-character-set-client-handshake --log-bin-trust-function-creators=1 --lower_case_table_names=2 + $CONTAINER_CLI run --name mysql -e MYSQL_USER=hibernate_orm_test -e MYSQL_PASSWORD=hibernate_orm_test -e MYSQL_ROOT_PASSWORD=hibernate_orm_test -e MYSQL_DATABASE=hibernate_orm_test -e MYSQL_ROOT_PASSWORD=hibernate_orm_test -p3306:3306 -d ${DB_IMAGE_MYSQL_8_0:-docker.io/mysql:8.0.31} --character-set-server=utf8mb4 --collation-server=utf8mb4_0900_as_cs --skip-character-set-client-handshake --log-bin-trust-function-creators=1 --lower_case_table_names=2 # Give the container some time to start OUTPUT= n=0 @@ -45,7 +45,7 @@ mysql_8_0() { mysql_8_1() { $CONTAINER_CLI rm -f mysql || true - $CONTAINER_CLI run --name mysql -e MYSQL_USER=hibernate_orm_test -e MYSQL_PASSWORD=hibernate_orm_test -e MYSQL_ROOT_PASSWORD=hibernate_orm_test -e MYSQL_DATABASE=hibernate_orm_test -e MYSQL_ROOT_PASSWORD=hibernate_orm_test -p3306:3306 -d docker.io/mysql:8.1.0 --character-set-server=utf8mb4 --collation-server=utf8mb4_0900_as_cs --skip-character-set-client-handshake --log-bin-trust-function-creators=1 --lower_case_table_names=2 + $CONTAINER_CLI run --name mysql -e MYSQL_USER=hibernate_orm_test -e MYSQL_PASSWORD=hibernate_orm_test -e MYSQL_ROOT_PASSWORD=hibernate_orm_test -e MYSQL_DATABASE=hibernate_orm_test -e MYSQL_ROOT_PASSWORD=hibernate_orm_test -p3306:3306 -d ${DB_IMAGE_MYSQL_8_1:-docker.io/mysql:8.1.0} --character-set-server=utf8mb4 --collation-server=utf8mb4_0900_as_cs --skip-character-set-client-handshake --log-bin-trust-function-creators=1 --lower_case_table_names=2 # Give the container some time to start OUTPUT= n=0 @@ -69,7 +69,7 @@ mysql_8_1() { mysql_8_2() { $CONTAINER_CLI rm -f mysql || true - $CONTAINER_CLI run --name mysql -e MYSQL_USER=hibernate_orm_test -e MYSQL_PASSWORD=hibernate_orm_test -e MYSQL_ROOT_PASSWORD=hibernate_orm_test -e MYSQL_DATABASE=hibernate_orm_test -e MYSQL_ROOT_PASSWORD=hibernate_orm_test -p3306:3306 -d docker.io/mysql:8.2.0 --character-set-server=utf8mb4 --collation-server=utf8mb4_0900_as_cs --skip-character-set-client-handshake --log-bin-trust-function-creators=1 --lower_case_table_names=2 + $CONTAINER_CLI run --name mysql -e MYSQL_USER=hibernate_orm_test -e MYSQL_PASSWORD=hibernate_orm_test -e MYSQL_ROOT_PASSWORD=hibernate_orm_test -e MYSQL_DATABASE=hibernate_orm_test -e MYSQL_ROOT_PASSWORD=hibernate_orm_test -p3306:3306 -d ${DB_IMAGE_MYSQL_8_2:-docker.io/mysql:8.2.0} --character-set-server=utf8mb4 --collation-server=utf8mb4_0900_as_cs --skip-character-set-client-handshake --log-bin-trust-function-creators=1 --lower_case_table_names=2 # Give the container some time to start OUTPUT= n=0 @@ -116,31 +116,31 @@ mariadb_wait_until_start() mariadb_10_4() { $CONTAINER_CLI rm -f mariadb || true - $CONTAINER_CLI run --name mariadb -e MARIADB_USER=hibernate_orm_test -e MARIADB_PASSWORD=hibernate_orm_test -e MARIADB_DATABASE=hibernate_orm_test -e MARIADB_ROOT_PASSWORD=hibernate_orm_test -p3306:3306 -d docker.io/mariadb:10.4.33 --character-set-server=utf8mb4 --collation-server=utf8mb4_bin --skip-character-set-client-handshake --lower_case_table_names=2 + $CONTAINER_CLI run --name mariadb -e MARIADB_USER=hibernate_orm_test -e MARIADB_PASSWORD=hibernate_orm_test -e MARIADB_DATABASE=hibernate_orm_test -e MARIADB_ROOT_PASSWORD=hibernate_orm_test -p3306:3306 -d ${DB_IMAGE_MARIADB_10_4:-docker.io/mariadb:10.4.33} --character-set-server=utf8mb4 --collation-server=utf8mb4_bin --skip-character-set-client-handshake --lower_case_table_names=2 mariadb_wait_until_start } mariadb_10_11() { $CONTAINER_CLI rm -f mariadb || true - $CONTAINER_CLI run --name mariadb -e MARIADB_USER=hibernate_orm_test -e MARIADB_PASSWORD=hibernate_orm_test -e MARIADB_DATABASE=hibernate_orm_test -e MARIADB_ROOT_PASSWORD=hibernate_orm_test -p3306:3306 -d docker.io/mariadb:10.11.8 --character-set-server=utf8mb4 --collation-server=utf8mb4_bin --skip-character-set-client-handshake --lower_case_table_names=2 + $CONTAINER_CLI run --name mariadb -e MARIADB_USER=hibernate_orm_test -e MARIADB_PASSWORD=hibernate_orm_test -e MARIADB_DATABASE=hibernate_orm_test -e MARIADB_ROOT_PASSWORD=hibernate_orm_test -p3306:3306 -d ${DB_IMAGE_MARIADB_10_11:-docker.io/mariadb:10.11.8} --character-set-server=utf8mb4 --collation-server=utf8mb4_bin --skip-character-set-client-handshake --lower_case_table_names=2 mariadb_wait_until_start } mariadb_11_1() { $CONTAINER_CLI rm -f mariadb || true - $CONTAINER_CLI run --name mariadb -e MARIADB_USER=hibernate_orm_test -e MARIADB_PASSWORD=hibernate_orm_test -e MARIADB_DATABASE=hibernate_orm_test -e MARIADB_ROOT_PASSWORD=hibernate_orm_test -p3306:3306 -d docker.io/mariadb:11.1.2 --character-set-server=utf8mb4 --collation-server=utf8mb4_bin --skip-character-set-client-handshake --lower_case_table_names=2 + $CONTAINER_CLI run --name mariadb -e MARIADB_USER=hibernate_orm_test -e MARIADB_PASSWORD=hibernate_orm_test -e MARIADB_DATABASE=hibernate_orm_test -e MARIADB_ROOT_PASSWORD=hibernate_orm_test -p3306:3306 -d ${DB_IMAGE_MARIADB_11_1:-docker.io/mariadb:11.1.2} --character-set-server=utf8mb4 --collation-server=utf8mb4_bin --skip-character-set-client-handshake --lower_case_table_names=2 mariadb_wait_until_start } mariadb_11_4() { $CONTAINER_CLI rm -f mariadb || true - $CONTAINER_CLI run --name mariadb -e MARIADB_USER=hibernate_orm_test -e MARIADB_PASSWORD=hibernate_orm_test -e MARIADB_DATABASE=hibernate_orm_test -e MARIADB_ROOT_PASSWORD=hibernate_orm_test -p3306:3306 -d docker.io/mariadb:11.4.2 --character-set-server=utf8mb4 --collation-server=utf8mb4_bin --skip-character-set-client-handshake --lower_case_table_names=2 + $CONTAINER_CLI run --name mariadb -e MARIADB_USER=hibernate_orm_test -e MARIADB_PASSWORD=hibernate_orm_test -e MARIADB_DATABASE=hibernate_orm_test -e MARIADB_ROOT_PASSWORD=hibernate_orm_test -p3306:3306 -d ${DB_IMAGE_MARIADB_11_4:-docker.io/mariadb:11.4.2} --character-set-server=utf8mb4 --collation-server=utf8mb4_bin --skip-character-set-client-handshake --lower_case_table_names=2 mariadb_wait_until_start } mariadb_verylatest() { $CONTAINER_CLI rm -f mariadb || true - $CONTAINER_CLI run --name mariadb -e MARIADB_USER=hibernate_orm_test -e MARIADB_PASSWORD=hibernate_orm_test -e MARIADB_DATABASE=hibernate_orm_test -e MARIADB_ROOT_PASSWORD=hibernate_orm_test -p3306:3306 -d quay.io/mariadb-foundation/mariadb-devel:verylatest --character-set-server=utf8mb4 --collation-server=utf8mb4_bin --skip-character-set-client-handshake --lower_case_table_names=2 + $CONTAINER_CLI run --name mariadb -e MARIADB_USER=hibernate_orm_test -e MARIADB_PASSWORD=hibernate_orm_test -e MARIADB_DATABASE=hibernate_orm_test -e MARIADB_ROOT_PASSWORD=hibernate_orm_test -p3306:3306 -d ${DB_IMAGE_MARIADB_VERYLATEST:-quay.io/mariadb-foundation/mariadb-devel:verylatest} --character-set-server=utf8mb4 --collation-server=utf8mb4_bin --skip-character-set-client-handshake --lower_case_table_names=2 mariadb_wait_until_start } @@ -150,32 +150,32 @@ postgresql() { postgresql_12() { $CONTAINER_CLI rm -f postgres || true - $CONTAINER_CLI run --name postgres -e POSTGRES_USER=hibernate_orm_test -e POSTGRES_PASSWORD=hibernate_orm_test -e POSTGRES_DB=hibernate_orm_test -p5432:5432 -d docker.io/postgis/postgis:12-3.4 + $CONTAINER_CLI run --name postgres -e POSTGRES_USER=hibernate_orm_test -e POSTGRES_PASSWORD=hibernate_orm_test -e POSTGRES_DB=hibernate_orm_test -p5432:5432 -d ${DB_IMAGE_POSTGRESQL_12:-docker.io/postgis/postgis:12-3.4} $CONTAINER_CLI exec postgres bash -c '/usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y && apt install -y postgresql-12-pgvector && psql -U hibernate_orm_test -d hibernate_orm_test -c "create extension vector;"' } postgresql_13() { $CONTAINER_CLI rm -f postgres || true - $CONTAINER_CLI run --name postgres -e POSTGRES_USER=hibernate_orm_test -e POSTGRES_PASSWORD=hibernate_orm_test -e POSTGRES_DB=hibernate_orm_test -p5432:5432 -d docker.io/postgis/postgis:13-3.1 + $CONTAINER_CLI run --name postgres -e POSTGRES_USER=hibernate_orm_test -e POSTGRES_PASSWORD=hibernate_orm_test -e POSTGRES_DB=hibernate_orm_test -p5432:5432 -d ${DB_IMAGE_POSTGRESQL_13:-docker.io/postgis/postgis:13-3.1} $CONTAINER_CLI exec postgres bash -c '/usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y && apt install -y postgresql-13-pgvector && psql -U hibernate_orm_test -d hibernate_orm_test -c "create extension vector;"' } postgresql_14() { $CONTAINER_CLI rm -f postgres || true - $CONTAINER_CLI run --name postgres -e POSTGRES_USER=hibernate_orm_test -e POSTGRES_PASSWORD=hibernate_orm_test -e POSTGRES_DB=hibernate_orm_test -p5432:5432 -d docker.io/postgis/postgis:14-3.3 + $CONTAINER_CLI run --name postgres -e POSTGRES_USER=hibernate_orm_test -e POSTGRES_PASSWORD=hibernate_orm_test -e POSTGRES_DB=hibernate_orm_test -p5432:5432 -d ${DB_IMAGE_POSTGRESQL_14:-docker.io/postgis/postgis:14-3.3} $CONTAINER_CLI exec postgres bash -c '/usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y && apt install -y postgresql-14-pgvector && psql -U hibernate_orm_test -d hibernate_orm_test -c "create extension vector;"' } postgresql_15() { $CONTAINER_CLI rm -f postgres || true - $CONTAINER_CLI run --name postgres -e POSTGRES_USER=hibernate_orm_test -e POSTGRES_PASSWORD=hibernate_orm_test -e POSTGRES_DB=hibernate_orm_test -p5432:5432 --tmpfs /pgtmpfs:size=131072k -d docker.io/postgis/postgis:15-3.3 \ + $CONTAINER_CLI run --name postgres -e POSTGRES_USER=hibernate_orm_test -e POSTGRES_PASSWORD=hibernate_orm_test -e POSTGRES_DB=hibernate_orm_test -p5432:5432 --tmpfs /pgtmpfs:size=131072k -d ${DB_IMAGE_POSTGRESQL_15:-docker.io/postgis/postgis:15-3.3} \ -c fsync=off -c synchronous_commit=off -c full_page_writes=off -c shared_buffers=256MB -c maintenance_work_mem=256MB -c max_wal_size=1GB -c checkpoint_timeout=1d $CONTAINER_CLI exec postgres bash -c '/usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y && apt install -y postgresql-15-pgvector && psql -U hibernate_orm_test -d hibernate_orm_test -c "create extension vector;"' } postgresql_16() { $CONTAINER_CLI rm -f postgres || true - $CONTAINER_CLI run --name postgres -e POSTGRES_USER=hibernate_orm_test -e POSTGRES_PASSWORD=hibernate_orm_test -e POSTGRES_DB=hibernate_orm_test -p5432:5432 --tmpfs /pgtmpfs:size=131072k -d docker.io/postgis/postgis:16-3.4 \ + $CONTAINER_CLI run --name postgres -e POSTGRES_USER=hibernate_orm_test -e POSTGRES_PASSWORD=hibernate_orm_test -e POSTGRES_DB=hibernate_orm_test -p5432:5432 --tmpfs /pgtmpfs:size=131072k -d ${DB_IMAGE_POSTGRESQL_16:-docker.io/postgis/postgis:16-3.4} \ -c fsync=off -c synchronous_commit=off -c full_page_writes=off -c shared_buffers=256MB -c maintenance_work_mem=256MB -c max_wal_size=1GB -c checkpoint_timeout=1d $CONTAINER_CLI exec postgres bash -c '/usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y && apt install -y postgresql-16-pgvector && psql -U hibernate_orm_test -d hibernate_orm_test -c "create extension vector;"' } @@ -218,7 +218,7 @@ db2() { db2_11_5() { $PRIVILEGED_CLI $CONTAINER_CLI rm -f db2 || true - $PRIVILEGED_CLI $CONTAINER_CLI run --name db2 --privileged -e DB2INSTANCE=orm_test -e DB2INST1_PASSWORD=orm_test -e DBNAME=orm_test -e LICENSE=accept -e AUTOCONFIG=false -e ARCHIVE_LOGS=false -e TO_CREATE_SAMPLEDB=false -e REPODB=false -p 50000:50000 -d icr.io/db2_community/db2:11.5.9.0 + $PRIVILEGED_CLI $CONTAINER_CLI run --name db2 --privileged -e DB2INSTANCE=orm_test -e DB2INST1_PASSWORD=orm_test -e DBNAME=orm_test -e LICENSE=accept -e AUTOCONFIG=false -e ARCHIVE_LOGS=false -e TO_CREATE_SAMPLEDB=false -e REPODB=false -p 50000:50000 -d ${DB_IMAGE_DB2_11_5:-icr.io/db2_community/db2:11.5.9.0} # Give the container some time to start OUTPUT= while [[ $OUTPUT != *"INSTANCE"* ]]; do @@ -232,7 +232,7 @@ db2_11_5() { db2_10_5() { $PRIVILEGED_CLI $CONTAINER_CLI rm -f db2 || true # The sha represents the tag 10.5.0.5-3.10.0 - $PRIVILEGED_CLI $CONTAINER_CLI run --name db2 --privileged -e DB2INST1_PASSWORD=db2inst1-pwd -e LICENSE=accept -p 50000:50000 -d docker.io/ibmoms/db2express-c@sha256:a499afd9709a1f69fb41703e88def9869955234c3525547e2efc3418d1f4ca2b db2start + $PRIVILEGED_CLI $CONTAINER_CLI run --name db2 --privileged -e DB2INST1_PASSWORD=db2inst1-pwd -e LICENSE=accept -p 50000:50000 -d ${DB_IMAGE_DB2_10_5:-quay.io/hibernate/db2express-c@sha256:a499afd9709a1f69fb41703e88def9869955234c3525547e2efc3418d1f4ca2b} db2start # Give the container some time to start OUTPUT= while [[ $OUTPUT != *"DB2START"* ]]; do @@ -293,7 +293,7 @@ CREATE TRANSFORM FOR db2gse.ST_Geometry DB2_PROGRAM ( EOF $PRIVILEGED_CLI $CONTAINER_CLI run --name db2spatial --privileged -e DB2INSTANCE=orm_test -e DB2INST1_PASSWORD=orm_test -e DBNAME=orm_test -e LICENSE=accept -e AUTOCONFIG=false -e ARCHIVE_LOGS=false -e TO_CREATE_SAMPLEDB=false -e REPODB=false \ -v ${temp_dir}:/conf \ - -p 50000:50000 -d docker.io/ibmcom/db2:11.5.5.0 + -p 50000:50000 -d ${DB_IMAGE_DB2_SPATIAL:-docker.io/ibmcom/db2:11.5.5.0} # Give the container some time to start OUTPUT= @@ -317,7 +317,7 @@ mssql() { mssql_2017() { $CONTAINER_CLI rm -f mssql || true #This sha256 matches a specific tag of mcr.microsoft.com/mssql/server:2017-latest : - $CONTAINER_CLI run --name mssql -d -p 1433:1433 -e "SA_PASSWORD=Hibernate_orm_test" -e ACCEPT_EULA=Y mcr.microsoft.com/mssql/server@sha256:7d194c54e34cb63bca083542369485c8f4141596805611e84d8c8bab2339eede + $CONTAINER_CLI run --name mssql -d -p 1433:1433 -e "SA_PASSWORD=Hibernate_orm_test" -e ACCEPT_EULA=Y ${DB_IMAGE_MSSQL_2017:-mcr.microsoft.com/mssql/server@sha256:7d194c54e34cb63bca083542369485c8f4141596805611e84d8c8bab2339eede} sleep 5 n=0 until [ "$n" -ge 5 ] @@ -339,7 +339,7 @@ mssql_2017() { mssql_2022() { $CONTAINER_CLI rm -f mssql || true #This sha256 matches a specific tag of 2022-CU12-ubuntu-22.04 (https://mcr.microsoft.com/en-us/product/mssql/server/tags): - $CONTAINER_CLI run --name mssql -d -p 1433:1433 -e "SA_PASSWORD=Hibernate_orm_test" -e ACCEPT_EULA=Y mcr.microsoft.com/mssql/server@sha256:b94071acd4612bfe60a73e265097c2b6388d14d9d493db8f37cf4479a4337480 + $CONTAINER_CLI run --name mssql -d -p 1433:1433 -e "SA_PASSWORD=Hibernate_orm_test" -e ACCEPT_EULA=Y ${DB_IMAGE_MSSQL_2022:-mcr.microsoft.com/mssql/server@sha256:b94071acd4612bfe60a73e265097c2b6388d14d9d493db8f37cf4479a4337480} sleep 5 n=0 until [ "$n" -ge 5 ] @@ -361,7 +361,7 @@ mssql_2022() { sybase() { $CONTAINER_CLI rm -f sybase || true # Yup, that sucks, but on ubuntu we need to use -T11889 as per: https://github.com/DataGrip/docker-env/issues/12 - $CONTAINER_CLI run -d -p 9000:5000 -p 9001:5001 --name sybase --entrypoint /bin/bash docker.io/nguoianphu/docker-sybase -c "source /opt/sybase/SYBASE.sh + $CONTAINER_CLI run -d -p 9000:5000 -p 9001:5001 --name sybase --entrypoint /bin/bash ${DB_IMAGE_SYBASE:-docker.io/nguoianphu/docker-sybase} -c "source /opt/sybase/SYBASE.sh /opt/sybase/ASE-16_0/bin/dataserver \ -d/opt/sybase/data/master.dat \ -e/opt/sybase/ASE-16_0/install/MYSYBASE.log \ @@ -742,7 +742,7 @@ oracle_21() { --health-interval 5s \ --health-timeout 5s \ --health-retries 10 \ - docker.io/gvenzl/oracle-xe:21.3.0 + ${DB_IMAGE_ORACLE_21:-docker.io/gvenzl/oracle-xe:21.3.0} oracle_setup } @@ -756,7 +756,7 @@ oracle_23() { --health-interval 5s \ --health-timeout 5s \ --health-retries 10 \ - docker.io/gvenzl/oracle-free:23 + ${DB_IMAGE_ORACLE_23:-docker.io/gvenzl/oracle-free:23} oracle_free_setup } @@ -773,7 +773,7 @@ hana() { --sysctl kernel.shmmni=4096 \ --sysctl kernel.shmall=8388608 \ -v $temp_dir:/config:Z \ - docker.io/saplabs/hanaexpress:2.00.072.00.20231123.1 \ + ${DB_IMAGE_HANA:-docker.io/saplabs/hanaexpress:2.00.072.00.20231123.1} \ --passwords-url file:///config/password.json \ --agree-to-sap-license # Give the container some time to start @@ -800,7 +800,7 @@ sinks: redact: false exit-on-error: true " - $CONTAINER_CLI run -d --name=cockroach -m 6g -p 26257:26257 -p 8080:8080 docker.io/cockroachdb/cockroach:v23.1.12 start-single-node \ + $CONTAINER_CLI run -d --name=cockroach -m 6g -p 26257:26257 -p 8080:8080 ${DB_IMAGE_COCKROACHDB_23_1:-docker.io/cockroachdb/cockroach:v23.1.12} start-single-node \ --insecure --store=type=mem,size=0.25 --advertise-addr=localhost --log="$LOG_CONFIG" OUTPUT= while [[ $OUTPUT != *"CockroachDB node starting"* ]]; do @@ -841,7 +841,7 @@ sinks: redact: false exit-on-error: true " - $CONTAINER_CLI run -d --name=cockroach -m 6g -p 26257:26257 -p 8080:8080 docker.io/cockroachdb/cockroach:v22.2.2 start-single-node \ + $CONTAINER_CLI run -d --name=cockroach -m 6g -p 26257:26257 -p 8080:8080 ${DB_IMAGE_COCKROACHDB_22_2:-docker.io/cockroachdb/cockroach:v22.2.2} start-single-node \ --insecure --store=type=mem,size=0.25 --advertise-addr=localhost --log="$LOG_CONFIG" OUTPUT= while [[ $OUTPUT != *"CockroachDB node starting"* ]]; do @@ -881,7 +881,7 @@ tidb_5_4() { $CONTAINER_CLI rm -f tidb || true $CONTAINER_CLI network rm -f tidb_network || true $CONTAINER_CLI network create tidb_network - $CONTAINER_CLI run --name tidb -p4000:4000 -d --network tidb_network docker.io/pingcap/tidb:v5.4.3 + $CONTAINER_CLI run --name tidb -p4000:4000 -d --network tidb_network ${DB_IMAGE_TIDB_5_4:-docker.io/pingcap/tidb:v5.4.3} # Give the container some time to start OUTPUT= n=0 @@ -908,8 +908,11 @@ informix() { } informix_14_10() { + temp_dir=$(mktemp -d) + echo "ALLOW_NEWLINE 1" >$temp_dir/onconfig.mod + chmod 777 -R $temp_dir $PRIVILEGED_CLI $CONTAINER_CLI rm -f informix || true - $PRIVILEGED_CLI $CONTAINER_CLI run --name informix --privileged -p 9088:9088 -e LICENSE=accept -e GL_USEGLU=1 -d icr.io/informix/informix-developer-database:14.10.FC9W1DE + $PRIVILEGED_CLI $CONTAINER_CLI run --name informix --privileged -p 9088:9088 -v $temp_dir:/opt/ibm/config -e LICENSE=accept -e GL_USEGLU=1 -d ${DB_IMAGE_INFORMIX_14_10:-icr.io/informix/informix-developer-database:14.10.FC9W1DE} echo "Starting Informix. This can take a few minutes" # Give the container some time to start OUTPUT= @@ -935,7 +938,7 @@ informix_14_10() { informix_12_10() { $PRIVILEGED_CLI $CONTAINER_CLI rm -f informix || true - $PRIVILEGED_CLI $CONTAINER_CLI run --name informix --privileged -p 9088:9088 -e LICENSE=accept -e GL_USEGLU=1 -d ibmcom/informix-developer-database:12.10.FC12W1DE + $PRIVILEGED_CLI $CONTAINER_CLI run --name informix --privileged -p 9088:9088 -e LICENSE=accept -e GL_USEGLU=1 -d ${DB_IMAGE_INFORMIX_12_10:-ibmcom/informix-developer-database:12.10.FC12W1DE} echo "Starting Informix. This can take a few minutes" # Give the container some time to start OUTPUT= diff --git a/documentation/documentation.gradle b/documentation/documentation.gradle index 02b5f8829852..ad0ee5f00b55 100644 --- a/documentation/documentation.gradle +++ b/documentation/documentation.gradle @@ -573,7 +573,7 @@ settingsDocumentation { } transaction { explicitPosition = 6 - summary = "Proxool Connection Pool Settings" + summary = "Transaction Environment Settings" description = "Settings which control how Hibernate interacts with and manages transactions" settingsClassName "org.hibernate.cfg.TransactionSettings" } diff --git a/documentation/src/main/asciidoc/introduction/Introduction.adoc b/documentation/src/main/asciidoc/introduction/Introduction.adoc index d7d33e09787c..861aa5023c74 100644 --- a/documentation/src/main/asciidoc/introduction/Introduction.adoc +++ b/documentation/src/main/asciidoc/introduction/Introduction.adoc @@ -533,8 +533,8 @@ Whatever the case, the code which orchestrates a unit of work usually just calls [source,java] ---- @GET -@Path("books/{titlePattern}") -public List findBooks(String titlePattern) { +@Path("books/{titlePattern}/{page:\\d+}") +public List findBooks(String titlePattern, int page) { var books = sessionFactory.fromTransaction(session -> Queries.findBooksByTitleWithPagination(session, titlePattern, Page.page(RESULTS_PER_PAGE, page)); @@ -565,8 +565,8 @@ We can call it just like we called our handwritten version: [source,java] ---- @GET -@Path("books/{titlePattern}") -public List findBooks(String titlePattern) { +@Path("books/{titlePattern}/{page:\\d+}") +public List findBooks(String titlePattern, int page) { var books = sessionFactory.fromTransaction(session -> Queries_.findBooksByTitleWithPagination(session, titlePattern, Page.page(RESULTS_PER_PAGE, page)); diff --git a/gradle/databases.gradle b/gradle/databases.gradle index d00a6fbf689c..9e4ac21bb51d 100644 --- a/gradle/databases.gradle +++ b/gradle/databases.gradle @@ -295,7 +295,7 @@ ext { 'jdbc.driver': 'com.informix.jdbc.IfxDriver', 'jdbc.user' : 'informix', 'jdbc.pass' : 'in4mix', - 'jdbc.url' : 'jdbc:informix-sqli://' + dbHost + ':9088/dev:INFORMIXSERVER=informix;user=informix;password=in4mix;DELIMIDENT=Y;DB_LOCALE=en_US.utf8', + 'jdbc.url' : 'jdbc:informix-sqli://' + dbHost + ':9088/dev:INFORMIXSERVER=informix;user=informix;password=in4mix;DBDATE=Y4MD-;DELIMIDENT=Y;DB_LOCALE=en_US.utf8', 'jdbc.datasource' : 'com.informix.jdbc.IfxDriver', // 'jdbc.datasource' : 'com.informix.jdbcx.IfxDataSource', 'connection.init_sql' : '' diff --git a/gradle/gradle-develocity.gradle b/gradle/gradle-develocity.gradle index 26422d767186..18279c3e44e7 100644 --- a/gradle/gradle-develocity.gradle +++ b/gradle/gradle-develocity.gradle @@ -6,12 +6,13 @@ */ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -// Applies details for `https://ge.hibernate.org` +// Applies details for `https://develocity.commonhaus.dev` // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ext { isCiEnvironment = isJenkins() || isGitHubActions() || isGenericCi() populateRemoteBuildCache = isEnabled( "POPULATE_REMOTE_GRADLE_CACHE" ) + useRemoteCache = !isEnabled( "DISABLE_REMOTE_GRADLE_CACHE" ) } private static boolean isJenkins() { @@ -45,7 +46,7 @@ static boolean isEnabled(String setting) { } develocity { - server = 'https://ge.hibernate.org' + server = 'https://develocity.commonhaus.dev' buildScan { capture { diff --git a/gradle/published-java-module.gradle b/gradle/published-java-module.gradle index b9ea71617b22..4919d285b1e5 100644 --- a/gradle/published-java-module.gradle +++ b/gradle/published-java-module.gradle @@ -9,8 +9,6 @@ apply from: rootProject.file( 'gradle/releasable.gradle' ) apply from: rootProject.file( 'gradle/java-module.gradle' ) apply from: rootProject.file( 'gradle/publishing-pom.gradle' ) -apply plugin: 'signing' - // Make sure that the publishReleaseArtifacts task of the release module runs the release task of this sub module tasks.getByPath( ':release:publishReleaseArtifacts' ).dependsOn tasks.release @@ -21,6 +19,7 @@ configurations { } dependencies { + // `javadocSources` is a special Configuration which is used as a basis for the aggregated-javadocs we produce javadocSources sourceSets.main.allJava } @@ -29,6 +28,8 @@ dependencies { // Publishing java { + // Configure the Java "software component" to include javadoc and sources jars in addition to the classes jar. + // Ultimately, this component is what makes up the publication for this project. withJavadocJar() withSourcesJar() } @@ -97,151 +98,20 @@ publishing { } } - -var signingKey = resolveSigningKey() -var signingPassword = findSigningProperty( "signingPassword" ) - -signing { - useInMemoryPgpKeys( signingKey, signingPassword ) - - sign publishing.publications.publishedArtifacts -} - -String resolveSigningKey() { - var key = findSigningProperty( "signingKey" ) - if ( key != null ) { - return key - } - - var keyFile = findSigningProperty( "signingKeyFile" ) - if ( keyFile != null ) { - return new File( keyFile ).text - } - - return null -} - -String findSigningProperty(String propName) { - if ( System.getProperty( propName ) != null ) { - logger.debug "Found `{}` as a system property", propName - return System.getProperty(propName ) - } - else if ( System.getenv().get( propName ) != null ) { - logger.debug "Found `{}` as an env-var property", propName - return System.getenv().get( propName ) - } - else if ( project.hasProperty( propName ) ) { - logger.debug "Found `{}` as a project property", propName - return project.hasProperty( propName ) - } - else { - logger.debug "Did not find `{}`", propName - return null - } -} - - -var signingTask = project.tasks.getByName( "signPublishedArtifactsPublication" ) as Sign -var signingExtension = project.getExtensions().getByType(SigningExtension) as SigningExtension - -task sign { - dependsOn "signPublications" -} - -task signPublications { t -> - tasks.withType( Sign ).all { s -> - t.dependsOn s - } -} - -signingTask.doFirst { - if ( signingKey == null || signingPassword == null ) { - throw new GradleException( - "Cannot perform signing without GPG details. Please set the `signingKey` and `signingKeyFile` properties" - ) - } -} - - -boolean wasSigningExplicitlyRequested() { - // check whether signing task was explicitly requested when running the build - // - // NOTE: due to https://discuss.gradle.org/t/how-to-tell-if-a-task-was-explicitly-asked-for-on-the-command-line/42853/3 - // we cannot definitively know whether the task was requested. Gradle really just does not expose this information. - // so we make a convention - we check the "start parameters" object to see which task-names were requested; - // the problem is that these are the raw names directly from the command line. e.g. it is perfectly legal to - // say `gradlew signPubArtPub` in place of `gradlew signPublishedArtifactsPublication` - Gradle will simply - // "expand" the name it finds. However, it does not make that available. - // - // so the convention is that we will check for the following task names - // - // for each of: - // 1. `sign` - // 2. `signPublications` - // 3. `signPublishedArtifactsPublication` - // - // and we check both forms: - // 1. "${taskName}" - // 2. project.path + ":${taskName}" - // - // we need to check both again because of the "start parameters" discussion - - def signingTaskNames = ["sign", "signPublications", "signPublishedArtifactsPublication"] - - for ( String taskName : signingTaskNames ) { - if ( gradle.startParameter.taskNames.contains( taskName ) - || gradle.startParameter.taskNames.contains( "${project.path}:${taskName}" ) ) { - return true - } - } - - return false -} - -if ( wasSigningExplicitlyRequested() ) { - // signing was explicitly requested - signingExtension.required = true -} -else { - gradle.taskGraph.whenReady { graph -> - if ( graph.hasTask( signingTask ) ) { - // signing is scheduled to happen. - // - // we know, from above if-check, that it was not explicitly requested - - // so it is triggered via task dependency. make sure we want it to happen - var publishingTask = project.tasks.getByName( "publishPublishedArtifactsPublicationToSonatypeRepository" ) as PublishToMavenRepository - if ( graph.hasTask( publishingTask ) ) { - // we are publishing to Sonatype OSSRH - we need the signing to happen - signingExtension.required = true - } - else { - // signing was not explicitly requested and we are not publishing to OSSRH, - // so do not sign. - signingTask.enabled = false - } - } - - } -} - - // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Release / publishing tasks task ciBuild { - dependsOn test, tasks.publishToSonatype + dependsOn test } -tasks.release.dependsOn tasks.test, tasks.publishToSonatype +tasks.release.dependsOn tasks.test tasks.preVerifyRelease.dependsOn build tasks.preVerifyRelease.dependsOn generateMetadataFileForPublishedArtifactsPublication tasks.preVerifyRelease.dependsOn generatePomFileForPublishedArtifactsPublication tasks.preVerifyRelease.dependsOn generatePomFileForRelocationPomPublication -tasks.publishToSonatype.mustRunAfter test - - // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Ancillary tasks @@ -254,4 +124,4 @@ task showPublications { } } } -} \ No newline at end of file +} diff --git a/gradle/publishing-pom.gradle b/gradle/publishing-pom.gradle index 4654f0d873e1..c548bbb337f4 100644 --- a/gradle/publishing-pom.gradle +++ b/gradle/publishing-pom.gradle @@ -59,5 +59,18 @@ publishing { } } + repositories { + maven { + name = "staging" + url = rootProject.layout.buildDirectory.dir("staging-deploy${File.separator}maven") + } + maven { + name = 'snapshots' + url = "https://oss.sonatype.org/content/repositories/snapshots/" + // So that Gradle uses the `ORG_GRADLE_PROJECT_snapshotsPassword` / `ORG_GRADLE_PROJECT_snapshotsUsername` + // env variables to read the username/password for the `snapshots` repository publishing: + credentials(PasswordCredentials) + } + } } diff --git a/gradle/version.properties b/gradle/version.properties index f61ae8f56a8b..b1aa7633adee 100644 --- a/gradle/version.properties +++ b/gradle/version.properties @@ -1 +1 @@ -hibernateVersion=6.6.1-SNAPSHOT \ No newline at end of file +hibernateVersion=6.6.17-SNAPSHOT \ No newline at end of file diff --git a/hibernate-agroal/src/main/java/org/hibernate/agroal/internal/AgroalConnectionProvider.java b/hibernate-agroal/src/main/java/org/hibernate/agroal/internal/AgroalConnectionProvider.java index a96e42277c28..9ef17546b8b6 100644 --- a/hibernate-agroal/src/main/java/org/hibernate/agroal/internal/AgroalConnectionProvider.java +++ b/hibernate-agroal/src/main/java/org/hibernate/agroal/internal/AgroalConnectionProvider.java @@ -9,10 +9,11 @@ import java.sql.Connection; import java.sql.SQLException; +import java.sql.DatabaseMetaData; +import javax.sql.DataSource; import java.util.Map; import java.util.function.Consumer; import java.util.function.Function; -import javax.sql.DataSource; import org.hibernate.HibernateException; import org.hibernate.cfg.AgroalSettings; @@ -36,6 +37,7 @@ import io.agroal.api.security.SimplePassword; import static org.hibernate.cfg.AgroalSettings.AGROAL_CONFIG_PREFIX; +import static org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.allowJdbcMetadataAccess; /** * ConnectionProvider based on Agroal connection pool @@ -64,6 +66,7 @@ public class AgroalConnectionProvider implements ConnectionProvider, Configurabl public static final String CONFIG_PREFIX = AGROAL_CONFIG_PREFIX + "."; private static final long serialVersionUID = 1L; private AgroalDataSource agroalDataSource = null; + private boolean isMetadataAccessAllowed = true; // --- Configurable @@ -92,6 +95,8 @@ private static void copyProperty(Map properties, String key, @Override public void configure(Map props) throws HibernateException { + isMetadataAccessAllowed = allowJdbcMetadataAccess( props ); + ConnectionInfoLogger.INSTANCE.configureConnectionPool( "Agroal" ); try { AgroalPropertiesReader agroalProperties = new AgroalPropertiesReader( CONFIG_PREFIX ) @@ -139,9 +144,12 @@ public DatabaseConnectionInfo getDatabaseConnectionInfo(Dialect dialect) { final AgroalConnectionPoolConfiguration acpc = agroalDataSource.getConfiguration().connectionPoolConfiguration(); final AgroalConnectionFactoryConfiguration acfc = acpc.connectionFactoryConfiguration(); + return new DatabaseConnectionInfoImpl( acfc.jdbcUrl(), - acfc.connectionProviderClass().toString(), + // Attempt to resolve the driver name from the dialect, in case it wasn't explicitly set and access to + // the database metadata is allowed + acfc.connectionProviderClass() != null ? acfc.connectionProviderClass().toString() : extractDriverNameFromMetadata(), dialect.getVersion(), Boolean.toString( acfc.autoCommit() ), acfc.jdbcTransactionIsolation() != null @@ -152,6 +160,19 @@ public DatabaseConnectionInfo getDatabaseConnectionInfo(Dialect dialect) { ); } + private String extractDriverNameFromMetadata() { + if (isMetadataAccessAllowed) { + try ( Connection conn = getConnection() ) { + DatabaseMetaData dbmd = conn.getMetaData(); + return dbmd.getDriverName(); + } + catch (SQLException e) { + // Do nothing + } + } + return null; + } + @Override public boolean isUnwrappableAs(Class unwrapType) { return ConnectionProvider.class.equals( unwrapType ) diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/AltibaseDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/AltibaseDialect.java index 09d6ccbb2877..b4dc15316072 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/AltibaseDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/AltibaseDialect.java @@ -395,20 +395,26 @@ public String castPattern(CastType from, CastType to) { } break; case INTEGER_BOOLEAN: - result = BooleanDecoder.toIntegerBoolean( from ); + result = from == CastType.STRING + ? buildStringToBooleanCastDecode( "1", "0" ) + : BooleanDecoder.toIntegerBoolean( from ); if ( result != null ) { return result; } break; case YN_BOOLEAN: - result = BooleanDecoder.toYesNoBoolean( from ); + result = from == CastType.STRING + ? buildStringToBooleanCastDecode( "'Y'", "'N'" ) + : BooleanDecoder.toYesNoBoolean( from ); if ( result != null ) { return result; } break; case BOOLEAN: case TF_BOOLEAN: - result = BooleanDecoder.toTrueFalseBoolean( from ); + result = from == CastType.STRING + ? buildStringToBooleanCastDecode( "'T'", "'F'" ) + : BooleanDecoder.toTrueFalseBoolean( from ); if ( result != null ) { return result; } @@ -704,4 +710,14 @@ public SQLExceptionConversionDelegate buildSQLExceptionConversionDelegate() { }; } + @Override + public String getDual() { + return "dual"; + } + + @Override + public String getFromDualForSelectOnly() { + return " from " + getDual(); + } + } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/AltibaseSqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/AltibaseSqlAstTranslator.java index 0ab86ef3ef16..a258f1d977d3 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/AltibaseSqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/AltibaseSqlAstTranslator.java @@ -221,16 +221,6 @@ public void visitQueryPartTableReference(QueryPartTableReference tableReference) emulateQueryPartTableReferenceColumnAliasing( tableReference ); } - @Override - protected String getDual() { - return "dual"; - } - - @Override - protected String getFromDualForSelectOnly() { - return " from " + getDual(); - } - @Override protected boolean needsRecursiveKeywordInWithClause() { return false; diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CUBRIDDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CUBRIDDialect.java index d73897a57aa6..1c4a5fbbca13 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CUBRIDDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CUBRIDDialect.java @@ -517,4 +517,16 @@ private void timediff( sqlAppender.append( diffUnit.conversionFactor( toUnit, this ) ); } + @Override + public String getDual() { + //TODO: is this really needed? + //TODO: would "from table({0})" be better? + return "db_root"; + } + + @Override + public String getFromDualForSelectOnly() { + return " from " + getDual(); + } + } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CUBRIDSqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CUBRIDSqlAstTranslator.java index e187406b3baa..5c59c6a29de3 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CUBRIDSqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CUBRIDSqlAstTranslator.java @@ -80,16 +80,4 @@ protected boolean supportsRowValueConstructorSyntaxInInList() { protected boolean supportsRowValueConstructorSyntaxInQuantifiedPredicates() { return false; } - - @Override - protected String getDual() { - //TODO: is this really needed? - //TODO: would "from table({0})" be better? - return "db_root"; - } - - @Override - protected String getFromDualForSelectOnly() { - return " from " + getDual(); - } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacyDialect.java index c0cebad8c745..0dc198df18ef 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacyDialect.java @@ -70,6 +70,7 @@ import org.hibernate.metamodel.spi.RuntimeModelCreationContext; import org.hibernate.procedure.internal.DB2CallableStatementSupport; import org.hibernate.procedure.spi.CallableStatementSupport; +import org.hibernate.query.sqm.CastType; import org.hibernate.query.sqm.IntervalType; import org.hibernate.query.sqm.TemporalUnit; import org.hibernate.query.sqm.mutation.internal.cte.CteInsertStrategy; @@ -1056,6 +1057,19 @@ public IdentityColumnSupport getIdentityColumnSupport() { return DB2IdentityColumnSupport.INSTANCE; } + /** + * @return {@code true} because we can use {@code select ... from new table (insert .... )} + */ + @Override + public boolean supportsInsertReturning() { + return true; + } + + @Override + public boolean supportsInsertReturningRowId() { + return false; + } + @Override public boolean supportsValuesList() { return true; @@ -1141,6 +1155,16 @@ public String extractPattern(TemporalUnit unit) { return super.extractPattern( unit ); } + @Override + public String castPattern(CastType from, CastType to) { + if ( from == CastType.STRING && to == CastType.BOOLEAN ) { + return "cast(?1 as ?2)"; + } + else { + return super.castPattern( from, to ); + } + } + @Override public int getInExpressionCountLimit() { return BIND_PARAMETERS_NUMBER_LIMIT; @@ -1208,4 +1232,14 @@ public DmlTargetColumnQualifierSupport getDmlTargetColumnQualifierSupport() { public boolean supportsFromClauseInUpdate() { return getDB2Version().isSameOrAfter( 11 ); } + + @Override + public String getDual() { + return "sysibm.dual"; + } + + @Override + public String getFromDualForSelectOnly() { + return " from " + getDual(); + } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacySqlAstTranslator.java index f8ef6b3fb263..5f75a3062dd3 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacySqlAstTranslator.java @@ -76,7 +76,7 @@ protected boolean supportsWithClauseInSubquery() { } @Override - protected void renderTableReferenceJoins(TableGroup tableGroup) { + protected void renderTableReferenceJoins(TableGroup tableGroup, int swappedJoinIndex, boolean forceLeftJoin) { // When we are in a recursive CTE, we can't render joins on DB2... // See https://modern-sql.com/feature/with-recursive/db2/error-345-state-42836 if ( isInRecursiveQueryPart() ) { @@ -103,7 +103,7 @@ protected void renderTableReferenceJoins(TableGroup tableGroup) { } } else { - super.renderTableReferenceJoins( tableGroup ); + super.renderTableReferenceJoins( tableGroup, swappedJoinIndex, forceLeftJoin ); } } @@ -606,16 +606,6 @@ protected boolean supportsRowValueConstructorSyntaxInQuantifiedPredicates() { return false; } - @Override - protected String getDual() { - return "sysibm.dual"; - } - - @Override - protected String getFromDualForSelectOnly() { - return " from " + getDual(); - } - @Override protected void visitReturningColumns(List returningColumns) { // For DB2 we use #renderReturningClause to render a wrapper around the DML statement diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DerbyLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DerbyLegacyDialect.java index 70e098ece421..df1b6bfa4df1 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DerbyLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DerbyLegacyDialect.java @@ -1055,6 +1055,11 @@ public boolean supportsWindowFunctions() { return getVersion().isSameOrAfter( 10, 4 ); } + @Override + public boolean supportsValuesList() { + return true; + } + @Override public IdentifierHelper buildIdentifierHelper(IdentifierHelperBuilder builder, DatabaseMetaData dbMetaData) throws SQLException { @@ -1066,4 +1071,14 @@ public IdentifierHelper buildIdentifierHelper(IdentifierHelperBuilder builder, D public DmlTargetColumnQualifierSupport getDmlTargetColumnQualifierSupport() { return DmlTargetColumnQualifierSupport.TABLE_ALIAS; } + + @Override + public String getDual() { + return "(values 0)"; + } + + @Override + public String getFromDualForSelectOnly() { + return " from " + getDual() + " dual"; + } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DerbyLegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DerbyLegacySqlAstTranslator.java index 3aea1a5ebc1d..d3fee18516ff 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DerbyLegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DerbyLegacySqlAstTranslator.java @@ -302,16 +302,6 @@ protected boolean supportsRowValueConstructorSyntaxInQuantifiedPredicates() { return false; } - @Override - protected String getDual() { - return "(values 0)"; - } - - @Override - protected String getFromDualForSelectOnly() { - return " from " + getDual() + " dual"; - } - @Override protected boolean needsRowsToSkip() { return !supportsOffsetFetchClause(); diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/FirebirdDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/FirebirdDialect.java index 6f47f288fa0e..380fea18a3f8 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/FirebirdDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/FirebirdDialect.java @@ -403,25 +403,33 @@ public String castPattern(CastType from, CastType to) { } break; case BOOLEAN: - result = BooleanDecoder.toBoolean( from ); + result = from == CastType.STRING + ? buildStringToBooleanCastDecode( "true", "false" ) + : BooleanDecoder.toBoolean( from ); if ( result != null ) { return result; } break; case INTEGER_BOOLEAN: - result = BooleanDecoder.toIntegerBoolean( from ); + result = from == CastType.STRING + ? buildStringToBooleanCastDecode( "1", "0" ) + : BooleanDecoder.toIntegerBoolean( from ); if ( result != null ) { return result; } break; case YN_BOOLEAN: - result = BooleanDecoder.toYesNoBoolean( from ); + result = from == CastType.STRING + ? buildStringToBooleanCastDecode( "'Y'", "'N'" ) + : BooleanDecoder.toYesNoBoolean( from ); if ( result != null ) { return result; } break; case TF_BOOLEAN: - result = BooleanDecoder.toTrueFalseBoolean( from ); + result = from == CastType.STRING + ? buildStringToBooleanCastDecode( "'T'", "'F'" ) + : BooleanDecoder.toTrueFalseBoolean( from ); if ( result != null ) { return result; } @@ -1096,4 +1104,14 @@ else if ( supportsOffset && temporalAccessor instanceof Instant ) { } } + + @Override + public String getDual() { + return "rdb$database"; + } + + @Override + public String getFromDualForSelectOnly() { + return " from " + getDual(); + } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/FirebirdSqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/FirebirdSqlAstTranslator.java index 6f9c1215fda9..84ca5355bfa3 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/FirebirdSqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/FirebirdSqlAstTranslator.java @@ -262,16 +262,6 @@ protected boolean supportsRowValueConstructorSyntaxInQuantifiedPredicates() { return false; } - @Override - protected String getDual() { - return "rdb$database"; - } - - @Override - protected String getFromDualForSelectOnly() { - return " from " + getDual(); - } - private boolean supportsOffsetFetchClause() { return getDialect().getVersion().isSameOrAfter( 3 ); } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacyDialect.java index 133e12225afc..17764e545966 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacyDialect.java @@ -48,6 +48,7 @@ import org.hibernate.internal.util.JdbcExceptionHelper; import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.spi.RuntimeModelCreationContext; +import org.hibernate.query.sqm.CastType; import org.hibernate.query.sqm.FetchClauseType; import org.hibernate.query.sqm.IntervalType; import org.hibernate.dialect.NullOrdering; @@ -514,6 +515,16 @@ public String extractPattern(TemporalUnit unit) { : super.extractPattern(unit); } + @Override + public String castPattern(CastType from, CastType to) { + if ( from == CastType.STRING && to == CastType.BOOLEAN ) { + return "cast(?1 as ?2)"; + } + else { + return super.castPattern( from, to ); + } + } + @Override public String timestampaddPattern(TemporalUnit unit, TemporalType temporalType, IntervalType intervalType) { if ( intervalType != null ) { @@ -992,4 +1003,14 @@ public String getCaseInsensitiveLike() { public boolean supportsCaseInsensitiveLike() { return getVersion().isSameOrAfter( 1, 4, 194 ); } + + @Override + public boolean supportsValuesList() { + return true; + } + + @Override + public String getDual() { + return "dual"; + } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacySqlAstTranslator.java index 7213cd169079..b081591b6452 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacySqlAstTranslator.java @@ -340,13 +340,9 @@ protected boolean renderPrimaryTableReference(TableGroup tableGroup, LockMode lo final TableReference tableRef = tableGroup.getPrimaryTableReference(); // The H2 parser can't handle a sub-query as first element in a nested join // i.e. `join ( (select ...) alias join ... )`, so we have to introduce a dummy table reference - if ( tableRef instanceof QueryPartTableReference || tableRef.getTableId().startsWith( "(select" ) ) { - final boolean realTableGroup = tableGroup.isRealTableGroup() - && ( CollectionHelper.isNotEmpty( tableGroup.getTableReferenceJoins() ) - || hasNestedTableGroupsToRender( tableGroup.getNestedTableGroupJoins() ) ); - if ( realTableGroup ) { - appendSql( "dual cross join " ); - } + if ( getSqlBuffer().charAt( getSqlBuffer().length() - 1 ) == '(' + && ( tableRef instanceof QueryPartTableReference || tableRef.getTableId().startsWith( "(select" ) ) ) { + appendSql( "dual cross join " ); } return super.renderPrimaryTableReference( tableGroup, lockMode ); } @@ -392,11 +388,6 @@ protected boolean supportsNullPrecedence() { return getClauseStack().getCurrent() != Clause.WITHIN_GROUP || getDialect().getVersion().isSameOrAfter( 2 ); } - @Override - protected String getDual() { - return "dual"; - } - private boolean supportsOffsetFetchClause() { return getDialect().getVersion().isSameOrAfter( 1, 4, 195 ); } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HSQLLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HSQLLegacyDialect.java index b491f19a8592..5460ff680c8f 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HSQLLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HSQLLegacyDialect.java @@ -315,25 +315,33 @@ public String castPattern(CastType from, CastType to) { } break; case BOOLEAN: - result = BooleanDecoder.toBoolean( from ); + result = from == CastType.STRING + ? buildStringToBooleanCastDecode( "true", "false" ) + : BooleanDecoder.toBoolean( from ); if ( result != null ) { return result; } break; case INTEGER_BOOLEAN: - result = BooleanDecoder.toIntegerBoolean( from ); + result = from == CastType.STRING + ? buildStringToBooleanCastDecode( "1", "0" ) + : BooleanDecoder.toIntegerBoolean( from ); if ( result != null ) { return result; } break; case YN_BOOLEAN: - result = BooleanDecoder.toYesNoBoolean( from ); + result = from == CastType.STRING + ? buildStringToBooleanCastDecode( "'Y'", "'N'" ) + : BooleanDecoder.toYesNoBoolean( from ); if ( result != null ) { return result; } break; case TF_BOOLEAN: - result = BooleanDecoder.toTrueFalseBoolean( from ); + result = from == CastType.STRING + ? buildStringToBooleanCastDecode( "'T'", "'F'" ) + : BooleanDecoder.toTrueFalseBoolean( from ); if ( result != null ) { return result; } @@ -825,6 +833,11 @@ public boolean requiresFloatCastingOfIntegerDivision() { return true; } + @Override + public boolean supportsValuesList() { + return true; + } + @Override public IdentityColumnSupport getIdentityColumnSupport() { return identityColumnSupport; @@ -900,4 +913,9 @@ public UniqueDelegate getUniqueDelegate() { public DmlTargetColumnQualifierSupport getDmlTargetColumnQualifierSupport() { return DmlTargetColumnQualifierSupport.TABLE_ALIAS; } + + @Override + public String getFromDualForSelectOnly() { + return " from " + getDual(); + } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HSQLLegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HSQLLegacySqlAstTranslator.java index 8f7e958ee22b..1ca3acd135b3 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HSQLLegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HSQLLegacySqlAstTranslator.java @@ -12,20 +12,14 @@ import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.metamodel.mapping.JdbcMappingContainer; import org.hibernate.query.IllegalQueryOperationException; -import org.hibernate.query.sqm.BinaryArithmeticOperator; import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.sql.ast.Clause; import org.hibernate.sql.ast.SqlAstNodeRenderingMode; import org.hibernate.sql.ast.spi.AbstractSqlAstTranslator; import org.hibernate.sql.ast.spi.SqlSelection; -import org.hibernate.sql.ast.tree.MutationStatement; import org.hibernate.sql.ast.tree.Statement; -import org.hibernate.sql.ast.tree.cte.CteStatement; -import org.hibernate.sql.ast.tree.delete.DeleteStatement; -import org.hibernate.sql.ast.tree.expression.BinaryArithmeticExpression; import org.hibernate.sql.ast.tree.expression.CaseSearchedExpression; import org.hibernate.sql.ast.tree.expression.CaseSimpleExpression; -import org.hibernate.sql.ast.tree.expression.ColumnReference; import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.Literal; import org.hibernate.sql.ast.tree.expression.SqlTuple; @@ -147,8 +141,7 @@ protected void visitRecursivePath(Expression recursivePath, int sizeEstimate) { protected void visitAnsiCaseSearchedExpression( CaseSearchedExpression expression, Consumer resultRenderer) { - if ( getParameterRenderingMode() == SqlAstNodeRenderingMode.DEFAULT && areAllResultsParameters( expression ) - || areAllResultsPlainParametersOrLiterals( expression ) ) { + if ( areAllResultsPlainParametersOrStringLiterals( expression ) ) { final List whenFragments = expression.getWhenFragments(); final Expression firstResult = whenFragments.get( 0 ).getResult(); super.visitAnsiCaseSearchedExpression( @@ -172,8 +165,7 @@ protected void visitAnsiCaseSearchedExpression( protected void visitAnsiCaseSimpleExpression( CaseSimpleExpression expression, Consumer resultRenderer) { - if ( getParameterRenderingMode() == SqlAstNodeRenderingMode.DEFAULT && areAllResultsParameters( expression ) - || areAllResultsPlainParametersOrLiterals( expression ) ) { + if ( areAllResultsPlainParametersOrStringLiterals( expression ) ) { final List whenFragments = expression.getWhenFragments(); final Expression firstResult = whenFragments.get( 0 ).getResult(); super.visitAnsiCaseSimpleExpression( @@ -193,11 +185,11 @@ protected void visitAnsiCaseSimpleExpression( } } - protected boolean areAllResultsPlainParametersOrLiterals(CaseSearchedExpression caseSearchedExpression) { + protected boolean areAllResultsPlainParametersOrStringLiterals(CaseSearchedExpression caseSearchedExpression) { final List whenFragments = caseSearchedExpression.getWhenFragments(); final Expression firstResult = whenFragments.get( 0 ).getResult(); if ( isParameter( firstResult ) && getParameterRenderingMode() == SqlAstNodeRenderingMode.DEFAULT - || isLiteral( firstResult ) ) { + || isStringLiteral( firstResult ) ) { for ( int i = 1; i < whenFragments.size(); i++ ) { final Expression result = whenFragments.get( i ).getResult(); if ( isParameter( result ) ) { @@ -205,7 +197,7 @@ protected boolean areAllResultsPlainParametersOrLiterals(CaseSearchedExpression return false; } } - else if ( !isLiteral( result ) ) { + else if ( !isStringLiteral( result ) ) { return false; } } @@ -214,11 +206,11 @@ else if ( !isLiteral( result ) ) { return false; } - protected boolean areAllResultsPlainParametersOrLiterals(CaseSimpleExpression caseSimpleExpression) { + protected boolean areAllResultsPlainParametersOrStringLiterals(CaseSimpleExpression caseSimpleExpression) { final List whenFragments = caseSimpleExpression.getWhenFragments(); final Expression firstResult = whenFragments.get( 0 ).getResult(); if ( isParameter( firstResult ) && getParameterRenderingMode() == SqlAstNodeRenderingMode.DEFAULT - || isLiteral( firstResult ) ) { + || isStringLiteral( firstResult ) ) { for ( int i = 1; i < whenFragments.size(); i++ ) { final Expression result = whenFragments.get( i ).getResult(); if ( isParameter( result ) ) { @@ -226,7 +218,7 @@ protected boolean areAllResultsPlainParametersOrLiterals(CaseSimpleExpression ca return false; } } - else if ( !isLiteral( result ) ) { + else if ( !isStringLiteral( result ) ) { return false; } } @@ -235,6 +227,13 @@ else if ( !isLiteral( result ) ) { return false; } + private boolean isStringLiteral( Expression expression ) { + if ( expression instanceof Literal ) { + return ( (Literal) expression ).getJdbcMapping().getJdbcType().isStringLike(); + } + return false; + } + @Override public boolean supportsFilterClause() { return true; @@ -342,11 +341,6 @@ protected boolean supportsRowValueConstructorSyntaxInQuantifiedPredicates() { return false; } - @Override - protected String getFromDualForSelectOnly() { - return " from " + getDual(); - } - private boolean supportsOffsetFetchClause() { return getDialect().getVersion().isSameOrAfter( 2, 5 ); } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/InformixDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/InformixDialect.java index b269c9206b36..95e4983dc398 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/InformixDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/InformixDialect.java @@ -7,6 +7,9 @@ package org.hibernate.community.dialect; import java.sql.Types; +import java.time.temporal.TemporalAccessor; +import java.util.Date; +import java.util.TimeZone; import org.hibernate.boot.Metadata; import org.hibernate.boot.model.FunctionContributions; @@ -23,6 +26,7 @@ import org.hibernate.dialect.NullOrdering; import org.hibernate.dialect.Replacer; import org.hibernate.dialect.SelectItemReferenceStrategy; +import org.hibernate.dialect.VarcharUUIDJdbcType; import org.hibernate.dialect.function.CaseLeastGreatestEmulation; import org.hibernate.dialect.function.CommonFunctionFactory; import org.hibernate.dialect.identity.IdentityColumnSupport; @@ -33,6 +37,7 @@ import org.hibernate.dialect.unique.UniqueDelegate; import org.hibernate.engine.jdbc.Size; import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo; +import org.hibernate.engine.jdbc.env.spi.NameQualifierSupport; import org.hibernate.engine.spi.LoadQueryInfluencers; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.exception.spi.TemplatedViolatedConstraintNameExtractor; @@ -51,11 +56,13 @@ import org.hibernate.query.sqm.mutation.internal.temptable.LocalTemporaryTableMutationStrategy; import org.hibernate.query.sqm.mutation.spi.SqmMultiTableInsertStrategy; import org.hibernate.query.sqm.mutation.spi.SqmMultiTableMutationStrategy; +import org.hibernate.query.sqm.produce.function.StandardFunctionArgumentTypeResolvers; import org.hibernate.query.sqm.sql.SqmTranslator; import org.hibernate.query.sqm.sql.SqmTranslatorFactory; import org.hibernate.query.sqm.sql.StandardSqmTranslatorFactory; import org.hibernate.query.sqm.tree.select.SqmSelectStatement; import org.hibernate.service.ServiceRegistry; +import org.hibernate.sql.ast.SqlAstNodeRenderingMode; import org.hibernate.sql.ast.SqlAstTranslator; import org.hibernate.sql.ast.SqlAstTranslatorFactory; import org.hibernate.sql.ast.spi.SqlAppender; @@ -67,14 +74,21 @@ import org.hibernate.tool.schema.extract.spi.SequenceInformationExtractor; import org.hibernate.tool.schema.internal.StandardForeignKeyExporter; import org.hibernate.tool.schema.spi.Exporter; +import org.hibernate.type.JavaObjectType; +import org.hibernate.type.StandardBasicTypes; import org.hibernate.type.descriptor.jdbc.ClobJdbcType; +import org.hibernate.type.descriptor.jdbc.ObjectNullAsBinaryTypeJdbcType; import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; import org.hibernate.type.descriptor.sql.DdlType; import org.hibernate.type.descriptor.sql.internal.CapacityDependentDdlType; +import org.hibernate.type.descriptor.sql.internal.DdlTypeImpl; import org.hibernate.type.descriptor.sql.spi.DdlTypeRegistry; import org.hibernate.type.spi.TypeConfiguration; +import jakarta.persistence.TemporalType; + import static org.hibernate.exception.spi.TemplatedViolatedConstraintNameExtractor.extractUsingTemplate; +import static org.hibernate.query.sqm.produce.function.FunctionParameterType.STRING; import static org.hibernate.type.SqlTypes.BIGINT; import static org.hibernate.type.SqlTypes.BINARY; import static org.hibernate.type.SqlTypes.FLOAT; @@ -86,8 +100,17 @@ import static org.hibernate.type.SqlTypes.TIMESTAMP; import static org.hibernate.type.SqlTypes.TIMESTAMP_WITH_TIMEZONE; import static org.hibernate.type.SqlTypes.TINYINT; +import static org.hibernate.type.SqlTypes.UUID; import static org.hibernate.type.SqlTypes.VARBINARY; import static org.hibernate.type.SqlTypes.VARCHAR; +import static org.hibernate.type.descriptor.DateTimeUtils.JDBC_ESCAPE_END; +import static org.hibernate.type.descriptor.DateTimeUtils.JDBC_ESCAPE_START_DATE; +import static org.hibernate.type.descriptor.DateTimeUtils.JDBC_ESCAPE_START_TIME; +import static org.hibernate.type.descriptor.DateTimeUtils.JDBC_ESCAPE_START_TIMESTAMP; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsDate; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsLocalTime; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTime; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTimestampWithMicros; /** * Dialect for Informix 7.31.UD3 with Informix @@ -203,6 +226,7 @@ protected void registerColumnTypes(TypeContributions typeContributions, ServiceR .withTypeCapacity( getMaxNVarcharLength(), columnType( NVARCHAR ) ) .build() ); + ddlTypeRegistry.addDescriptor( new DdlTypeImpl( UUID, "char(36)", this ) ); } @Override @@ -234,6 +258,11 @@ public int getDefaultTimestampPrecision() { return 5; } + @Override + public boolean doesRoundTemporalOnOverflow() { + return false; + } + @Override public int getFloatPrecision() { return 8; @@ -254,10 +283,10 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio super.initializeFunctionRegistry(functionContributions); CommonFunctionFactory functionFactory = new CommonFunctionFactory(functionContributions); + functionFactory.aggregates( this, SqlAstNodeRenderingMode.NO_PLAIN_PARAMETER ); functionFactory.instr(); functionFactory.substr(); - functionFactory.substring_substr(); - //also natively supports ANSI-style substring() + functionFactory.substringFromFor(); functionFactory.trunc(); functionFactory.trim2(); functionFactory.space(); @@ -280,12 +309,30 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.monthsBetween(); functionFactory.stddev(); functionFactory.variance(); - functionFactory.locate_positionSubstring(); + functionFactory.bitLength_pattern( "length(?1)*8" ); + + if ( getVersion().isSameOrAfter( 12 ) ) { + functionFactory.locate_charindex(); + } //coalesce() and nullif() both supported since Informix 12 functionContributions.getFunctionRegistry().register( "least", new CaseLeastGreatestEmulation( true ) ); functionContributions.getFunctionRegistry().register( "greatest", new CaseLeastGreatestEmulation( false ) ); + functionContributions.getFunctionRegistry().namedDescriptorBuilder( "matches" ) + .setInvariantType( functionContributions.getTypeConfiguration() + .getBasicTypeRegistry() + .resolve( StandardBasicTypes.STRING ) + ) + .setExactArgumentCount( 2 ) + .setArgumentTypeResolver( + StandardFunctionArgumentTypeResolvers.impliedOrInvariant( + functionContributions.getTypeConfiguration(), + STRING + ) + ) + .setArgumentListSignature( "(STRING string, STRING pattern)" ) + .register(); if ( supportsWindowFunctions() ) { functionFactory.windowFunctions(); } @@ -620,6 +667,11 @@ public String[] getDropSchemaCommand(String schemaName) { return new String[] { "" }; } + @Override + public NameQualifierSupport getNameQualifierSupport() { + return NameQualifierSupport.BOTH; + } + @Override public boolean useCrossReferenceForeignKeys(){ return true; @@ -650,6 +702,11 @@ public String currentDate() { return "today"; } + @Override + public String currentTime() { + return currentTimestamp(); + } + @Override public String currentTimestamp() { return "current"; @@ -713,6 +770,56 @@ public static Replacer datetimeFormat(String format) { .replace("S", "%F1"); } + @Override + public void appendDateTimeLiteral( + SqlAppender appender, + TemporalAccessor temporalAccessor, + TemporalType precision, + TimeZone jdbcTimeZone) { + switch ( precision ) { + case DATE: + appender.appendSql( JDBC_ESCAPE_START_DATE ); + appendAsDate( appender, temporalAccessor ); + appender.appendSql( JDBC_ESCAPE_END ); + break; + case TIME: + appender.appendSql( JDBC_ESCAPE_START_TIME ); + appendAsTime( appender, temporalAccessor, supportsTemporalLiteralOffset(), jdbcTimeZone ); + appender.appendSql( JDBC_ESCAPE_END ); + break; + case TIMESTAMP: + appender.appendSql( JDBC_ESCAPE_START_TIMESTAMP ); + appendAsTimestampWithMicros( appender, temporalAccessor, supportsTemporalLiteralOffset(), jdbcTimeZone ); + appender.appendSql( JDBC_ESCAPE_END ); + break; + default: + throw new IllegalArgumentException(); + } + } + + @Override + public void appendDateTimeLiteral(SqlAppender appender, Date date, TemporalType precision, TimeZone jdbcTimeZone) { + switch ( precision ) { + case DATE: + appender.appendSql( JDBC_ESCAPE_START_DATE ); + appendAsDate( appender, date ); + appender.appendSql( JDBC_ESCAPE_END ); + break; + case TIME: + appender.appendSql( JDBC_ESCAPE_START_TIME ); + appendAsLocalTime( appender, date ); + appender.appendSql( JDBC_ESCAPE_END ); + break; + case TIMESTAMP: + appender.appendSql( JDBC_ESCAPE_START_TIMESTAMP ); + appendAsTimestampWithMicros( appender, date, jdbcTimeZone ); + appender.appendSql( JDBC_ESCAPE_END ); + break; + default: + throw new IllegalArgumentException(); + } + } + @Override public String getSelectClauseNullString(int sqlType, TypeConfiguration typeConfiguration) { DdlType descriptor = typeConfiguration.getDdlTypeRegistry().getDescriptor( sqlType ); @@ -743,5 +850,27 @@ public void contributeTypes(TypeContributions typeContributions, ServiceRegistry super.contributeTypes( typeContributions, serviceRegistry ); final JdbcTypeRegistry jdbcTypeRegistry = typeContributions.getTypeConfiguration().getJdbcTypeRegistry(); jdbcTypeRegistry.addDescriptor( Types.NCLOB, ClobJdbcType.DEFAULT ); + typeContributions.contributeJdbcType( VarcharUUIDJdbcType.INSTANCE ); + typeContributions.contributeJdbcType( ObjectNullAsBinaryTypeJdbcType.INSTANCE ); + + // Until we remove StandardBasicTypes, we have to keep this + typeContributions.contributeType( + new JavaObjectType( + ObjectNullAsBinaryTypeJdbcType.INSTANCE, + typeContributions.getTypeConfiguration() + .getJavaTypeRegistry() + .getDescriptor( Object.class ) + ) + ); + } + + @Override + public String getDual() { + return "(select 0 from systables where tabid=1)"; + } + + @Override + public String getFromDualForSelectOnly() { + return " from " + getDual() + " dual"; } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/InformixSqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/InformixSqlAstTranslator.java index 22528b21d065..eb451d4369e3 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/InformixSqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/InformixSqlAstTranslator.java @@ -153,16 +153,6 @@ protected boolean supportsRowValueConstructorSyntaxInQuantifiedPredicates() { return false; } - @Override - protected String getDual() { - return "(select 0 from systables where tabid=1)"; - } - - @Override - protected String getFromDualForSelectOnly() { - return " from " + getDual() + " dual"; - } - @Override protected void renderNull(Literal literal) { if ( getParameterRenderingMode() == SqlAstNodeRenderingMode.NO_UNTYPED ) { diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/IngresDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/IngresDialect.java index 5f62bfd28138..c2f5073404d0 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/IngresDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/IngresDialect.java @@ -559,4 +559,15 @@ public String translateExtractField(TemporalUnit unit) { public boolean supportsFetchClause(FetchClauseType type) { return getVersion().isSameOrAfter( 9, 3 ); } + + @Override + public String getDual() { + return "(select 0)"; + } + + @Override + public String getFromDualForSelectOnly() { + //this is only necessary if the query has a where clause + return " from " + getDual() + " dual"; + } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/IngresSqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/IngresSqlAstTranslator.java index 7a4af55cc9a6..be2c420c79e9 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/IngresSqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/IngresSqlAstTranslator.java @@ -138,17 +138,6 @@ protected boolean supportsRowValueConstructorSyntaxInQuantifiedPredicates() { return false; } - @Override - protected String getDual() { - return "(select 0)"; - } - - @Override - protected String getFromDualForSelectOnly() { - //this is only necessary if the query has a where clause - return " from " + getDual() + " dual"; - } - @Override protected boolean needsRowsToSkip() { return !supportsOffsetFetchClause(); diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MariaDBLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MariaDBLegacyDialect.java index d56e791050cb..402b34ee9a03 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MariaDBLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MariaDBLegacyDialect.java @@ -264,4 +264,14 @@ public IdentifierHelper buildIdentifierHelper(IdentifierHelperBuilder builder, D return super.buildIdentifierHelper( builder, dbMetaData ); } + + @Override + public String getDual() { + return "dual"; + } + + @Override + public String getFromDualForSelectOnly() { + return getVersion().isBefore( 10, 4 ) ? ( " from " + getDual() ) : ""; + } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MariaDBLegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MariaDBLegacySqlAstTranslator.java index 01cf6eb82e42..37b158444fb9 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MariaDBLegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MariaDBLegacySqlAstTranslator.java @@ -366,16 +366,6 @@ protected boolean supportsDistinctFromPredicate() { return true; } - @Override - protected String getDual() { - return "dual"; - } - - @Override - protected String getFromDualForSelectOnly() { - return getDialect().getVersion().isBefore( 10, 4 ) ? ( " from " + getDual() ) : ""; - } - @Override public MariaDBLegacyDialect getDialect() { return this.dialect; diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MaxDBDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MaxDBDialect.java index e527bd5baafa..f179bed01780 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MaxDBDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MaxDBDialect.java @@ -331,5 +331,15 @@ public String getTemporaryTableCreateOptions() { public boolean supportsJdbcConnectionLobCreation(DatabaseMetaData databaseMetaData) { return false; } + + @Override + public String getDual() { + return "dual"; + } + + @Override + public String getFromDualForSelectOnly() { + return " from " + getDual(); + } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MaxDBSqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MaxDBSqlAstTranslator.java index 011098389488..3955edfddcf3 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MaxDBSqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MaxDBSqlAstTranslator.java @@ -92,14 +92,4 @@ protected boolean supportsRowValueConstructorSyntaxInInList() { protected boolean supportsRowValueConstructorSyntaxInQuantifiedPredicates() { return false; } - - @Override - protected String getDual() { - return "dual"; - } - - @Override - protected String getFromDualForSelectOnly() { - return " from " + getDual(); - } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MimerSQLDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MimerSQLDialect.java index 3da7ba4005a5..198ffa412d79 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MimerSQLDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MimerSQLDialect.java @@ -344,4 +344,9 @@ public boolean useConnectionToCreateLob() { public IdentityColumnSupport getIdentityColumnSupport() { return MimerSQLIdentityColumnSupport.INSTANCE; } + + @Override + public String getFromDualForSelectOnly() { + return " from " + getDual(); + } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MimerSQLSqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MimerSQLSqlAstTranslator.java index 7510df6451ba..16d79ec7ffbb 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MimerSQLSqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MimerSQLSqlAstTranslator.java @@ -81,9 +81,4 @@ protected boolean supportsRowValueConstructorSyntaxInInList() { protected boolean supportsRowValueConstructorSyntaxInQuantifiedPredicates() { return false; } - - @Override - protected String getFromDualForSelectOnly() { - return " from " + getDual(); - } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacyDialect.java index 4739b447acd2..88f292f62db6 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacyDialect.java @@ -1429,4 +1429,14 @@ public boolean supportsFromClauseInUpdate() { return true; } + @Override + public String getDual() { + return "dual"; + } + + @Override + public String getFromDualForSelectOnly() { + return getVersion().isSameOrAfter( 8 ) ? "" : ( " from " + getDual() ); + } + } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacySqlAstTranslator.java index ec382f39b65d..78f63527c452 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacySqlAstTranslator.java @@ -389,16 +389,6 @@ protected boolean supportsWithClause() { return getDialect().getVersion().isSameOrAfter( 8 ); } - @Override - protected String getDual() { - return "dual"; - } - - @Override - protected String getFromDualForSelectOnly() { - return getDialect().getVersion().isSameOrAfter( 8 ) ? "" : ( " from " + getDual() ); - } - @Override public MySQLLegacyDialect getDialect() { return (MySQLLegacyDialect) DialectDelegateWrapper.extractRealDialect( super.getDialect() ); diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java index 14d0037cb2ae..254758e2a4a5 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java @@ -398,20 +398,33 @@ public String castPattern(CastType from, CastType to) { } break; case INTEGER_BOOLEAN: - result = BooleanDecoder.toIntegerBoolean( from ); + result = from == CastType.STRING + ? buildStringToBooleanCastDecode( "1", "0" ) + : BooleanDecoder.toIntegerBoolean( from ); if ( result != null ) { return result; } break; case YN_BOOLEAN: - result = BooleanDecoder.toYesNoBoolean( from ); + result = from == CastType.STRING + ? buildStringToBooleanCastDecode( "'Y'", "'N'" ) + : BooleanDecoder.toYesNoBoolean( from ); if ( result != null ) { return result; } break; case BOOLEAN: + result = from == CastType.STRING + ? buildStringToBooleanCastDecode( "true", "false" ) + : BooleanDecoder.toBoolean( from ); + if ( result != null ) { + return result; + } + break; case TF_BOOLEAN: - result = BooleanDecoder.toTrueFalseBoolean( from ); + result = from == CastType.STRING + ? buildStringToBooleanCastDecode( "'T'", "'F'" ) + : BooleanDecoder.toTrueFalseBoolean( from ); if ( result != null ) { return result; } @@ -1564,4 +1577,14 @@ public boolean useInputStreamToInsertBlob() { return false; } + @Override + public String getDual() { + return "dual"; + } + + @Override + public String getFromDualForSelectOnly() { + return " from " + getDual(); + } + } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacySqlAstTranslator.java index 1b8af5cc5d6c..15f5f489c427 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacySqlAstTranslator.java @@ -674,16 +674,6 @@ protected boolean supportsRowValueConstructorSyntaxInInSubQuery() { return getDialect().getVersion().isSameOrAfter( 9 ); } - @Override - protected String getDual() { - return "dual"; - } - - @Override - protected String getFromDualForSelectOnly() { - return " from " + getDual(); - } - private boolean supportsOffsetFetchClause() { return getDialect().supportsFetchClause( FetchClauseType.ROWS_ONLY ); } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java index 219079710bcf..4b5b73c012ce 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java @@ -60,6 +60,7 @@ import org.hibernate.procedure.spi.CallableStatementSupport; import org.hibernate.query.SemanticException; import org.hibernate.query.spi.QueryOptions; +import org.hibernate.query.sqm.CastType; import org.hibernate.query.sqm.FetchClauseType; import org.hibernate.query.sqm.IntervalType; import org.hibernate.query.sqm.TemporalUnit; @@ -418,6 +419,16 @@ public String extractPattern(TemporalUnit unit) { } } + @Override + public String castPattern(CastType from, CastType to) { + if ( from == CastType.STRING && to == CastType.BOOLEAN ) { + return "cast(?1 as ?2)"; + } + else { + return super.castPattern( from, to ); + } + } + /** * {@code microsecond} is the smallest unit for an {@code interval}, * and the highest precision for a {@code timestamp}, so we could diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacySqlAstTranslator.java index 4dae9260dac3..2a96aa8d8c55 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacySqlAstTranslator.java @@ -24,6 +24,7 @@ import org.hibernate.sql.ast.tree.insert.ConflictClause; import org.hibernate.sql.ast.tree.insert.InsertSelectStatement; import org.hibernate.sql.ast.tree.predicate.BooleanExpressionPredicate; +import org.hibernate.sql.ast.tree.predicate.InArrayPredicate; import org.hibernate.sql.ast.tree.predicate.LikePredicate; import org.hibernate.sql.ast.tree.predicate.NullnessPredicate; import org.hibernate.sql.ast.tree.select.QueryGroup; @@ -46,6 +47,14 @@ public class PostgreSQLLegacySqlAstTranslator extends A public PostgreSQLLegacySqlAstTranslator(SessionFactoryImplementor sessionFactory, Statement statement) { super( sessionFactory, statement ); } + + @Override + public void visitInArrayPredicate(InArrayPredicate inArrayPredicate) { + inArrayPredicate.getTestExpression().accept( this ); + appendSql( " = any (" ); + inArrayPredicate.getArrayParameter().accept( this ); + appendSql( ")" ); + } @Override protected void renderInsertIntoNoColumns(TableInsertStandard tableInsert) { diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/RDMSOS2200Dialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/RDMSOS2200Dialect.java index b4b111aa52a1..1e5f5db6cfcf 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/RDMSOS2200Dialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/RDMSOS2200Dialect.java @@ -434,4 +434,14 @@ public void appendDatetimeFormat(SqlAppender appender, String format) { public String trimPattern(TrimSpec specification, boolean isWhitespace) { return AbstractTransactSQLDialect.replaceLtrimRtrim( specification, isWhitespace ); } + + @Override + public String getDual() { + return "rdms.rdms_dummy"; + } + + @Override + public String getFromDualForSelectOnly() { + return " from " + getDual() + " where key_col=1"; + } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/RDMSOS2200SqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/RDMSOS2200SqlAstTranslator.java index 9a1f97109d78..24f726c877a1 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/RDMSOS2200SqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/RDMSOS2200SqlAstTranslator.java @@ -124,14 +124,4 @@ protected boolean supportsRowValueConstructorSyntaxInInList() { protected boolean supportsRowValueConstructorSyntaxInQuantifiedPredicates() { return false; } - - @Override - protected String getDual() { - return "rdms.rdms_dummy"; - } - - @Override - protected String getFromDualForSelectOnly() { - return " from " + getDual() + " where key_col=1"; - } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SingleStoreDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SingleStoreDialect.java index 8af3ae96b905..7c0347fa07ce 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SingleStoreDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SingleStoreDialect.java @@ -1411,4 +1411,9 @@ public boolean isForUpdateLockingEnabled() { * @settingDefault {@code false} */ public static final String SINGLE_STORE_FOR_UPDATE_LOCK_ENABLED = "hibernate.dialect.singlestore.for_update_lock_enabled"; + + @Override + public String getDual() { + return "dual"; + } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SingleStoreSqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SingleStoreSqlAstTranslator.java index c646a5500542..dac437dd4015 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SingleStoreSqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SingleStoreSqlAstTranslator.java @@ -437,11 +437,6 @@ protected boolean supportsDistinctFromPredicate() { return false; } - @Override - protected String getDual() { - return "dual"; - } - @Override public SingleStoreDialect getDialect() { return this.dialect; diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseASELegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseASELegacyDialect.java index 230b6d4f1e6a..db66214344cd 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseASELegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseASELegacyDialect.java @@ -125,13 +125,8 @@ protected void registerColumnTypes(TypeContributions typeContributions, ServiceR // But with jTDS we can't use them as the driver can't handle the types if ( getVersion().isSameOrAfter( 15, 5 ) && getDriverKind() != SybaseDriverKind.JTDS ) { ddlTypeRegistry.addDescriptor( - CapacityDependentDdlType.builder( DATE, "bigdatetime", "bigdatetime", this ) - .withTypeCapacity( 3, "datetime" ) - .build() - ); - ddlTypeRegistry.addDescriptor( - CapacityDependentDdlType.builder( TIME, "bigdatetime", "bigdatetime", this ) - .withTypeCapacity( 3, "datetime" ) + CapacityDependentDdlType.builder( TIME, "bigtime", "bigtime", this ) + .withTypeCapacity( 3, "time" ) .build() ); ddlTypeRegistry.addDescriptor( @@ -696,4 +691,9 @@ public LimitHandler getLimitHandler() { } return new TopLimitHandler(false); } + + @Override + public String getDual() { + return "(select 1 c1)"; + } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseASELegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseASELegacySqlAstTranslator.java index b5ace7590994..18197f136559 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseASELegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseASELegacySqlAstTranslator.java @@ -530,11 +530,6 @@ protected boolean supportsRowValueConstructorSyntaxInQuantifiedPredicates() { return false; } - @Override - protected String getDual() { - return "(select 1 c1)"; - } - private boolean supportsTopClause() { return getDialect().getVersion().isSameOrAfter( 12, 5 ); } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseAnywhereDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseAnywhereDialect.java index e77527c4c9a6..0a5d8d137367 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseAnywhereDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseAnywhereDialect.java @@ -215,4 +215,14 @@ public LimitHandler getLimitHandler() { return TopLimitHandler.INSTANCE; } + @Override + public String getDual() { + return "sys.dummy"; + } + + @Override + public String getFromDualForSelectOnly() { + return " from " + getDual(); + } + } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseAnywhereSqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseAnywhereSqlAstTranslator.java index 841594f12e66..eb086e24fb5e 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseAnywhereSqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseAnywhereSqlAstTranslator.java @@ -125,6 +125,7 @@ protected boolean renderNamedTableReference(NamedTableReference tableReference, // Just always return true because SQL Server doesn't support the FOR UPDATE clause return true; } + super.renderNamedTableReference( tableReference, lockMode ); return false; } @@ -239,14 +240,4 @@ protected boolean supportsRowValueConstructorSyntaxInInList() { protected boolean supportsRowValueConstructorSyntaxInQuantifiedPredicates() { return false; } - - @Override - protected String getDual() { - return "sys.dummy"; - } - - @Override - protected String getFromDualForSelectOnly() { - return " from " + getDual(); - } } diff --git a/hibernate-community-dialects/src/test/java/org/hibernate/community/dialect/InformixFunctionTest.java b/hibernate-community-dialects/src/test/java/org/hibernate/community/dialect/InformixFunctionTest.java index 91070aaf6587..762bf92940bf 100644 --- a/hibernate-community-dialects/src/test/java/org/hibernate/community/dialect/InformixFunctionTest.java +++ b/hibernate-community-dialects/src/test/java/org/hibernate/community/dialect/InformixFunctionTest.java @@ -181,6 +181,23 @@ public void testCurrentTimestamp(SessionFactoryScope scope) { ); } + @Test + @TestForIssue(jiraKey = "HHH-18369") + public void testMatches(SessionFactoryScope scope) { + scope.inTransaction( + (session) -> { + String country = (String) session.createQuery( + "select e.country " + + "from Event e " + + "where e.id = :id and matches(e.country, :country) = 'T'" ) + .setParameter( "id", event.id ) + .setParameter( "country", "R*" ) + .getSingleResult(); + assertEquals( "Romania", country ); + } + ); + } + private Calendar todayCalendar() { Calendar calendar = Calendar.getInstance(); calendar.set(Calendar.HOUR_OF_DAY, 0); diff --git a/hibernate-core/src/main/java/org/hibernate/binder/internal/CommentBinder.java b/hibernate-core/src/main/java/org/hibernate/binder/internal/CommentBinder.java index bafe20609a14..148962b76215 100644 --- a/hibernate-core/src/main/java/org/hibernate/binder/internal/CommentBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/binder/internal/CommentBinder.java @@ -38,26 +38,30 @@ public void bind(Comment comment, MetadataBuildingContext context, PersistentCla } else if ( value instanceof Collection ) { Collection collection = (Collection) value; - Table table = collection.getTable(); + Table table = collection.getCollectionTable(); // by default, the comment goes on the table if ( on.isEmpty() || table.getName().equalsIgnoreCase( on ) ) { table.setComment( text ); } - // but if 'on' is explicit, it can go on a column - Value element = collection.getElement(); - for ( Column column : element.getColumns() ) { - if ( column.getName().equalsIgnoreCase( on ) ) { - column.setComment( text ); + else { + // but if 'on' is explicit, it can go on a column + for ( Column column : table.getColumns() ) { + if ( column.getName().equalsIgnoreCase( on ) ) { + column.setComment( text ); + return; + } } + throw new AnnotationException( "No matching column for '@Comment(on=\"" + on + "\")'" ); } - //TODO: list index / map key columns } else { for ( Column column : value.getColumns() ) { if ( on.isEmpty() || column.getName().equalsIgnoreCase( on ) ) { column.setComment( text ); + return; } } + throw new AnnotationException( "No matching column for '@Comment(on=\"" + on + "\")'" ); } } @@ -70,12 +74,16 @@ public void bind(Comment comment, MetadataBuildingContext context, PersistentCla if ( on.isEmpty() || primary.getName().equalsIgnoreCase( on ) ) { primary.setComment( text ); } - // but if 'on' is explicit, it can go on a secondary table - for ( Join join : entity.getJoins() ) { - Table secondary = join.getTable(); - if ( secondary.getName().equalsIgnoreCase( on ) ) { - secondary.setComment( text ); + else { + // but if 'on' is explicit, it can go on a secondary table + for ( Join join : entity.getJoins() ) { + Table secondary = join.getTable(); + if ( secondary.getName().equalsIgnoreCase( on ) ) { + secondary.setComment( text ); + return; + } } + throw new AnnotationException( "No matching column for '@Comment(on=\"" + on + "\")'" ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/boot/beanvalidation/BeanValidationEventListener.java b/hibernate-core/src/main/java/org/hibernate/boot/beanvalidation/BeanValidationEventListener.java index b579c91b16c5..574e560b0f1d 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/beanvalidation/BeanValidationEventListener.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/beanvalidation/BeanValidationEventListener.java @@ -14,6 +14,8 @@ import org.hibernate.boot.internal.ClassLoaderAccessImpl; import org.hibernate.boot.registry.classloading.spi.ClassLoaderService; import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.event.spi.PreCollectionUpdateEvent; +import org.hibernate.event.spi.PreCollectionUpdateEventListener; import org.hibernate.event.spi.PreDeleteEvent; import org.hibernate.event.spi.PreDeleteEventListener; import org.hibernate.event.spi.PreInsertEvent; @@ -36,6 +38,8 @@ import jakarta.validation.Validator; import jakarta.validation.ValidatorFactory; +import static org.hibernate.internal.util.NullnessUtil.castNonNull; + /** * Event listener used to enable Bean Validation for insert/update/delete events. * @@ -44,7 +48,7 @@ */ //FIXME review exception model public class BeanValidationEventListener - implements PreInsertEventListener, PreUpdateEventListener, PreDeleteEventListener, PreUpsertEventListener { + implements PreInsertEventListener, PreUpdateEventListener, PreDeleteEventListener, PreUpsertEventListener, PreCollectionUpdateEventListener { private static final CoreMessageLogger LOG = Logger.getMessageLogger( CoreMessageLogger.class, @@ -121,6 +125,17 @@ public boolean onPreUpsert(PreUpsertEvent event) { return false; } + @Override + public void onPreUpdateCollection(PreCollectionUpdateEvent event) { + final Object entity = castNonNull( event.getCollection().getOwner() ); + validate( + entity, + event.getSession().getEntityPersister( event.getAffectedOwnerEntityName(), entity ), + event.getFactory(), + GroupsPerOperation.Operation.UPDATE + ); + } + private void validate( T object, EntityPersister persister, diff --git a/hibernate-core/src/main/java/org/hibernate/boot/beanvalidation/HibernateTraversableResolver.java b/hibernate-core/src/main/java/org/hibernate/boot/beanvalidation/HibernateTraversableResolver.java index 1058a19823e5..c0afa9d0dc1d 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/beanvalidation/HibernateTraversableResolver.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/beanvalidation/HibernateTraversableResolver.java @@ -15,8 +15,11 @@ import org.hibernate.Hibernate; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.type.AnyType; import org.hibernate.type.CollectionType; +import org.hibernate.type.ComponentType; import org.hibernate.type.CompositeType; +import org.hibernate.type.EntityType; import org.hibernate.type.Type; import jakarta.validation.Path; @@ -54,17 +57,17 @@ private void addAssociationsToTheSetForAllProperties(String[] names, Type[] type private void addAssociationsToTheSetForOneProperty(String name, Type type, String prefix, SessionFactoryImplementor factory) { - if ( type.isCollectionType() ) { + if ( type instanceof CollectionType ) { CollectionType collType = (CollectionType) type; Type assocType = collType.getElementType( factory ); addAssociationsToTheSetForOneProperty(name, assocType, prefix, factory); } //ToOne association - else if ( type.isEntityType() || type.isAnyType() ) { + else if ( type instanceof EntityType || type instanceof AnyType ) { associations.add( prefix + name ); } - else if ( type.isComponentType() ) { - CompositeType componentType = (CompositeType) type; + else if ( type instanceof ComponentType ) { + ComponentType componentType = (ComponentType) type; addAssociationsToTheSetForAllProperties( componentType.getPropertyNames(), componentType.getSubtypes(), diff --git a/hibernate-core/src/main/java/org/hibernate/boot/beanvalidation/TypeSafeActivator.java b/hibernate-core/src/main/java/org/hibernate/boot/beanvalidation/TypeSafeActivator.java index b66b4d623ada..c8086f409e87 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/beanvalidation/TypeSafeActivator.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/beanvalidation/TypeSafeActivator.java @@ -126,6 +126,7 @@ public static void applyCallbackListeners(ValidatorFactory validatorFactory, Act listenerRegistry.appendListeners( EventType.PRE_UPDATE, listener ); listenerRegistry.appendListeners( EventType.PRE_DELETE, listener ); listenerRegistry.appendListeners( EventType.PRE_UPSERT, listener ); + listenerRegistry.appendListeners( EventType.PRE_COLLECTION_UPDATE, listener ); listener.initialize( cfgService.getSettings(), classLoaderService ); } diff --git a/hibernate-core/src/main/java/org/hibernate/boot/internal/NamedHqlQueryDefinitionImpl.java b/hibernate-core/src/main/java/org/hibernate/boot/internal/NamedHqlQueryDefinitionImpl.java index 284a97cf9699..5bdb30e81522 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/internal/NamedHqlQueryDefinitionImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/internal/NamedHqlQueryDefinitionImpl.java @@ -70,6 +70,7 @@ public String getHqlString() { public NamedSqmQueryMemento resolve(SessionFactoryImplementor factory) { return new NamedHqlQueryMementoImpl( getRegistrationName(), + null, hqlString, firstResult, maxResults, diff --git a/hibernate-core/src/main/java/org/hibernate/boot/internal/NamedNativeQueryDefinitionImpl.java b/hibernate-core/src/main/java/org/hibernate/boot/internal/NamedNativeQueryDefinitionImpl.java index 688d8dc5e16e..987b873730b4 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/internal/NamedNativeQueryDefinitionImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/internal/NamedNativeQueryDefinitionImpl.java @@ -18,6 +18,8 @@ import org.hibernate.query.sql.internal.NamedNativeQueryMementoImpl; import org.hibernate.query.sql.spi.NamedNativeQueryMemento; +import org.checkerframework.checker.nullness.qual.Nullable; + import static org.hibernate.internal.util.StringHelper.isNotEmpty; /** @@ -86,15 +88,16 @@ public String getResultSetMappingClassName() { @Override public NamedNativeQueryMemento resolve(SessionFactoryImplementor factory) { + Class resultClass = isNotEmpty( resultSetMappingClassName ) + ? factory.getServiceRegistry().requireService( ClassLoaderService.class ).classForName( resultSetMappingClassName ) + : null; return new NamedNativeQueryMementoImpl( getRegistrationName(), + resultClass, sqlString, sqlString, resultSetMappingName, - isNotEmpty( resultSetMappingClassName ) - ? factory.getServiceRegistry().requireService( ClassLoaderService.class ) - .classForName( resultSetMappingClassName ) - : null, + resultClass, querySpaces, getCacheable(), getCacheRegion(), diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/TypeContributions.java b/hibernate-core/src/main/java/org/hibernate/boot/model/TypeContributions.java index 4e12ec503c0d..21752b741bf8 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/TypeContributions.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/TypeContributions.java @@ -58,7 +58,7 @@ default void contributeJdbcTypeConstructor(JdbcTypeConstructor typeConstructor) * type for values of type {@link UserType#returnedClass()}. */ default void contributeType(UserType type) { - contributeType( type, type.returnedClass().getName() ); + contributeType( type, type.returnedClass().getTypeName() ); } /** diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/AnnotationBinder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/AnnotationBinder.java index c0d55e21caf4..bb246dfc32a5 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/AnnotationBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/AnnotationBinder.java @@ -42,7 +42,6 @@ import org.hibernate.boot.registry.classloading.spi.ClassLoaderService; import org.hibernate.boot.spi.MetadataBuildingContext; import org.hibernate.internal.CoreMessageLogger; -import org.hibernate.internal.util.StringHelper; import org.hibernate.resource.beans.internal.FallbackBeanInstanceProducer; import org.hibernate.resource.beans.spi.ManagedBeanRegistry; import org.hibernate.type.descriptor.java.BasicJavaType; @@ -69,7 +68,6 @@ import static org.hibernate.boot.model.internal.AnnotatedClassType.EMBEDDABLE; import static org.hibernate.boot.model.internal.AnnotatedClassType.ENTITY; -import static org.hibernate.boot.model.internal.FilterDefBinder.bindFilterDefs; import static org.hibernate.boot.model.internal.GeneratorBinder.buildGenerators; import static org.hibernate.boot.model.internal.GeneratorBinder.buildIdGenerator; import static org.hibernate.boot.model.internal.InheritanceState.getInheritanceStateOfSuperEntity; @@ -226,7 +224,7 @@ public static void bindPackage(ClassLoaderService cls, String packageName, Metad bindGenericGenerators( annotatedPackage, context ); bindQueries( annotatedPackage, context ); - bindFilterDefs( annotatedPackage, context ); + FilterDefBinder.bindFilterDefs( annotatedPackage, context ); } private static void handleIdGenerators(XPackage annotatedPackage, MetadataBuildingContext context) { @@ -371,6 +369,12 @@ private static void bindNamedStoredProcedureQuery( } } + public static void bindFilterDefs( + XClass annotatedClass, + MetadataBuildingContext context) throws MappingException { + FilterDefBinder.bindFilterDefs( annotatedClass, context ); + } + /** * Bind an annotated class. A subclass must be bound after its superclass. * @@ -388,7 +392,6 @@ public static void bindClass( bindQueries( annotatedClass, context ); handleImport( annotatedClass, context ); - bindFilterDefs( annotatedClass, context ); bindTypeDescriptorRegistrations( annotatedClass, context ); bindEmbeddableInstantiatorRegistrations( annotatedClass, context ); bindUserTypeRegistrations( annotatedClass, context ); diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/CollectionBinder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/CollectionBinder.java index 786e6dc7f875..5432f18e16d5 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/CollectionBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/CollectionBinder.java @@ -2340,8 +2340,7 @@ private AnnotatedClassType annotatedElementType( } else { //force in case of attribute override - final boolean attributeOverride = property.isAnnotationPresent( AttributeOverride.class ) - || property.isAnnotationPresent( AttributeOverrides.class ); + final boolean attributeOverride = mappingDefinedAttributeOverrideOnElement(property); // todo : force in the case of Convert annotation(s) with embedded paths (beyond key/value prefixes)? return isEmbedded || attributeOverride ? EMBEDDABLE @@ -2349,6 +2348,11 @@ private AnnotatedClassType annotatedElementType( } } + protected boolean mappingDefinedAttributeOverrideOnElement(XProperty property) { + return property.isAnnotationPresent( AttributeOverride.class ) + || property.isAnnotationPresent( AttributeOverrides.class ); + } + static AnnotatedColumns createElementColumnsIfNecessary( Collection collection, AnnotatedColumns elementColumns, diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EmbeddableBinder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EmbeddableBinder.java index 92243de3333b..e70c0efa39e8 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EmbeddableBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EmbeddableBinder.java @@ -11,9 +11,9 @@ import java.lang.reflect.Type; import java.util.ArrayList; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.TreeMap; import org.hibernate.AnnotationException; import org.hibernate.annotations.DiscriminatorFormula; @@ -28,7 +28,6 @@ import org.hibernate.boot.spi.MetadataBuildingContext; import org.hibernate.boot.spi.PropertyData; import org.hibernate.internal.CoreMessageLogger; -import org.hibernate.mapping.AggregateColumn; import org.hibernate.mapping.BasicValue; import org.hibernate.mapping.Component; import org.hibernate.mapping.Property; @@ -66,19 +65,15 @@ import static org.hibernate.boot.model.internal.BinderHelper.getPropertyOverriddenByMapperOrMapsId; import static org.hibernate.boot.model.internal.BinderHelper.getRelativePath; import static org.hibernate.boot.model.internal.BinderHelper.hasToOneAnnotation; -import static org.hibernate.boot.model.internal.BinderHelper.isGlobalGeneratorNameGlobal; -import static org.hibernate.boot.model.internal.GeneratorBinder.buildGenerators; -import static org.hibernate.boot.model.internal.GeneratorBinder.generatorType; -import static org.hibernate.boot.model.internal.GeneratorBinder.makeIdGenerator; import static org.hibernate.boot.model.internal.HCANNHelper.findContainingAnnotations; import static org.hibernate.boot.model.internal.PropertyBinder.addElementsOfClass; import static org.hibernate.boot.model.internal.PropertyBinder.processElementAnnotations; +import static org.hibernate.boot.model.internal.PropertyBinder.processId; import static org.hibernate.boot.model.internal.PropertyHolderBuilder.buildPropertyHolder; import static org.hibernate.internal.CoreLogging.messageLogger; import static org.hibernate.internal.util.StringHelper.isEmpty; import static org.hibernate.internal.util.StringHelper.qualify; import static org.hibernate.internal.util.StringHelper.unqualify; -import static org.hibernate.mapping.SimpleValue.DEFAULT_ID_GEN_STRATEGY; /** * A binder responsible for interpreting {@link Embeddable} classes and producing @@ -413,7 +408,7 @@ static Component fillEmbeddable( final BasicType discriminatorType = (BasicType) component.getDiscriminator().getType(); // Discriminator values are used to construct the embeddable domain // type hierarchy so order of processing is important - final Map discriminatorValues = new TreeMap<>(); + final Map discriminatorValues = new LinkedHashMap<>(); collectDiscriminatorValue( returnedClassOrElement, discriminatorType, discriminatorValues ); collectSubclassElements( propertyAccessor, @@ -452,18 +447,27 @@ static Component fillEmbeddable( inheritanceStatePerClass ); - final XProperty property = propertyAnnotatedElement.getProperty(); - if ( property.isAnnotationPresent( GeneratedValue.class ) ) { - if ( isIdClass || subholder.isOrWithinEmbeddedId() ) { - processGeneratedId( context, component, property ); - } - else { - throw new AnnotationException( - "Property '" + property.getName() + "' of '" - + getPath( propertyHolder, inferredData ) - + "' is annotated '@GeneratedValue' but is not part of an identifier" ); + final XProperty member = propertyAnnotatedElement.getProperty(); + if ( isIdClass || subholder.isOrWithinEmbeddedId() ) { + final Property property = findProperty( component, member.getName() ); + if ( property != null ) { + // Identifier properties are always simple values + final SimpleValue value = (SimpleValue) property.getValue(); + processId( + subholder, + propertyAnnotatedElement, + value, + Map.of(), + context + ); } } + else if ( member.isAnnotationPresent( GeneratedValue.class ) ) { + throw new AnnotationException( + "Property '" + member.getName() + "' of '" + + getPath( propertyHolder, inferredData ) + + "' is annotated '@GeneratedValue' but is not part of an identifier" ); + } } if ( compositeUserType != null ) { @@ -473,6 +477,15 @@ static Component fillEmbeddable( return component; } + private static Property findProperty(Component component, String name) { + for ( Property property : component.getProperties() ) { + if ( property.getName().equals( name ) ) { + return property; + } + } + return null; + } + private static CompositeUserType compositeUserType( Class> compositeUserTypeClass, MetadataBuildingContext context) { @@ -616,7 +629,7 @@ private static List collectClassElements( //embeddable elements can have type defs final PropertyContainer container = new PropertyContainer( returnedClassOrElement, annotatedClass, propertyAccessor ); - addElementsOfClass( classElements, container, context); + addElementsOfClass( classElements, container, context, 0 ); //add elements of the embeddable's mapped superclasses XClass subclass = returnedClassOrElement; XClass superClass; @@ -624,7 +637,7 @@ private static List collectClassElements( //FIXME: proper support of type variables incl var resolved at upper levels final PropertyContainer superContainer = new PropertyContainer( superClass, annotatedClass, propertyAccessor ); - addElementsOfClass( classElements, superContainer, context ); + addElementsOfClass( classElements, superContainer, context, 0 ); if ( subclassToSuperclass != null ) { subclassToSuperclass.put( subclass.getName(), superClass.getName() ); } @@ -655,7 +668,7 @@ private static void collectSubclassElements( assert put == null; // collect property of subclass final PropertyContainer superContainer = new PropertyContainer( subclass, superclass, propertyAccessor ); - addElementsOfClass( classElements, superContainer, context ); + addElementsOfClass( classElements, superContainer, context, 0 ); // recursively do that same for all subclasses collectSubclassElements( propertyAccessor, @@ -726,7 +739,7 @@ private static List collectBaseClassElements( while ( !Object.class.getName().equals( baseReturnedClassOrElement.getName() ) ) { final PropertyContainer container = new PropertyContainer( baseReturnedClassOrElement, annotatedClass, propertyAccessor ); - addElementsOfClass( baseClassElements, container, context ); + addElementsOfClass( baseClassElements, container, context, 0 ); baseReturnedClassOrElement = baseReturnedClassOrElement.getSuperclass(); } return baseClassElements; @@ -782,40 +795,6 @@ private static boolean hasTriggeringAnnotation(XAnnotatedElement property) { || property.isAnnotationPresent(ManyToMany.class); } - private static void processGeneratedId(MetadataBuildingContext context, Component component, XProperty property) { - final GeneratedValue generatedValue = property.getAnnotation( GeneratedValue.class ); - final String generatorType = generatedValue != null - ? generatorType( generatedValue, property.getType(), context ) - : DEFAULT_ID_GEN_STRATEGY; - final String generator = generatedValue != null ? generatedValue.generator() : ""; - - if ( isGlobalGeneratorNameGlobal( context ) ) { - buildGenerators( property, context ); - context.getMetadataCollector().addSecondPass( new IdGeneratorResolverSecondPass( - (SimpleValue) component.getProperty( property.getName() ).getValue(), - property, - generatorType, - generator, - context - ) ); - -// handleTypeDescriptorRegistrations( property, context ); -// bindEmbeddableInstantiatorRegistrations( property, context ); -// bindCompositeUserTypeRegistrations( property, context ); -// handleConverterRegistrations( property, context ); - } - else { - makeIdGenerator( - (SimpleValue) component.getProperty( property.getName() ).getValue(), - property, - generatorType, - generator, - context, - new HashMap<>( buildGenerators( property, context ) ) - ); - } - } - private static void processIdClassElements( PropertyHolder propertyHolder, PropertyData baseInferredData, diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EntityBinder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EntityBinder.java index edc1d878b08d..751c8b90f9d7 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EntityBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EntityBinder.java @@ -105,6 +105,7 @@ import org.hibernate.mapping.SimpleValue; import org.hibernate.mapping.SingleTableSubclass; import org.hibernate.mapping.Subclass; +import org.hibernate.mapping.SyntheticProperty; import org.hibernate.mapping.Table; import org.hibernate.mapping.TableOwner; import org.hibernate.mapping.UnionSubclass; @@ -550,7 +551,7 @@ private Component createMapperProperty( propertyAccessor, isIdClass ); - final Property mapperProperty = new Property(); + final Property mapperProperty = new SyntheticProperty(); mapperProperty.setName( NavigablePath.IDENTIFIER_MAPPER_PROPERTY ); mapperProperty.setUpdateable( false ); mapperProperty.setInsertable( false ); @@ -616,7 +617,8 @@ private static PropertyData getUniqueIdPropertyFromBaseClass( inferredData.getPropertyClass(), propertyAccessor ); - addElementsOfClass( baseClassElements, propContainer, context ); + final int idPropertyCount = addElementsOfClass( baseClassElements, propContainer, context, 0 ); + assert idPropertyCount == 1; //Id properties are on top and there is only one return baseClassElements.get( 0 ); } diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/GeneratorBinder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/GeneratorBinder.java index 8f6707d8312c..d60ba173d99f 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/GeneratorBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/GeneratorBinder.java @@ -59,6 +59,7 @@ import static org.hibernate.boot.model.internal.BinderHelper.isCompositeId; import static org.hibernate.boot.model.internal.BinderHelper.isGlobalGeneratorNameGlobal; import static org.hibernate.id.factory.internal.IdentifierGeneratorUtil.collectParameters; +import static org.hibernate.internal.util.NullnessUtil.castNonNull; import static org.hibernate.mapping.SimpleValue.DEFAULT_ID_GEN_STRATEGY; public class GeneratorBinder { @@ -113,16 +114,10 @@ public static void makeIdGenerator( || identifierGeneratorStrategy.equals( "seqhilo" ); if ( generatorType == null || !avoidOverriding ) { id.setIdentifierGeneratorStrategy( identifierGeneratorStrategy ); - if ( DEFAULT_ID_GEN_STRATEGY.equals( identifierGeneratorStrategy ) ) { - id.setNullValue( "undefined" ); - } } //checkIfMatchingGenerator(definition, generatorType, generatorName); parameters.putAll( definition.getParameters() ); } - if ( DEFAULT_ID_GEN_STRATEGY.equals( generatorType ) ) { - id.setNullValue( "undefined" ); - } id.setIdentifierGeneratorParameters( parameters ); } @@ -534,14 +529,20 @@ private static void checkVersionGenerationAlways(XProperty property, Generator g private static void callConfigure(GeneratorCreationContext creationContext, Generator generator) { if ( generator instanceof Configurable ) { + final Configurable configurable = (Configurable) generator; final Value value = creationContext.getProperty().getValue(); - ( (Configurable) generator ).configure( value.getType(), collectParameters( - (SimpleValue) value, - creationContext.getDatabase().getDialect(), - creationContext.getDefaultCatalog(), - creationContext.getDefaultSchema(), - creationContext.getPersistentClass().getRootClass() - ), creationContext.getServiceRegistry() ); + configurable.create( creationContext ); + configurable.configure( + value.getType(), + collectParameters( + (SimpleValue) value, + creationContext.getDatabase().getDialect(), + creationContext.getDefaultCatalog(), + creationContext.getDefaultSchema(), + creationContext.getPersistentClass().getRootClass() + ), + creationContext.getServiceRegistry() + ); } } @@ -564,9 +565,9 @@ static void createIdGenerator( XProperty idProperty) { //manage composite related metadata //guess if its a component and find id data access (property, field etc) - final GeneratedValue generatedValue = idProperty.getAnnotation( GeneratedValue.class ); + final GeneratedValue generatedValue = castNonNull( idProperty.getAnnotation( GeneratedValue.class ) ); final String generatorType = generatorType( context, entityClass, isCompositeId( entityClass, idProperty ), generatedValue ); - final String generatorName = generatedValue == null ? "" : generatedValue.generator(); + final String generatorName = generatedValue.generator(); if ( isGlobalGeneratorNameGlobal( context ) ) { buildGenerators( idProperty, context ); context.getMetadataCollector().addSecondPass( new IdGeneratorResolverSecondPass( diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/InheritanceState.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/InheritanceState.java index 16156c606db8..7d7a9ba9d561 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/InheritanceState.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/InheritanceState.java @@ -234,12 +234,11 @@ private ElementsToProcess getElementsToProcess() { clazz, accessType ); - int currentIdPropertyCount = addElementsOfClass( + idPropertyCount = addElementsOfClass( elements, propertyContainer, - buildingContext - ); - idPropertyCount += currentIdPropertyCount; + buildingContext, + idPropertyCount ); } if ( idPropertyCount == 0 && !inheritanceState.hasParents() ) { diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/MapBinder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/MapBinder.java index a5b5665fdf2f..745a444b7950 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/MapBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/MapBinder.java @@ -107,8 +107,27 @@ public void secondPass(java.util.Map persistentClasses) }; } - private void makeOneToManyMapKeyColumnNullableIfNotInProperty( - final XProperty property) { + @Override + protected boolean mappingDefinedAttributeOverrideOnElement(XProperty property) { + if ( property.isAnnotationPresent( AttributeOverride.class ) ) { + return namedMapValue( property.getAnnotation( AttributeOverride.class ) ); + } + if ( property.isAnnotationPresent( AttributeOverrides.class ) ) { + final AttributeOverrides annotations = property.getAnnotation( AttributeOverrides.class ); + for ( AttributeOverride attributeOverride : annotations.value() ) { + if ( namedMapValue( attributeOverride ) ) { + return true; + } + } + } + return false; + } + + private boolean namedMapValue(AttributeOverride annotation) { + return annotation.name().startsWith( "value." ); + } + + private void makeOneToManyMapKeyColumnNullableIfNotInProperty(XProperty property) { final Map map = (Map) this.collection; if ( map.isOneToMany() && property.isAnnotationPresent( MapKeyColumn.class ) ) { diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/PropertyBinder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/PropertyBinder.java index 5c98e401d0ba..611543cabd54 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/PropertyBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/PropertyBinder.java @@ -15,6 +15,7 @@ import jakarta.persistence.Column; import jakarta.persistence.ElementCollection; import jakarta.persistence.EmbeddedId; +import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.JoinColumns; @@ -53,6 +54,8 @@ import org.hibernate.boot.spi.MetadataBuildingContext; import org.hibernate.boot.spi.PropertyData; import org.hibernate.engine.OptimisticLockStyle; +import org.hibernate.id.IdentifierGenerator; +import org.hibernate.id.PersistentIdentifierGenerator; import org.hibernate.internal.CoreMessageLogger; import org.hibernate.mapping.Collection; import org.hibernate.mapping.Component; @@ -552,17 +555,17 @@ private void validateOptimisticLock(OptimisticLock optimisticLock) { /** * @param elements List of {@link PropertyData} instances * @param propertyContainer Metadata about a class and its properties + * @param idPropertyCounter number of id properties already present in list of {@link PropertyData} instances * - * @return the number of id properties found while iterating the elements of - * {@code annotatedClass} using the determined access strategy + * @return total number of id properties found after iterating the elements of {@code annotatedClass} + * using the determined access strategy (starting from the provided {@code idPropertyCounter}) */ static int addElementsOfClass( List elements, PropertyContainer propertyContainer, - MetadataBuildingContext context) { - int idPropertyCounter = 0; + MetadataBuildingContext context, int idPropertyCounter) { for ( XProperty property : propertyContainer.propertyIterator() ) { - idPropertyCounter += addProperty( propertyContainer, property, elements, context ); + idPropertyCounter = addProperty( propertyContainer, property, elements, context, idPropertyCounter ); } return idPropertyCounter; } @@ -571,20 +574,20 @@ private static int addProperty( PropertyContainer propertyContainer, XProperty property, List inFlightPropertyDataList, - MetadataBuildingContext context) { + MetadataBuildingContext context, + int idPropertyCounter) { // see if inFlightPropertyDataList already contains a PropertyData for this name, // and if so, skip it.. for ( PropertyData propertyData : inFlightPropertyDataList ) { if ( propertyData.getPropertyName().equals( property.getName() ) ) { checkIdProperty( property, propertyData ); // EARLY EXIT!!! - return 0; + return idPropertyCounter; } } final XClass declaringClass = propertyContainer.getDeclaringClass(); final XClass entity = propertyContainer.getEntityAtStake(); - int idPropertyCounter = 0; final PropertyData propertyAnnotatedElement = new PropertyInferredData( declaringClass, property, @@ -1156,13 +1159,16 @@ && isEmbedded( property, property.getElementClass() ) ) { ); } else if ( propertyBinder.isId() ) { + if ( isIdentifierMapper ) { + throw new AnnotationException( "Property '"+ getPath( propertyHolder, inferredData ) + + "' belongs to an '@IdClass' and may not be annotated '@Id' or '@EmbeddedId'" ); + } //components and regular basic types create SimpleValue objects processId( propertyHolder, inferredData, (SimpleValue) propertyBinder.getValue(), classGenerators, - isIdentifierMapper, context ); } @@ -1415,17 +1421,12 @@ private static Class> resolveCompositeUserType( return null; } - private static void processId( + static void processId( PropertyHolder propertyHolder, PropertyData inferredData, SimpleValue idValue, Map classGenerators, - boolean isIdentifierMapper, MetadataBuildingContext context) { - if ( isIdentifierMapper ) { - throw new AnnotationException( "Property '"+ getPath( propertyHolder, inferredData ) - + "' belongs to an '@IdClass' and may not be annotated '@Id' or '@EmbeddedId'" ); - } final XProperty idProperty = inferredData.getProperty(); final List idGeneratorAnnotations = findContainingAnnotations( idProperty, IdGeneratorType.class ); final List generatorAnnotations = findContainingAnnotations( idProperty, ValueGenerationType.class ); @@ -1449,7 +1450,7 @@ else if ( !generatorAnnotations.isEmpty() ) { + "' is annotated '" + generatorAnnotations.get(0).annotationType() + "' which is not an '@IdGeneratorType'" ); } - else { + else if ( idProperty.isAnnotationPresent( GeneratedValue.class ) ) { final XClass entityClass = inferredData.getClassOrElement(); createIdGenerator( idValue, classGenerators, context, entityClass, idProperty ); if ( LOG.isTraceEnabled() ) { diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/PropertyContainer.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/PropertyContainer.java index 3248eee16f21..5b20e1c5e47e 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/PropertyContainer.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/PropertyContainer.java @@ -97,8 +97,8 @@ public PropertyContainer(XClass clazz, XClass entityAtStake, AccessType defaultC final Map localAttributeMap; // If the record class has only record components which match up with fields and no additional getters, - // we can retain the property order, to match up with the record component order - if ( !recordComponents.isEmpty() && recordComponents.size() == fields.size() && getters.isEmpty() ) { + // we must retain the property order, to match up with the record component order + if ( !recordComponents.isEmpty() && recordComponents.size() == fields.size() ) { localAttributeMap = new LinkedHashMap<>(); } //otherwise we sort them in alphabetical order, since this is at least deterministic diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/naming/ObjectNameNormalizer.java b/hibernate-core/src/main/java/org/hibernate/boot/model/naming/ObjectNameNormalizer.java index 08155aee23dd..c12454cee041 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/naming/ObjectNameNormalizer.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/naming/ObjectNameNormalizer.java @@ -34,7 +34,7 @@ public Identifier normalizeIdentifierQuoting(String identifierText) { return database().toIdentifier( identifierText ); } - protected Database database() { + public Database database() { if ( database == null ) { database = getBuildingContext().getMetadataCollector().getDatabase(); } @@ -42,10 +42,7 @@ protected Database database() { } public Identifier normalizeIdentifierQuoting(Identifier identifier) { - return getBuildingContext().getMetadataCollector() - .getDatabase() - .getJdbcEnvironment() - .getIdentifierHelper() + return database().getJdbcEnvironment().getIdentifierHelper() .normalizeQuoting( identifier ); } diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/source/internal/annotations/AnnotationMetadataSourceProcessorImpl.java b/hibernate-core/src/main/java/org/hibernate/boot/model/source/internal/annotations/AnnotationMetadataSourceProcessorImpl.java index fde786ffcbf3..a799c0365cd0 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/source/internal/annotations/AnnotationMetadataSourceProcessorImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/source/internal/annotations/AnnotationMetadataSourceProcessorImpl.java @@ -37,6 +37,8 @@ import org.jboss.jandex.IndexView; import org.jboss.logging.Logger; +import static org.hibernate.boot.model.internal.AnnotationBinder.bindFilterDefs; + /** * @author Steve Ebersole */ @@ -248,6 +250,13 @@ public void processEntityHierarchies(Set processedEntityNames) { orderedClasses, rootMetadataBuildingContext ); + // we want to go through all classes and collect the filter definitions first, + // so that when we bind the classes we have the complete list of filters to search from: + for ( XClass clazz : orderedClasses ) { + if ( !processedEntityNames.contains( clazz.getName() ) ) { + bindFilterDefs( clazz, rootMetadataBuildingContext ); + } + } for ( XClass clazz : orderedClasses ) { if ( processedEntityNames.contains( clazz.getName() ) ) { @@ -304,7 +313,28 @@ private List orderAndFillHierarchy(List classes) { } } } - return new ArrayList<>( orderedClasses ); + + // order the hierarchy + ArrayList workingCopy = new ArrayList<>( orderedClasses ); + List newList = new ArrayList<>( orderedClasses.size() ); + while ( !workingCopy.isEmpty() ) { + XClass clazz = workingCopy.get( 0 ); + orderHierarchy( workingCopy, newList, orderedClasses, clazz ); + } + return newList; + } + + private void orderHierarchy(List copy, List newList, LinkedHashSet original, XClass clazz) { + if ( clazz != null && !Object.class.getName().equals( clazz.getName() ) ) { + //process superclass first + orderHierarchy( copy, newList, original, clazz.getSuperclass() ); + if ( original.contains( clazz ) ) { + if ( !newList.contains( clazz ) ) { + newList.add( clazz ); + } + copy.remove( clazz ); + } + } } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/BiDirectionalAssociationHandler.java b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/BiDirectionalAssociationHandler.java index 7fcb1503a704..66483541f934 100644 --- a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/BiDirectionalAssociationHandler.java +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/BiDirectionalAssociationHandler.java @@ -80,6 +80,7 @@ static Implementation wrap( TypeDescription targetType = FieldLocator.ForClassHierarchy.Factory.INSTANCE.make( targetEntity ) .locate( bidirectionalAttributeName ) .getField() + .asDefined() .getType() .asErasure(); diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/ByteBuddyEnhancementContext.java b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/ByteBuddyEnhancementContext.java index 5e495b30f1ad..dd2097ae0bb4 100644 --- a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/ByteBuddyEnhancementContext.java +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/ByteBuddyEnhancementContext.java @@ -24,6 +24,7 @@ import net.bytebuddy.dynamic.scaffold.MethodGraph; import net.bytebuddy.matcher.ElementMatcher; import net.bytebuddy.pool.TypePool; +import org.hibernate.bytecode.enhance.spi.UnsupportedEnhancementStrategy; import static net.bytebuddy.matcher.ElementMatchers.isGetter; @@ -106,6 +107,10 @@ public void registerDiscoveredType(TypeDescription typeDescription, Type.Persist enhancementContext.registerDiscoveredType( new UnloadedTypeDescription( typeDescription ), type ); } + public UnsupportedEnhancementStrategy getUnsupportedEnhancementStrategy() { + return enhancementContext.getUnsupportedEnhancementStrategy(); + } + public void discoverCompositeTypes(TypeDescription managedCtClass, TypePool typePool) { if ( isDiscoveredType( managedCtClass ) ) { return; diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/CoreCacheProvider.java b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/CoreCacheProvider.java new file mode 100644 index 000000000000..f7aa7f3cdfe8 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/CoreCacheProvider.java @@ -0,0 +1,71 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.bytecode.enhance.internal.bytebuddy; + +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.pool.TypePool; + +/** + * A CacheProvider for ByteBuddy which is specifically designed for + * our CoreTypePool: it ensures the cache content doesn't get tainted + * by model types or anything else which isn't responsibility of this + * particular type pool. + * @implNote This cache instance shares the same @{@link CorePrefixFilter} + * instance of the @{@link CoreTypePool} which is using it, and uses it + * to guard writes into its internal caches. + */ +class CoreCacheProvider implements TypePool.CacheProvider { + + private final ConcurrentMap storage = new ConcurrentHashMap<>(); + private final CorePrefixFilter acceptedPrefixes; + + CoreCacheProvider(final CorePrefixFilter acceptedPrefixes) { + this.acceptedPrefixes = Objects.requireNonNull( acceptedPrefixes ); + register( + Object.class.getName(), + new TypePool.Resolution.Simple( TypeDescription.ForLoadedType.of( Object.class ) ) + ); + } + + /** + * {@inheritDoc} + */ + @Override + public TypePool.Resolution find(final String name) { + return storage.get( name ); + } + + /** + * {@inheritDoc} + */ + @Override + public TypePool.Resolution register(String name, TypePool.Resolution resolution) { + //Ensure we DO NOT cache anything from a non-core namespace, to not leak application specific code: + if ( acceptedPrefixes.isCoreClassName( name ) ) { + TypePool.Resolution cached = storage.putIfAbsent( name, resolution ); + return cached == null + ? resolution + : cached; + } + else { + return resolution; + } + } + + /** + * {@inheritDoc} + */ + @Override + public void clear() { + storage.clear(); + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/CorePrefixFilter.java b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/CorePrefixFilter.java new file mode 100644 index 000000000000..291e90d25b27 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/CorePrefixFilter.java @@ -0,0 +1,57 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.bytecode.enhance.internal.bytebuddy; + +import java.util.Objects; + +/** + * We differentiate between core classes and application classes during symbol + * resolution for the purposes of entity enhancement. + * The discriminator is the prefix of the fully qualified classname, for + * example it could be package names. + * The "core classes" don't have to be comprehensively defined: we want a small + * set of prefixes for which we know with certainty that a)They won't be used + * in application code (assuming people honour reasonable package name rules) + * or any code that needs being enhanced. and b) frequently need to be looked up + * during the enhancement process. + * A great example is the "jakarta.persistence.Entity" annotation: we'll most likely + * need to load it when doing any form of introspection on user's code, but we expect + * the bytecode which represents the annotation to not be enhanced. + * We then benefit from caching such representations of object types which are frequently + * loaded; since caching end user code would lead to enhancement problems, it's best + * to keep the list conservative when in doubt. + * For example, don't consider all of {@code "org.hibernate."} prefixes as safe, as + * that would include entities used during our own testsuite and entities defined by Envers. + * + */ +public final class CorePrefixFilter { + + private final String[] acceptedPrefixes; + public static final CorePrefixFilter DEFAULT_INSTANCE = new CorePrefixFilter(); + + /** + * Do not invoke: use DEFAULT_INSTANCE + */ + CorePrefixFilter() { + //By default optimise for jakarta annotations, java util collections, and Hibernate marker interfaces + this("jakarta.", "java.", "org.hibernate.annotations.", "org.hibernate.bytecode.enhance.spi.", "org.hibernate.engine.spi."); + } + + public CorePrefixFilter(final String... acceptedPrefixes) { + this.acceptedPrefixes = Objects.requireNonNull( acceptedPrefixes ); + } + + public boolean isCoreClassName(final String name) { + for ( String acceptedPrefix : this.acceptedPrefixes ) { + if ( name.startsWith( acceptedPrefix ) ) { + return true; + } + } + return false; + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/CoreTypePool.java b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/CoreTypePool.java index 87dbc33729d9..48d45eddc2aa 100644 --- a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/CoreTypePool.java +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/CoreTypePool.java @@ -6,6 +6,7 @@ */ package org.hibernate.bytecode.enhance.internal.bytebuddy; +import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import net.bytebuddy.description.type.TypeDescription; @@ -27,42 +28,39 @@ public class CoreTypePool extends TypePool.AbstractBase implements TypePool { private final ClassLoader hibernateClassLoader = CoreTypePool.class.getClassLoader(); private final ConcurrentHashMap resolutions = new ConcurrentHashMap<>(); - private final String[] acceptedPrefixes; + private final CorePrefixFilter acceptedPrefixes; /** - * Construct a new {@link CoreTypePool} with its default configuration: - * to only load classes whose package names start with either "jakarta." - * or "java." + * Construct a new {@link CoreTypePool} with its default configuration. + * + * @see CorePrefixFilter */ public CoreTypePool() { - //By default optimise for jakarta annotations, and java util collections - this("jakarta.", "java.", "org.hibernate.annotations."); + this( CorePrefixFilter.DEFAULT_INSTANCE ); } /** * Construct a new {@link CoreTypePool} with a choice of which prefixes * for fully qualified classnames will be loaded by this {@link TypePool}. + * + * @deprecated used by Quarkus */ + @Deprecated public CoreTypePool(final String... acceptedPrefixes) { + this( new CorePrefixFilter( acceptedPrefixes ) ); + } + + public CoreTypePool(CorePrefixFilter acceptedPrefixes) { //While we implement a cache in this class we also want to enable //ByteBuddy's default caching mechanism as it will cache the more //useful output of the parsing and introspection of such types. - super( new TypePool.CacheProvider.Simple() ); - this.acceptedPrefixes = acceptedPrefixes; - } - - private boolean isCoreClassName(final String name) { - for ( String acceptedPrefix : this.acceptedPrefixes ) { - if ( name.startsWith( acceptedPrefix ) ) { - return true; - } - } - return false; + super( new CoreCacheProvider( acceptedPrefixes ) ); + this.acceptedPrefixes = Objects.requireNonNull( acceptedPrefixes ); } @Override protected Resolution doDescribe(final String name) { - if ( isCoreClassName( name ) ) { + if ( acceptedPrefixes.isCoreClassName( name ) ) { final Resolution resolution = resolutions.get( name ); if ( resolution != null ) { return resolution; diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/EnhancerCacheProvider.java b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/EnhancerCacheProvider.java new file mode 100644 index 000000000000..dea753830635 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/EnhancerCacheProvider.java @@ -0,0 +1,66 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.bytecode.enhance.internal.bytebuddy; + +import net.bytebuddy.dynamic.ClassFileLocator; +import net.bytebuddy.pool.TypePool; + +/** + * A simple cache provider that allows overriding the resolution for the class that is currently being enhanced. + */ +final class EnhancerCacheProvider extends TypePool.CacheProvider.Simple { + + private final ThreadLocal enhancementState = new ThreadLocal<>(); + + @Override + public TypePool.Resolution find(final String name) { + final EnhancementState enhancementState = getEnhancementState(); + if ( enhancementState != null && enhancementState.getClassName().equals( name ) ) { + return enhancementState.getTypePoolResolution(); + } + return super.find( name ); + } + + EnhancementState getEnhancementState() { + return enhancementState.get(); + } + + void setEnhancementState(EnhancementState state) { + enhancementState.set( state ); + } + + void removeEnhancementState() { + enhancementState.remove(); + } + + static final class EnhancementState { + private final String className; + private final ClassFileLocator.Resolution classFileResolution; + private TypePool.Resolution typePoolResolution; + + public EnhancementState(String className, ClassFileLocator.Resolution classFileResolution) { + this.className = className; + this.classFileResolution = classFileResolution; + } + + public String getClassName() { + return className; + } + + public ClassFileLocator.Resolution getClassFileResolution() { + return classFileResolution; + } + + public TypePool.Resolution getTypePoolResolution() { + return typePoolResolution; + } + + public void setTypePoolResolution(TypePool.Resolution typePoolResolution) { + this.typePoolResolution = typePoolResolution; + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/EnhancerClassFileLocator.java b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/EnhancerClassFileLocator.java new file mode 100644 index 000000000000..1fa193479133 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/EnhancerClassFileLocator.java @@ -0,0 +1,40 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.bytecode.enhance.internal.bytebuddy; + +import net.bytebuddy.dynamic.ClassFileLocator; + +import java.io.IOException; + +/** + * A delegating ClassFileLocator that allows overriding the resolution for the class that is currently being enhanced. + */ +final class EnhancerClassFileLocator implements ClassFileLocator { + + private final EnhancerCacheProvider cacheProvider; + private final ClassFileLocator delegate; + + public EnhancerClassFileLocator(EnhancerCacheProvider cacheProvider, ClassFileLocator delegate) { + this.cacheProvider = cacheProvider; + this.delegate = delegate; + } + + @Override + public Resolution locate(final String name) throws IOException { + final EnhancerCacheProvider.EnhancementState enhancementState = cacheProvider.getEnhancementState(); + if ( enhancementState != null && enhancementState.getClassName().equals( name ) ) { + return enhancementState.getClassFileResolution(); + } + return delegate.locate( name ); + } + + @Override + public void close() throws IOException { + delegate.close(); + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/EnhancerImpl.java b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/EnhancerImpl.java index e696d1beb802..139de3f1b7e8 100644 --- a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/EnhancerImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/EnhancerImpl.java @@ -6,17 +6,35 @@ */ package org.hibernate.bytecode.enhance.internal.bytebuddy; -import java.lang.annotation.Annotation; -import java.lang.reflect.Modifier; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.function.Supplier; +import jakarta.persistence.Access; +import jakarta.persistence.AccessType; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.MappedSuperclass; +import jakarta.persistence.Transient; +import jakarta.persistence.metamodel.Type; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.annotation.AnnotationDescription; +import net.bytebuddy.description.annotation.AnnotationList; +import net.bytebuddy.description.annotation.AnnotationSource; +import net.bytebuddy.description.field.FieldDescription; +import net.bytebuddy.description.field.FieldDescription.InDefinedShape; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.method.MethodList; +import net.bytebuddy.description.type.TypeDefinition; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.description.type.TypeDescription.Generic; +import net.bytebuddy.dynamic.DynamicType; +import net.bytebuddy.dynamic.scaffold.MethodGraph; +import net.bytebuddy.implementation.FieldAccessor; +import net.bytebuddy.implementation.FixedValue; +import net.bytebuddy.implementation.Implementation; +import net.bytebuddy.implementation.StubMethod; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.hibernate.AssertionFailure; import org.hibernate.Version; import org.hibernate.bytecode.enhance.VersionMismatchException; import org.hibernate.bytecode.enhance.internal.tracker.CompositeOwnerTracker; @@ -27,6 +45,7 @@ import org.hibernate.bytecode.enhance.spi.Enhancer; import org.hibernate.bytecode.enhance.spi.EnhancerConstants; import org.hibernate.bytecode.enhance.spi.UnloadedField; +import org.hibernate.bytecode.enhance.spi.UnsupportedEnhancementStrategy; import org.hibernate.bytecode.enhance.spi.interceptor.LazyAttributeLoadingInterceptor; import org.hibernate.bytecode.internal.bytebuddy.ByteBuddyState; import org.hibernate.engine.spi.CompositeOwner; @@ -41,25 +60,24 @@ import org.hibernate.internal.CoreLogging; import org.hibernate.internal.CoreMessageLogger; -import jakarta.persistence.Access; -import jakarta.persistence.AccessType; -import jakarta.persistence.metamodel.Type; -import net.bytebuddy.asm.Advice; -import net.bytebuddy.description.annotation.AnnotationDescription; -import net.bytebuddy.description.annotation.AnnotationList; -import net.bytebuddy.description.field.FieldDescription; -import net.bytebuddy.description.field.FieldDescription.InDefinedShape; -import net.bytebuddy.description.method.MethodDescription; -import net.bytebuddy.description.type.TypeDefinition; -import net.bytebuddy.description.type.TypeDescription; -import net.bytebuddy.description.type.TypeDescription.Generic; -import net.bytebuddy.dynamic.DynamicType; -import net.bytebuddy.implementation.FieldAccessor; -import net.bytebuddy.implementation.FixedValue; -import net.bytebuddy.implementation.Implementation; -import net.bytebuddy.implementation.StubMethod; +import java.lang.annotation.Annotation; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Supplier; import static net.bytebuddy.matcher.ElementMatchers.isDefaultFinalizer; +import static net.bytebuddy.matcher.ElementMatchers.isGetter; +import static net.bytebuddy.matcher.ElementMatchers.isSetter; +import static net.bytebuddy.matcher.ElementMatchers.isStatic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.not; public class EnhancerImpl implements Enhancer { @@ -151,6 +169,18 @@ public void discoverTypes(String className, byte[] originalBytes) { } private DynamicType.Builder doEnhance(Supplier> builderSupplier, TypeDescription managedCtClass) { + // skip if the class was already enhanced. This is very common in WildFly as classloading is highly concurrent. + // We need to ensure that no class is instrumented multiple times as that might result in incorrect bytecode. + // N.B. there is a second check below using a different approach: checking for the marker interfaces, + // which does not address the case of extended bytecode enhancement + // (because it enhances classes that do not end up with these marker interfaces). + // I'm currently inclined to keep both checks, as one is safer and the other has better backwards compatibility. + if ( managedCtClass.getDeclaredAnnotations().isAnnotationPresent( EnhancementInfo.class ) ) { + verifyVersions( managedCtClass, enhancementContext ); + log.debugf( "Skipping enhancement of [%s]: it's already annotated with @EnhancementInfo", managedCtClass.getName() ); + return null; + } + // can't effectively enhance interfaces if ( managedCtClass.isInterface() ) { log.debugf( "Skipping enhancement of [%s]: it's an interface", managedCtClass.getName() ); @@ -167,11 +197,16 @@ private DynamicType.Builder doEnhance(Supplier> builde if ( alreadyEnhanced( managedCtClass ) ) { verifyVersions( managedCtClass, enhancementContext ); - log.debugf( "Skipping enhancement of [%s]: already enhanced", managedCtClass.getName() ); + log.debugf( "Skipping enhancement of [%s]: it's already implementing 'Managed'", managedCtClass.getName() ); return null; } if ( enhancementContext.isEntityClass( managedCtClass ) ) { + if ( checkUnsupportedAttributeNaming( managedCtClass, enhancementContext ) ) { + // do not enhance classes with mismatched names for PROPERTY-access persistent attributes + return null; + } + log.debugf( "Enhancing [%s] as Entity", managedCtClass.getName() ); DynamicType.Builder builder = builderSupplier.get(); builder = builder.implement( ManagedEntity.class ) @@ -331,6 +366,11 @@ private DynamicType.Builder doEnhance(Supplier> builde return createTransformer( managedCtClass ).applyTo( builder ); } else if ( enhancementContext.isCompositeClass( managedCtClass ) ) { + if ( checkUnsupportedAttributeNaming( managedCtClass, enhancementContext ) ) { + // do not enhance classes with mismatched names for PROPERTY-access persistent attributes + return null; + } + log.debugf( "Enhancing [%s] as Composite", managedCtClass.getName() ); DynamicType.Builder builder = builderSupplier.get(); @@ -364,6 +404,11 @@ else if ( enhancementContext.isCompositeClass( managedCtClass ) ) { return createTransformer( managedCtClass ).applyTo( builder ); } else if ( enhancementContext.isMappedSuperclassClass( managedCtClass ) ) { + // Check for HHH-16572 (PROPERTY attributes with mismatched field and method names) + if ( checkUnsupportedAttributeNaming( managedCtClass, enhancementContext ) ) { + return null; + } + log.debugf( "Enhancing [%s] as MappedSuperclass", managedCtClass.getName() ); DynamicType.Builder builder = builderSupplier.get(); @@ -380,6 +425,198 @@ else if ( enhancementContext.doExtendedEnhancement( managedCtClass ) ) { } } + /** + * Utility that determines the access-type of a mapped class based on an explicit annotation + * or guessing it from the placement of its identifier property. Implementation should be + * aligned with {@code InheritanceState#determineDefaultAccessType()}. + * + * @return the {@link AccessType} used by the mapped class + * + * @implNote this does not fully account for embeddables, as they should inherit the access-type + * from the entities they're used in - defaulting to PROPERTY to always run the accessor check + */ + private static AccessType determineDefaultAccessType(TypeDefinition typeDefinition) { + for ( TypeDefinition candidate = typeDefinition; candidate != null && !candidate.represents( Object.class ); candidate = candidate.getSuperClass() ) { + final AnnotationList annotations = candidate.asErasure().getDeclaredAnnotations(); + if ( hasMappingAnnotation( annotations ) ) { + final AnnotationDescription.Loadable access = annotations.ofType( Access.class ); + if ( access != null ) { + return access.load().value(); + } + } + } + // Guess from identifier. + // FIX: Shouldn't this be determined by the first attribute (i.e., field or property) with annotations, + // but without an explicit Access annotation, according to JPA 2.0 spec 2.3.1: Default Access Type? + for ( TypeDefinition candidate = typeDefinition; candidate != null && !candidate.represents( Object.class ); candidate = candidate.getSuperClass() ) { + final AnnotationList annotations = candidate.asErasure().getDeclaredAnnotations(); + if ( hasMappingAnnotation( annotations ) ) { + for ( FieldDescription ctField : candidate.getDeclaredFields() ) { + if ( !Modifier.isStatic( ctField.getModifiers() ) ) { + final AnnotationList annotationList = ctField.getDeclaredAnnotations(); + if ( annotationList.isAnnotationPresent( Id.class ) || annotationList.isAnnotationPresent( EmbeddedId.class ) ) { + return AccessType.FIELD; + } + } + } + } + } + // We can assume AccessType.PROPERTY here + return AccessType.PROPERTY; + } + + /** + * Determines the access-type of the given annotation source if an explicit {@link Access} annotation + * is present, otherwise defaults to the provided {@code defaultAccessType} + */ + private static AccessType determineAccessType(AnnotationSource annotationSource, AccessType defaultAccessType) { + final AnnotationDescription.Loadable access = annotationSource.getDeclaredAnnotations().ofType( Access.class ); + return access != null ? access.load().value() : defaultAccessType; + } + + private static boolean hasMappingAnnotation(AnnotationList annotations) { + return annotations.isAnnotationPresent( Entity.class ) + || annotations.isAnnotationPresent( MappedSuperclass.class ) + || annotations.isAnnotationPresent( Embeddable.class ); + } + + private static boolean isPersistentMethod(MethodDescription method) { + final AnnotationList annotations = method.getDeclaredAnnotations(); + if ( annotations.isAnnotationPresent( Transient.class ) ) { + return false; + } + + return annotations.stream().noneMatch( a -> IGNORED_PERSISTENCE_ANNOTATIONS.contains( a.getAnnotationType().getName() ) ); + } + + private static final Set IGNORED_PERSISTENCE_ANNOTATIONS = Set.of( + "jakarta.persistence.PostLoad", + "jakarta.persistence.PostPersist", + "jakarta.persistence.PostRemove", + "jakarta.persistence.PostUpdate", + "jakarta.persistence.PrePersist", + "jakarta.persistence.PreRemove", + "jakarta.persistence.PreUpdate" + ); + + private static boolean containsField(Generic type, String fieldName) { + do { + if ( !type.getDeclaredFields().filter( not( isStatic() ).and( named( fieldName ) ) ).isEmpty() ) { + return true; + } + type = type.getSuperClass(); + } + while ( type != null && !type.represents( Object.class ) ); + return false; + } + + /** + * Check whether an entity class ({@code managedCtClass}) has mismatched names between a persistent field and its + * getter/setter when using {@link AccessType#PROPERTY}, which Hibernate does not currently support for enhancement. + * See HHH-16572 + * + * @return {@code true} if enhancement of the class must be {@link UnsupportedEnhancementStrategy#SKIP skipped} + * because it has mismatched names. + * {@code false} if enhancement of the class must proceed, either because it doesn't have any mismatched names, + * or because {@link UnsupportedEnhancementStrategy#LEGACY legacy mode} was opted into. + * @throws EnhancementException if enhancement of the class must {@link UnsupportedEnhancementStrategy#FAIL abort} because it has mismatched names. + */ + @SuppressWarnings("deprecation") + private static boolean checkUnsupportedAttributeNaming(TypeDescription managedCtClass, ByteBuddyEnhancementContext enhancementContext) { + var strategy = enhancementContext.getUnsupportedEnhancementStrategy(); + if ( UnsupportedEnhancementStrategy.LEGACY.equals( strategy ) ) { + // Don't check anything and act as if there was nothing unsupported in the class. + // This is unsafe but that's what LEGACY is about. + return false; + } + + // For process access rules, See https://jakarta.ee/specifications/persistence/3.2/jakarta-persistence-spec-3.2#default-access-type + // and https://jakarta.ee/specifications/persistence/3.2/jakarta-persistence-spec-3.2#a122 + // + // This check will determine if entity field names do not match Property accessor method name + // For example: + // @Entity + // class Book { + // Integer id; + // String smtg; + // + // @Id Integer getId() { return id; } + // String getSomething() { return smtg; } + // } + // + // Check name of the getter/setter method with persistence annotation and getter/setter method name that doesn't refer to an entity field + // and will return false. If the property accessor method(s) are named to match the field name(s), return true. + final AccessType defaultAccessType = determineDefaultAccessType( managedCtClass ); + final MethodList methods = MethodGraph.Compiler.DEFAULT.compile( (TypeDefinition) managedCtClass ) + .listNodes() + .asMethodList() + .filter( isGetter().or( isSetter() ) ); + for ( final MethodDescription methodDescription : methods ) { + if ( methodDescription.getDeclaringType().represents( Object.class ) + || determineAccessType( methodDescription, defaultAccessType ) != AccessType.PROPERTY ) { + // We only need to check this for AccessType.PROPERTY + continue; + } + + final String methodName = methodDescription.getActualName(); + String fieldName; + if ( methodName.startsWith( "get" ) || methodName.startsWith( "set" ) ) { + fieldName = methodName.substring( 3 ); + } + else { + assert methodName.startsWith( "is" ); + fieldName = methodName.substring( 2 ); + } + // convert first field letter to lower case + fieldName = getJavaBeansFieldName( fieldName ); + if ( fieldName != null && isPersistentMethod( methodDescription ) + && !containsField( managedCtClass.asGenericType(), fieldName ) ) { + // We shouldn't even be in this method if using LEGACY, see top of this method. + switch ( strategy ) { + case SKIP: + log.debugf( + "Skipping enhancement of [%s] because no field named [%s] could be found for property accessor method [%s]." + + " To fix this, make sure all property accessor methods have a matching field.", + managedCtClass.getName(), + fieldName, + methodDescription.getName() + ); + return true; + case FAIL: + throw new EnhancementException( String.format( + "Enhancement of [%s] failed because no field named [%s] could be found for property accessor method [%s]." + + " To fix this, make sure all property accessor methods have a matching field.", + managedCtClass.getName(), + fieldName, + methodDescription.getName() + ) ); + default: + throw new AssertionFailure( "Unexpected strategy at this point: " + strategy ); + } + } + } + return false; + } + + /** + * If the first two characters are upper case, assume all characters are upper case to be returned as is. + * Otherwise, return the name with the first character converted to lower case and the remaining part returned as is. + * + * @param name is the property accessor name to be updated following Persistence property name rules. + * @return name that follows JavaBeans rules, or {@code null} if the provided string is empty + */ + private static @Nullable String getJavaBeansFieldName(String name) { + if ( name.isEmpty() ) { + return null; + } + if ( name.length() > 1 && Character.isUpperCase( name.charAt( 1 ) ) && Character.isUpperCase( name.charAt( 0 ) ) ) { + return name; + } + final char[] chars = name.toCharArray(); + chars[0] = Character.toLowerCase( chars[0] ); + return new String( chars ); + } + private static void verifyVersions(TypeDescription managedCtClass, ByteBuddyEnhancementContext enhancementContext) { final AnnotationDescription.Loadable existingInfo = managedCtClass .getDeclaredAnnotations() diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/InlineDirtyCheckingHandler.java b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/InlineDirtyCheckingHandler.java index 4e687362419d..f263f9a31b88 100644 --- a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/InlineDirtyCheckingHandler.java +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/InlineDirtyCheckingHandler.java @@ -78,8 +78,9 @@ else if ( !persistentField.hasAnnotation( Id.class ) } if ( enhancementContext.isCompositeField( persistentField ) - // Don't do composite owner tracking for records - && !persistentField.getType().isRecord() ) { + && !persistentField.hasAnnotation( EmbeddedId.class ) + // Don't do composite owner tracking for records + && !persistentField.getType().isRecord() ) { // HHH-13759 - Call getter on superclass if field is not visible // An embedded field won't be visible if declared private in a superclass diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/ModelTypePool.java b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/ModelTypePool.java index ecbc1ede0fea..7c40165a7a5c 100644 --- a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/ModelTypePool.java +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/ModelTypePool.java @@ -6,23 +6,23 @@ */ package org.hibernate.bytecode.enhance.internal.bytebuddy; -import java.util.Objects; -import java.util.concurrent.ConcurrentHashMap; - import net.bytebuddy.dynamic.ClassFileLocator; import net.bytebuddy.pool.TypePool; +import java.util.Objects; + /** * A TypePool suitable for loading user's classes, * potentially in parallel operations. */ public class ModelTypePool extends TypePool.Default implements EnhancerClassLocator { - private final ConcurrentHashMap resolutions = new ConcurrentHashMap<>(); - private final OverridingClassFileLocator locator; + private final EnhancerClassFileLocator locator; + private final EnhancerCacheProvider poolCache; - private ModelTypePool(CacheProvider cacheProvider, OverridingClassFileLocator classFileLocator, CoreTypePool parent) { + private ModelTypePool(EnhancerCacheProvider cacheProvider, EnhancerClassFileLocator classFileLocator, CoreTypePool parent) { super( cacheProvider, classFileLocator, ReaderMode.FAST, parent ); + this.poolCache = cacheProvider; this.locator = classFileLocator; } @@ -62,7 +62,7 @@ public static EnhancerClassLocator buildModelTypePool(ClassFileLocator classFile * @return */ public static EnhancerClassLocator buildModelTypePool(ClassFileLocator classFileLocator, CoreTypePool coreTypePool) { - return buildModelTypePool( classFileLocator, coreTypePool, new TypePool.CacheProvider.Simple() ); + return buildModelTypePool( classFileLocator, coreTypePool, new EnhancerCacheProvider() ); } /** @@ -72,37 +72,35 @@ public static EnhancerClassLocator buildModelTypePool(ClassFileLocator classFile * @param cacheProvider * @return */ - public static EnhancerClassLocator buildModelTypePool(ClassFileLocator classFileLocator, CoreTypePool coreTypePool, CacheProvider cacheProvider) { + static EnhancerClassLocator buildModelTypePool(ClassFileLocator classFileLocator, CoreTypePool coreTypePool, EnhancerCacheProvider cacheProvider) { Objects.requireNonNull( classFileLocator ); Objects.requireNonNull( coreTypePool ); Objects.requireNonNull( cacheProvider ); - return new ModelTypePool( cacheProvider, new OverridingClassFileLocator( classFileLocator ), coreTypePool ); - } - - @Override - protected Resolution doDescribe(final String name) { - final Resolution resolution = resolutions.get( name ); - if ( resolution != null ) { - return resolution; - } - else { - return resolutions.computeIfAbsent( name, super::doDescribe ); - } + return new ModelTypePool( cacheProvider, new EnhancerClassFileLocator( cacheProvider, classFileLocator ), coreTypePool ); } @Override public void registerClassNameAndBytes(final String className, final byte[] bytes) { - locator.put( className, new ClassFileLocator.Resolution.Explicit( Objects.requireNonNull( bytes ) ) ); + final EnhancerCacheProvider.EnhancementState currentEnhancementState = poolCache.getEnhancementState(); + if ( currentEnhancementState != null ) { + throw new IllegalStateException( "Re-entrant enhancement is not supported: " + className ); + } + final EnhancerCacheProvider.EnhancementState state = new EnhancerCacheProvider.EnhancementState( + className, + new ClassFileLocator.Resolution.Explicit( Objects.requireNonNull( bytes ) ) + ); + // Set the state first because the ClassFileLocator needs this in the doDescribe() call below + poolCache.setEnhancementState( state ); + state.setTypePoolResolution( doDescribe( className ) ); } @Override - public void deregisterClassNameAndBytes(final String className) { - locator.remove( className ); + public void deregisterClassNameAndBytes(String className) { + poolCache.removeEnhancementState(); } @Override public ClassFileLocator asClassFileLocator() { return locator; } - } diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/OverridingClassFileLocator.java b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/OverridingClassFileLocator.java deleted file mode 100644 index 2f64dabbdd70..000000000000 --- a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/OverridingClassFileLocator.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Hibernate, Relational Persistence for Idiomatic Java - * - * License: GNU Lesser General Public License (LGPL), version 2.1 or later. - * See the lgpl.txt file in the root directory or . - */ -package org.hibernate.bytecode.enhance.internal.bytebuddy; - -import java.io.IOException; -import java.util.Objects; -import java.util.concurrent.ConcurrentHashMap; - -import net.bytebuddy.dynamic.ClassFileLocator; - -/** - * Allows wrapping another ClassFileLocator to add the ability to define - * resolution overrides for specific resources. - */ -public final class OverridingClassFileLocator implements ClassFileLocator { - - private final ConcurrentHashMap registeredResolutions = new ConcurrentHashMap<>(); - private final ClassFileLocator parent; - - public OverridingClassFileLocator(final ClassFileLocator parent) { - this.parent = Objects.requireNonNull( parent ); - } - - @Override - public Resolution locate(final String name) throws IOException { - final Resolution resolution = registeredResolutions.get( name ); - if ( resolution != null ) { - return resolution; - } - else { - return parent.locate( name ); - } - } - - @Override - public void close() throws IOException { - //Nothing to do: we're not responsible for parent - } - - void put(String className, Resolution.Explicit explicit) { - registeredResolutions.put( className, explicit ); - } - - void remove(String className) { - registeredResolutions.remove( className ); - } - -} diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/PersistentAttributeTransformer.java b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/PersistentAttributeTransformer.java index 5457474a38c9..61b8976c7a93 100644 --- a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/PersistentAttributeTransformer.java +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/PersistentAttributeTransformer.java @@ -148,7 +148,10 @@ private static Collection collectInheritPersistentFie } TypeDefinition managedCtSuperclass = managedCtClass.getSuperClass(); - if ( enhancementContext.isEntityClass( managedCtSuperclass.asErasure() ) ) { + // If managedCtSuperclass is null, managedCtClass can be either interface or module-info. + // Interfaces are already filtered-out, and module-info does not have any fields to enhance + // so we can safely return empty list. + if ( managedCtSuperclass == null || enhancementContext.isEntityClass( managedCtSuperclass.asErasure() ) ) { return Collections.emptyList(); } else if ( !enhancementContext.isMappedSuperclassClass( managedCtSuperclass.asErasure() ) ) { diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/EnhancementContext.java b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/EnhancementContext.java index 06086c65d2e8..e35bac133693 100644 --- a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/EnhancementContext.java +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/EnhancementContext.java @@ -6,7 +6,10 @@ */ package org.hibernate.bytecode.enhance.spi; +import org.hibernate.bytecode.spi.BytecodeProvider; + import jakarta.persistence.metamodel.Type; +import org.hibernate.Incubating; /** * The context for performing an enhancement. Enhancement can happen in any number of ways:
    @@ -146,4 +149,26 @@ public interface EnhancementContext { boolean isDiscoveredType(UnloadedClass classDescriptor); void registerDiscoveredType(UnloadedClass classDescriptor, Type.PersistenceType type); + + /** + * @return The expected behavior when encountering a class that cannot be enhanced, + * in particular when attribute names don't match field names. + * @see HHH-16572 + * @see HHH-18833 + */ + @Incubating + default UnsupportedEnhancementStrategy getUnsupportedEnhancementStrategy() { + return UnsupportedEnhancementStrategy.SKIP; + } + + /** + * Allows to force the use of a specific instance of BytecodeProvider to perform the enhancement. + * @return When returning {code null} the default implementation will be used. Only return a different instance if + * you need to override the default implementation. + */ + @Incubating + default BytecodeProvider getBytecodeProvider() { + return null; + } + } diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/UnsupportedEnhancementStrategy.java b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/UnsupportedEnhancementStrategy.java new file mode 100644 index 000000000000..ce5c97883fa1 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/UnsupportedEnhancementStrategy.java @@ -0,0 +1,40 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.bytecode.enhance.spi; + +import org.hibernate.Incubating; + +/** + * The expected behavior when encountering a class that cannot be enhanced, + * in particular when attribute names don't match field names. + * + * @see org.hibernate.bytecode.enhance.spi.EnhancementContext#getUnsupportedEnhancementStrategy + */ +@Incubating +public enum UnsupportedEnhancementStrategy { + + /** + * When a class cannot be enhanced, skip enhancement for that class only. + */ + SKIP, + /** + * When a class cannot be enhanced, throw an exception with an actionable message. + */ + FAIL, + /** + * Legacy behavior: when a class cannot be enhanced, ignore that fact and try to enhance it anyway. + *

    + * This is utterly unsafe and may cause errors, unpredictable behavior, and data loss. + *

    + * Intended only for internal use in contexts with rigid backwards compatibility requirements. + * + * @deprecated Use {@link #SKIP} or {@link #FAIL} instead. + */ + @Deprecated + LEGACY + +} diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/interceptor/EnhancementAsProxyLazinessInterceptor.java b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/interceptor/EnhancementAsProxyLazinessInterceptor.java index c0b1f0ba36b2..1f54399d177a 100644 --- a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/interceptor/EnhancementAsProxyLazinessInterceptor.java +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/interceptor/EnhancementAsProxyLazinessInterceptor.java @@ -16,6 +16,7 @@ import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.metamodel.mapping.AttributeMapping; import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.type.CollectionType; import org.hibernate.type.CompositeType; import org.hibernate.type.Type; @@ -65,7 +66,7 @@ public EnhancementAsProxyLazinessInterceptor( collectionAttributeNames = new HashSet<>(); for ( int i = 0; i < propertyTypes.length; i++ ) { Type propertyType = propertyTypes[i]; - if ( propertyType.isCollectionType() ) { + if ( propertyType instanceof CollectionType ) { collectionAttributeNames.add( propertyNames[i] ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/interceptor/LazyAttributeDescriptor.java b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/interceptor/LazyAttributeDescriptor.java index 0a2b54aff487..8f0ba8647737 100644 --- a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/interceptor/LazyAttributeDescriptor.java +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/interceptor/LazyAttributeDescriptor.java @@ -7,6 +7,7 @@ package org.hibernate.bytecode.enhance.spi.interceptor; import org.hibernate.mapping.Property; +import org.hibernate.type.CollectionType; import org.hibernate.type.Type; /** @@ -21,7 +22,7 @@ public static LazyAttributeDescriptor from( int lazyIndex) { String fetchGroupName = property.getLazyGroup(); if ( fetchGroupName == null ) { - fetchGroupName = property.getType().isCollectionType() + fetchGroupName = property.getType() instanceof CollectionType ? property.getName() : "DEFAULT"; } diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/interceptor/LazyAttributeLoadingInterceptor.java b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/interceptor/LazyAttributeLoadingInterceptor.java index 92e9da93c5e8..bc8a58e3a0b6 100644 --- a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/interceptor/LazyAttributeLoadingInterceptor.java +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/interceptor/LazyAttributeLoadingInterceptor.java @@ -34,8 +34,9 @@ public class LazyAttributeLoadingInterceptor extends AbstractLazyLoadInterceptor private final Object identifier; //N.B. this Set needs to be treated as immutable - private final Set lazyFields; + private Set lazyFields; private Set initializedLazyFields; + private Set mutableLazyFields; public LazyAttributeLoadingInterceptor( String entityName, @@ -193,4 +194,15 @@ public Set getInitializedLazyAttributeNames() { return initializedLazyFields == null ? Collections.emptySet() : initializedLazyFields; } + public void addLazyFieldByGraph(String fieldName) { + if ( mutableLazyFields == null ) { + mutableLazyFields = new HashSet<>( lazyFields ); + lazyFields = mutableLazyFields; + } + mutableLazyFields.add( fieldName ); + } + + public void clearInitializedLazyFields() { + initializedLazyFields = null; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/internal/bytebuddy/BasicProxyFactoryImpl.java b/hibernate-core/src/main/java/org/hibernate/bytecode/internal/bytebuddy/BasicProxyFactoryImpl.java index ad8b9310144c..e6b26cd729ad 100644 --- a/hibernate-core/src/main/java/org/hibernate/bytecode/internal/bytebuddy/BasicProxyFactoryImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/internal/bytebuddy/BasicProxyFactoryImpl.java @@ -15,8 +15,6 @@ import org.hibernate.internal.util.collections.ArrayHelper; import org.hibernate.proxy.ProxyConfiguration; -import net.bytebuddy.NamingStrategy; -import net.bytebuddy.TypeCache; import net.bytebuddy.description.modifier.Visibility; import net.bytebuddy.dynamic.scaffold.subclass.ConstructorStrategy; @@ -40,13 +38,12 @@ public BasicProxyFactoryImpl(final Class superClass, final Class interfaceClass, } final Class superClassOrMainInterface = superClass != null ? superClass : interfaceClass; - final TypeCache.SimpleKey cacheKey = new TypeCache.SimpleKey( superClassOrMainInterface ); + final ByteBuddyState.ProxyDefinitionHelpers helpers = byteBuddyState.getProxyDefinitionHelpers(); + final String proxyClassName = superClassOrMainInterface.getName() + "$" + PROXY_NAMING_SUFFIX; - ByteBuddyState.ProxyDefinitionHelpers helpers = byteBuddyState.getProxyDefinitionHelpers(); - - this.proxyClass = byteBuddyState.loadBasicProxy( superClassOrMainInterface, cacheKey, byteBuddy -> + this.proxyClass = byteBuddyState.loadBasicProxy( superClassOrMainInterface, proxyClassName, (byteBuddy, namingStrategy) -> helpers.appendIgnoreAlsoAtEnd( byteBuddy - .with( new NamingStrategy.SuffixingRandom( PROXY_NAMING_SUFFIX, new NamingStrategy.SuffixingRandom.BaseNameResolver.ForFixedValue( superClassOrMainInterface.getName() ) ) ) + .with( namingStrategy ) .subclass( superClass == null ? Object.class : superClass, ConstructorStrategy.Default.DEFAULT_CONSTRUCTOR ) .implement( interfaceClass == null ? NO_INTERFACES : new Class[]{ interfaceClass } ) .defineField( ProxyConfiguration.INTERCEPTOR_FIELD_NAME, ProxyConfiguration.Interceptor.class, Visibility.PRIVATE ) diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/internal/bytebuddy/ByteBuddyState.java b/hibernate-core/src/main/java/org/hibernate/bytecode/internal/bytebuddy/ByteBuddyState.java index eb1bba33de3e..f9f2dd6afd2c 100644 --- a/hibernate-core/src/main/java/org/hibernate/bytecode/internal/bytebuddy/ByteBuddyState.java +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/internal/bytebuddy/ByteBuddyState.java @@ -25,8 +25,11 @@ import java.security.PrivilegedAction; import java.util.ArrayList; import java.util.List; +import java.util.function.BiFunction; import java.util.function.Function; +import net.bytebuddy.NamingStrategy; +import net.bytebuddy.description.type.TypeDescription; import org.hibernate.HibernateException; import org.hibernate.bytecode.enhance.internal.bytebuddy.EnhancerImplConstants; import org.hibernate.bytecode.enhance.spi.EnhancerConstants; @@ -109,7 +112,9 @@ public ByteBuddyState() { * @param cacheKey The cache key. * @param makeProxyFunction A function building the proxy. * @return The loaded proxy class. + * @deprecated Use {@link #loadProxy(Class, String, BiFunction)} instead. */ + @Deprecated(forRemoval = true, since = "6.6") public Class loadProxy(Class referenceClass, TypeCache.SimpleKey cacheKey, Function> makeProxyFunction) { return load( referenceClass, proxyCache, cacheKey, makeProxyFunction ); @@ -122,12 +127,40 @@ public Class loadProxy(Class referenceClass, TypeCache.SimpleKey cacheKey, * @param cacheKey The cache key. * @param makeProxyFunction A function building the proxy. * @return The loaded proxy class. + * @deprecated Use {@link #loadBasicProxy(Class, String, BiFunction)} instead. */ + @Deprecated(forRemoval = true, since = "6.6") Class loadBasicProxy(Class referenceClass, TypeCache.SimpleKey cacheKey, Function> makeProxyFunction) { return load( referenceClass, basicProxyCache, cacheKey, makeProxyFunction ); } + /** + * Load a proxy as generated by the {@link ProxyFactory}. + * + * @param referenceClass The main class to proxy - might be an interface. + * @param proxyClassName The proxy class name. + * @param makeProxyFunction A function building the proxy. + * @return The loaded proxy class. + */ + public Class loadProxy(Class referenceClass, String proxyClassName, + BiFunction> makeProxyFunction) { + return load( referenceClass, proxyClassName, makeProxyFunction ); + } + + /** + * Load a proxy as generated by the {@link BasicProxyFactory}. + * + * @param referenceClass The main class to proxy - might be an interface. + * @param proxyClassName The proxy class name. + * @param makeProxyFunction A function building the proxy. + * @return The loaded proxy class. + */ + Class loadBasicProxy(Class referenceClass, String proxyClassName, + BiFunction> makeProxyFunction) { + return load( referenceClass, proxyClassName, makeProxyFunction ); + } + /** * Load a class generated by ByteBuddy. * @@ -136,18 +169,9 @@ Class loadBasicProxy(Class referenceClass, TypeCache.SimpleKey cacheKey, * @return The loaded generated class. */ public Class load(Class referenceClass, Function> makeClassFunction) { - Unloaded result = - make( makeClassFunction.apply( byteBuddy ) ); - if (DEBUG) { - try { - result.saveIn( new File( System.getProperty( "java.io.tmpdir" ) + "/bytebuddy/" ) ); - } - catch (IOException e) { - LOG.warn( "Unable to save generated class %1$s", result.getTypeDescription().getName(), e ); - } - } - return result.load( referenceClass.getClassLoader(), resolveClassLoadingStrategy( referenceClass ) ) - .getLoaded(); + return make( makeClassFunction.apply( byteBuddy ) ) + .load( referenceClass.getClassLoader(), resolveClassLoadingStrategy( referenceClass ) ) + .getLoaded(); } /** @@ -195,6 +219,39 @@ void clearState() { basicProxyCache.clear(); } + /** + * Load a class generated by ByteBuddy. + * + * @param referenceClass The main class for which to create a class - might be an interface. + * @param className The name under which the class shall be created. + * @param makeClassFunction A function building the class. + * @return The loaded generated class. + */ + public Class load(Class referenceClass, String className, BiFunction> makeClassFunction) { + try { + return referenceClass.getClassLoader().loadClass( className ); + } + catch (ClassNotFoundException e) { + // Ignore + } + try { + return make( makeClassFunction.apply( byteBuddy, new FixedNamingStrategy( className ) ) ) + .load( + referenceClass.getClassLoader(), + resolveClassLoadingStrategy( referenceClass ) + ) + .getLoaded(); + } + catch (LinkageError e) { + try { + return referenceClass.getClassLoader().loadClass( className ); + } + catch (ClassNotFoundException ex) { + throw new RuntimeException( "Couldn't load or define class [" + className + "]", e ); + } + } + } + private Class load(Class referenceClass, TypeCache cache, TypeCache.SimpleKey cacheKey, Function> makeProxyFunction) { return cache.findOrInsert( @@ -445,4 +502,16 @@ private static ClassLoadingStrategy resolveClassLoadingStrategy(Cla } } + private static class FixedNamingStrategy extends NamingStrategy.AbstractBase { + private final String className; + + public FixedNamingStrategy(String className) { + this.className = className; + } + + @Override + protected String name(TypeDescription typeDescription) { + return className; + } + } } diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/internal/bytebuddy/BytecodeProviderImpl.java b/hibernate-core/src/main/java/org/hibernate/bytecode/internal/bytebuddy/BytecodeProviderImpl.java index 392793881c6d..4452acdda58d 100644 --- a/hibernate-core/src/main/java/org/hibernate/bytecode/internal/bytebuddy/BytecodeProviderImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/internal/bytebuddy/BytecodeProviderImpl.java @@ -12,10 +12,10 @@ import java.lang.reflect.Member; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; @@ -68,7 +68,6 @@ import net.bytebuddy.jar.asm.Type; import net.bytebuddy.matcher.ElementMatcher; import net.bytebuddy.matcher.ElementMatchers; -import net.bytebuddy.pool.TypePool; import org.checkerframework.checker.nullness.qual.Nullable; public class BytecodeProviderImpl implements BytecodeProvider { @@ -76,6 +75,7 @@ public class BytecodeProviderImpl implements BytecodeProvider { private static final String INSTANTIATOR_PROXY_NAMING_SUFFIX = "HibernateInstantiator"; private static final String OPTIMIZER_PROXY_NAMING_SUFFIX = "HibernateAccessOptimizer"; + private static final String OPTIMIZER_PROXY_BRIDGE_NAMING_SUFFIX = "HibernateAccessOptimizerBridge"; private static final ElementMatcher.Junction newInstanceMethodName = ElementMatchers.named( "newInstance" ); private static final ElementMatcher.Junction getPropertyValuesMethodName = ElementMatchers.named( @@ -149,11 +149,9 @@ public ReflectionOptimizer getReflectionOptimizer( fastClass = null; } else { - fastClass = byteBuddyState.load( clazz, byteBuddy -> byteBuddy - .with( new NamingStrategy.SuffixingRandom( - INSTANTIATOR_PROXY_NAMING_SUFFIX, - new NamingStrategy.SuffixingRandom.BaseNameResolver.ForFixedValue( clazz.getName() ) - ) ) + final String className = clazz.getName() + "$" + INSTANTIATOR_PROXY_NAMING_SUFFIX; + fastClass = byteBuddyState.load( clazz, className, (byteBuddy, namingStrategy) -> byteBuddy + .with( namingStrategy ) .subclass( ReflectionOptimizer.InstantiationOptimizer.class ) .method( newInstanceMethodName ) .intercept( MethodCall.construct( constructor ) ) @@ -213,11 +211,9 @@ public ReflectionOptimizer getReflectionOptimizer( fastClass = null; } else { - fastClass = byteBuddyState.load( clazz, byteBuddy -> byteBuddy - .with( new NamingStrategy.SuffixingRandom( - INSTANTIATOR_PROXY_NAMING_SUFFIX, - new NamingStrategy.SuffixingRandom.BaseNameResolver.ForFixedValue( clazz.getName() ) - ) ) + final String className = clazz.getName() + "$" + INSTANTIATOR_PROXY_NAMING_SUFFIX; + fastClass = byteBuddyState.load( clazz, className, (byteBuddy, namingStrategy) -> byteBuddy + .with( namingStrategy ) .subclass( ReflectionOptimizer.InstantiationOptimizer.class ) .method( newInstanceMethodName ) .intercept( MethodCall.construct( constructor ) ) @@ -238,23 +234,41 @@ public ReflectionOptimizer getReflectionOptimizer( return null; } - Class superClass = determineAccessOptimizerSuperClass( clazz, getters, setters ); - final String[] propertyNames = propertyAccessMap.keySet().toArray( new String[0] ); - final Class bulkAccessor = byteBuddyState.load( clazz, byteBuddy -> byteBuddy - .with( new NamingStrategy.SuffixingRandom( - OPTIMIZER_PROXY_NAMING_SUFFIX, - new NamingStrategy.SuffixingRandom.BaseNameResolver.ForFixedValue( clazz.getName() ) - ) ) - .subclass( superClass ) - .implement( ReflectionOptimizer.AccessOptimizer.class ) - .method( getPropertyValuesMethodName ) - .intercept( new Implementation.Simple( new GetPropertyValues( clazz, propertyNames, getters ) ) ) - .method( setPropertyValuesMethodName ) - .intercept( new Implementation.Simple( new SetPropertyValues( clazz, propertyNames, setters ) ) ) - .method( getPropertyNamesMethodName ) - .intercept( MethodCall.call( new CloningPropertyCall( propertyNames ) ) ) - ); + final Class superClass = determineAccessOptimizerSuperClass( clazz, propertyNames, getters, setters ); + + final String className = clazz.getName() + "$" + OPTIMIZER_PROXY_NAMING_SUFFIX + encodeName( propertyNames, getters, setters ); + final Class bulkAccessor; + if ( className.getBytes( StandardCharsets.UTF_8 ).length >= 0x10000 ) { + // The JVM has a 64K byte limit on class name length, so fallback to random name if encoding exceeds that + bulkAccessor = byteBuddyState.load( clazz, byteBuddy -> byteBuddy + .with( new NamingStrategy.SuffixingRandom( + OPTIMIZER_PROXY_NAMING_SUFFIX, + new NamingStrategy.SuffixingRandom.BaseNameResolver.ForFixedValue( clazz.getName() ) + ) ) + .subclass( superClass ) + .implement( ReflectionOptimizer.AccessOptimizer.class ) + .method( getPropertyValuesMethodName ) + .intercept( new Implementation.Simple( new GetPropertyValues( clazz, propertyNames, getters ) ) ) + .method( setPropertyValuesMethodName ) + .intercept( new Implementation.Simple( new SetPropertyValues( clazz, propertyNames, setters ) ) ) + .method( getPropertyNamesMethodName ) + .intercept( MethodCall.call( new CloningPropertyCall( propertyNames ) ) ) + ); + } + else { + bulkAccessor = byteBuddyState.load( clazz, className, (byteBuddy, namingStrategy) -> byteBuddy + .with( namingStrategy ) + .subclass( superClass ) + .implement( ReflectionOptimizer.AccessOptimizer.class ) + .method( getPropertyValuesMethodName ) + .intercept( new Implementation.Simple( new GetPropertyValues( clazz, propertyNames, getters ) ) ) + .method( setPropertyValuesMethodName ) + .intercept( new Implementation.Simple( new SetPropertyValues( clazz, propertyNames, setters ) ) ) + .method( getPropertyNamesMethodName ) + .intercept( MethodCall.call( new CloningPropertyCall( propertyNames ) ) ) + ); + } try { return new ReflectionOptimizerImpl( @@ -267,104 +281,90 @@ public ReflectionOptimizer getReflectionOptimizer( } } - private static class ForeignPackageClassInfo { + private static class BridgeMembersClassInfo { final Class clazz; + final List propertyNames = new ArrayList<>(); final List getters = new ArrayList<>(); final List setters = new ArrayList<>(); - public ForeignPackageClassInfo(Class clazz) { + public BridgeMembersClassInfo(Class clazz) { this.clazz = clazz; } } - private Class determineAccessOptimizerSuperClass(Class clazz, Member[] getters, Member[] setters) { + private Class determineAccessOptimizerSuperClass(Class clazz, String[] propertyNames, Member[] getters, Member[] setters) { if ( clazz.isInterface() ) { return Object.class; } - // generate access optimizer super classes for foreign package super classes that declare fields + // generate access optimizer super classes for super classes that declare members requiring bridge methods // each should declare protected static methods get_FIELDNAME(OWNER)/set_FIELDNAME(OWNER, TYPE) // which should be called then from within GetPropertyValues/SetPropertyValues // Since these super classes will be in the correct package, the package-private entity field access is fine - final List foreignPackageClassInfos = createForeignPackageClassInfos( clazz ); - for ( Iterator iterator = foreignPackageClassInfos.iterator(); iterator.hasNext(); ) { - final ForeignPackageClassInfo foreignPackageClassInfo = iterator.next(); - for ( int i = 0; i < getters.length; i++ ) { - final Member getter = getters[i]; - final Member setter = setters[i]; - if ( getter.getDeclaringClass() == foreignPackageClassInfo.clazz && !Modifier.isPublic( getter.getModifiers() ) ) { - foreignPackageClassInfo.getters.add( getter ); - } - if ( setter.getDeclaringClass() == foreignPackageClassInfo.clazz && !Modifier.isPublic( setter.getModifiers() ) ) { - foreignPackageClassInfo.setters.add( setter ); - } - } - if ( foreignPackageClassInfo.getters.isEmpty() && foreignPackageClassInfo.setters.isEmpty() ) { - iterator.remove(); - } - } + final List bridgeMembersClassInfos = createBridgeMembersClassInfos( clazz, getters, setters, propertyNames ); Class superClass = Object.class; - for ( int i = foreignPackageClassInfos.size() - 1; i >= 0; i-- ) { - final ForeignPackageClassInfo foreignPackageClassInfo = foreignPackageClassInfos.get( i ); + for ( int i = bridgeMembersClassInfos.size() - 1; i >= 0; i-- ) { + final BridgeMembersClassInfo bridgeMembersClassInfo = bridgeMembersClassInfos.get( i ); final Class newSuperClass = superClass; + + final String className = bridgeMembersClassInfo.clazz.getName() + "$" + OPTIMIZER_PROXY_BRIDGE_NAMING_SUFFIX + encodeName( bridgeMembersClassInfo.propertyNames, bridgeMembersClassInfo.getters, bridgeMembersClassInfo.setters ); superClass = byteBuddyState.load( - foreignPackageClassInfo.clazz, - byteBuddy -> { - DynamicType.Builder builder = byteBuddy.with( - new NamingStrategy.SuffixingRandom( - OPTIMIZER_PROXY_NAMING_SUFFIX, - new NamingStrategy.SuffixingRandom.BaseNameResolver.ForFixedValue( - foreignPackageClassInfo.clazz.getName() ) - ) - ).subclass( newSuperClass ); - for ( Member getter : foreignPackageClassInfo.getters ) { - final Class getterType; - if ( getter instanceof Field ) { - getterType = ( (Field) getter ).getType(); + bridgeMembersClassInfo.clazz, + className, + (byteBuddy, namingStrategy) -> { + DynamicType.Builder builder = byteBuddy.with( namingStrategy ).subclass( newSuperClass ); + for ( Member getter : bridgeMembersClassInfo.getters ) { + if ( !Modifier.isPublic( getter.getModifiers() ) ) { + final Class getterType; + if ( getter instanceof Field ) { + getterType = ( (Field) getter ).getType(); + } + else { + getterType = ( (Method) getter ).getReturnType(); + } + + builder = builder.defineMethod( + "get_" + getter.getName(), + TypeDescription.Generic.OfNonGenericType.ForLoadedType.of( + getterType + ), + Opcodes.ACC_PROTECTED | Opcodes.ACC_STATIC + ) + .withParameter( bridgeMembersClassInfo.clazz ) + .intercept( + new Implementation.Simple( + new GetFieldOnArgument( + getter + ) + ) + ); } - else { - getterType = ( (Method) getter ).getReturnType(); - } - - builder = builder.defineMethod( - "get_" + getter.getName(), - TypeDescription.Generic.OfNonGenericType.ForLoadedType.of( - getterType - ), - Opcodes.ACC_PROTECTED | Opcodes.ACC_STATIC - ) - .withParameter( foreignPackageClassInfo.clazz ) - .intercept( - new Implementation.Simple( - new GetFieldOnArgument( - getter - ) - ) - ); } - for ( Member setter : foreignPackageClassInfo.setters ) { - final Class setterType; - if ( setter instanceof Field ) { - setterType = ( (Field) setter ).getType(); + for ( Member setter : bridgeMembersClassInfo.setters ) { + if ( !Modifier.isPublic( setter.getModifiers() ) ) { + final Class setterType; + if ( setter instanceof Field ) { + setterType = ( (Field) setter ).getType(); + } + else { + setterType = ( (Method) setter ).getParameterTypes()[0]; + } + + builder = builder.defineMethod( + "set_" + setter.getName(), + TypeDescription.Generic.VOID, + Opcodes.ACC_PROTECTED | Opcodes.ACC_STATIC + ) + .withParameter( bridgeMembersClassInfo.clazz ) + .withParameter( setterType ) + .intercept( + new Implementation.Simple( + new SetFieldOnArgument( + setter + ) + ) + ); } - else { - setterType = ( (Method) setter ).getParameterTypes()[0]; - } - - builder = builder.defineMethod( - "set_" + setter.getName(), - TypeDescription.Generic.VOID, - Opcodes.ACC_PROTECTED | Opcodes.ACC_STATIC - ) - .withParameter( foreignPackageClassInfo.clazz ) - .withParameter( setterType ) - .intercept( - new Implementation.Simple( - new SetFieldOnArgument( - setter - ) - ) - ); } return builder; @@ -374,10 +374,10 @@ private Class determineAccessOptimizerSuperClass(Class clazz, Member[] get for ( int j = 0; j < getters.length; j++ ) { final Member getter = getters[j]; final Member setter = setters[j]; - if ( foreignPackageClassInfo.getters.contains( getter ) ) { + if ( bridgeMembersClassInfo.getters.contains( getter ) && !Modifier.isPublic( getter.getModifiers() ) ) { getters[j] = new ForeignPackageMember( superClass, getter ); } - if ( foreignPackageClassInfo.setters.contains( setter ) ) { + if ( bridgeMembersClassInfo.setters.contains( setter ) && !Modifier.isPublic( setter.getModifiers() ) ) { setters[j] = new ForeignPackageMember( superClass, setter ); } } @@ -386,6 +386,42 @@ private Class determineAccessOptimizerSuperClass(Class clazz, Member[] get return superClass; } + private static String encodeName(String[] propertyNames, Member[] getters, Member[] setters) { + return encodeName( Arrays.asList( propertyNames ), Arrays.asList( getters ), Arrays.asList( setters ) ); + } + + private static String encodeName(List propertyNames, List getters, List setters) { + final StringBuilder sb = new StringBuilder(); + for ( int i = 0; i < propertyNames.size(); i++ ) { + final String propertyName = propertyNames.get( i ); + final Member getter = getters.get( i ); + final Member setter = setters.get( i ); + // Encode the two member types as 4 bit integer encoded as hex character + sb.append( Integer.toHexString( getKind( getter ) << 2 | getKind( setter ) ) ); + sb.append( propertyName ); + } + return sb.toString(); + } + + private static int getKind(Member member) { + // Encode the member type as 2 bit integer + if ( member == EMBEDDED_MEMBER ) { + return 0; + } + else if ( member instanceof Field ) { + return 1; + } + else if ( member instanceof Method ) { + return 2; + } + else if ( member instanceof ForeignPackageMember ) { + return 3; + } + else { + throw new IllegalArgumentException( "Unknown member type: " + member ); + } + } + private static class ForeignPackageMember implements Member { private final Class foreignPackageAccessor; @@ -565,16 +601,31 @@ private boolean is64BitType(Class type) { } } - private List createForeignPackageClassInfos(Class clazz) { - final List foreignPackageClassInfos = new ArrayList<>(); + private List createBridgeMembersClassInfos( + Class clazz, + Member[] getters, + Member[] setters, + String[] propertyNames) { + final List bridgeMembersClassInfos = new ArrayList<>(); Class c = clazz.getSuperclass(); while (c != Object.class) { - if ( !c.getPackageName().equals( clazz.getPackageName() ) ) { - foreignPackageClassInfos.add( new ForeignPackageClassInfo( c ) ); + final BridgeMembersClassInfo bridgeMemberClassInfo = new BridgeMembersClassInfo( c ); + for ( int i = 0; i < getters.length; i++ ) { + final Member getter = getters[i]; + final Member setter = setters[i]; + if ( getter.getDeclaringClass() == c && !Modifier.isPublic( getter.getModifiers() ) + || setter.getDeclaringClass() == c && !Modifier.isPublic( setter.getModifiers() ) ) { + bridgeMemberClassInfo.getters.add( getter ); + bridgeMemberClassInfo.setters.add( setter ); + bridgeMemberClassInfo.propertyNames.add( propertyNames[i] ); + } + } + if ( !bridgeMemberClassInfo.propertyNames.isEmpty() ) { + bridgeMembersClassInfos.add( bridgeMemberClassInfo ); } c = c.getSuperclass(); } - return foreignPackageClassInfos; + return bridgeMembersClassInfos; } public ByteBuddyProxyHelper getByteBuddyProxyHelper() { @@ -1213,7 +1264,7 @@ private static void findAccessors( getterMember = getter.getMethod(); } else if ( getter instanceof GetterFieldImpl ) { - getterMember = getter.getMember(); + getterMember = ((GetterFieldImpl) getter).getField(); } else { throw new InvalidPropertyAccessorException( @@ -1317,7 +1368,7 @@ public String[] call() { /** * Similar to {@link #getEnhancer(EnhancementContext)} but intended for advanced users who wish * to customize how ByteBuddy is locating the class files and caching the types. - * Possibly used in Quarkus in a future version. + * Used in Quarkus. * @param enhancementContext * @param classLocator * @return diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/BytecodeSettings.java b/hibernate-core/src/main/java/org/hibernate/cfg/BytecodeSettings.java index df5d7801e2da..474f0be1f2db 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/BytecodeSettings.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/BytecodeSettings.java @@ -28,6 +28,17 @@ public interface BytecodeSettings { @Deprecated( forRemoval = true ) String BYTECODE_PROVIDER = "hibernate.bytecode.provider"; + /** + * This is similar to the now deprecated legacy property {@code hibernate.bytecode.provider} except + * it's used specifically to pass an existing instance of a {@link org.hibernate.bytecode.spi.BytecodeProvider}; + * this happens to also allow to override the implementation, but is primarily intended to allow reusing a + * specific instance; this could be useful when the implementation benefits from internal caches. + * When not set, Hibernate will create its default implementation. + * + * @settingDefault {@code null} + */ + String BYTECODE_PROVIDER_INSTANCE = "hibernate.enhancer.bytecodeprovider.instance"; + /** * Enable association management feature in runtime bytecode enhancement * diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/Compatibility.java b/hibernate-core/src/main/java/org/hibernate/cfg/Compatibility.java new file mode 100644 index 000000000000..812a18c8335d --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/cfg/Compatibility.java @@ -0,0 +1,25 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.cfg; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.annotation.Retention; + +/** + * Denotes that a setting is intended to allow applications to upgrade + * versions of Hibernate and maintain backwards compatibility with the + * older version in some specific behavior. Such settings are almost always + * considered temporary and are usually also {@linkplain Deprecated deprecated}. + */ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.CLASS) +@Documented +public @interface Compatibility { +} diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/QuerySettings.java b/hibernate-core/src/main/java/org/hibernate/cfg/QuerySettings.java index 08d9aa70d8d7..cefbf61849b0 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/QuerySettings.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/QuerySettings.java @@ -23,6 +23,8 @@ public interface QuerySettings { * databases. By default, integer division in HQL can produce a non-integer * on Oracle, MySQL, or MariaDB. * + * @settingDefault {@code false} + * * @since 6.5 */ String PORTABLE_INTEGER_DIVISION = "hibernate.query.hql.portable_integer_division"; @@ -53,10 +55,10 @@ public interface QuerySettings { /** * When enabled, specifies that named queries be checked during startup. *

    - * By default, named queries are checked at startup. - *

    * Mainly intended for use in test environments. * + * @settingDefault {@code true} (enabled) - named queries are checked at startup. + * * @see org.hibernate.boot.SessionFactoryBuilder#applyNamedQueryCheckingOnStartup(boolean) */ String QUERY_STARTUP_CHECKING = "hibernate.query.startup_check"; @@ -85,8 +87,8 @@ public interface QuerySettings { * {@link jakarta.persistence.Query#setParameter(jakarta.persistence.Parameter,Object)} * to specify its argument are passed to JDBC using a bind parameter. *

- *

- * The default mode is {@link org.hibernate.query.criteria.ValueHandlingMode#BIND}. + * + * @settingDefault {@link org.hibernate.query.criteria.ValueHandlingMode#BIND}. * * @since 6.0.0 * @@ -101,8 +103,8 @@ public interface QuerySettings { * Specifies the default {@linkplain NullPrecedence precedence of null values} in the * HQL {@code ORDER BY} clause, either {@code none}, {@code first}, or {@code last}, * or an instance of {@link NullPrecedence}. - *

- * The default is {@code none}. + * + * @settingDefault {@code none}. * * @see NullPrecedence * @see org.hibernate.boot.SessionFactoryBuilder#applyDefaultNullPrecedence(NullPrecedence) @@ -135,10 +137,10 @@ public interface QuerySettings { String CRITERIA_COPY_TREE = "hibernate.criteria.copy_tree"; /** - * When set to true, indicates that ordinal parameters (represented by the '?' placeholder) in native queries will be ignored. - *

- * By default, this is set to false, i.e. native queries will be checked for ordinal placeholders. - *

+ * When enabled, ordinal parameters (represented by the {@code ?} placeholder) in + * native queries will be ignored. + * + * @settingDefault {@code false} (disabled) - native queries are checked for ordinal placeholders. * * @see SessionFactoryOptions#getNativeJdbcParametersIgnored() */ @@ -152,9 +154,9 @@ public interface QuerySettings { *

* When enabled, this setting specifies that an exception should be thrown for any * query which would result in the limit being applied in-memory. - *

- * By default, the exception is disabled, and the possibility of terrible - * performance is left as a problem for the client to avoid. + * + * @settingDefault {@code false} (disabled) - no exception is thrown and the + * possibility of terrible performance is left as a problem for the client to avoid. * * @since 5.2.13 */ @@ -172,8 +174,8 @@ public interface QuerySettings { *

  • {@link org.hibernate.query.ImmutableEntityUpdateQueryHandlingMode#EXCEPTION "exception"} * specifies that a {@link org.hibernate.HibernateException} should be thrown. * - *

    - * By default, a warning is logged. + * + * @settingDefault {@link org.hibernate.query.ImmutableEntityUpdateQueryHandlingMode#WARNING "warning"} * * @since 5.2.17 * diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/SchemaToolingSettings.java b/hibernate-core/src/main/java/org/hibernate/cfg/SchemaToolingSettings.java index 86f7a9153076..39c52f577131 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/SchemaToolingSettings.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/SchemaToolingSettings.java @@ -391,6 +391,14 @@ public interface SchemaToolingSettings { @Deprecated String HBM2DDL_IMPORT_FILES = "hibernate.hbm2ddl.import_files"; + /** + * Specifies if the default {@code /import.sql} script file should not be executed + * when {@link #HBM2DDL_IMPORT_FILES} is not specified and {@value #HBM2DDL_AUTO} is set to {@code create} or {@code create-drop}. + * + * The default value is {@code false}. + */ + String HBM2DDL_SKIP_DEFAULT_IMPORT_FILE = "hibernate.hbm2ddl.skip_default_import_file"; + /** * Specifies whether to automatically create also the database schema/catalog. * The default is false. diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/TransactionSettings.java b/hibernate-core/src/main/java/org/hibernate/cfg/TransactionSettings.java index 94d284499b23..6783f87eb9f7 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/TransactionSettings.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/TransactionSettings.java @@ -6,6 +6,7 @@ */ package org.hibernate.cfg; + import jakarta.persistence.spi.PersistenceUnitInfo; /** @@ -129,6 +130,26 @@ public interface TransactionSettings { */ String ALLOW_JTA_TRANSACTION_ACCESS = "hibernate.jta.allowTransactionAccess"; + /** + * When enabled, specifies that the {@link org.hibernate.Session} should be + * closed automatically at the end of each transaction. + * + * @settingDefault {@code false} + * + * @see org.hibernate.boot.SessionFactoryBuilder#applyAutoClosing(boolean) + */ + String AUTO_CLOSE_SESSION = "hibernate.transaction.auto_close_session"; + + /** + * When enabled, specifies that automatic flushing should occur during the JTA + * {@link jakarta.transaction.Synchronization#beforeCompletion()} callback. + * + * @settingDefault {@code true} unless using JPA bootstrap + * + * @see org.hibernate.boot.SessionFactoryBuilder#applyAutoFlushing(boolean) + */ + String FLUSH_BEFORE_COMPLETION = "hibernate.transaction.flush_before_completion"; + /** * Allows a detached proxy or lazy collection to be fetched even when not * associated with an open persistence context, by creating a temporary @@ -138,8 +159,11 @@ public interface TransactionSettings { * * @settingDefault {@code false} (disabled) * + * @apiNote Generally speaking, all access to transactional data should be done in a transaction. + * * @see org.hibernate.boot.SessionFactoryBuilder#applyLazyInitializationOutsideTransaction(boolean) */ + @Unsafe String ENABLE_LAZY_LOAD_NO_TRANS = "hibernate.enable_lazy_load_no_trans"; /** @@ -153,29 +177,15 @@ public interface TransactionSettings { *

    * The default behavior is to disallow update operations outside a transaction. * + * @settingDefault {@code false} (disabled) + * + * @apiNote Generally speaking, all access to transactional data should be done in a transaction. + * Combining this with second-level caching, e.g., will cause problems. + * * @see org.hibernate.boot.SessionFactoryBuilder#allowOutOfTransactionUpdateOperations(boolean) * * @since 5.2 */ + @Unsafe String ALLOW_UPDATE_OUTSIDE_TRANSACTION = "hibernate.allow_update_outside_transaction"; - - /** - * When enabled, specifies that the {@link org.hibernate.Session} should be - * closed automatically at the end of each transaction. - * - * @settingDefault {@code false} - * - * @see org.hibernate.boot.SessionFactoryBuilder#applyAutoClosing(boolean) - */ - String AUTO_CLOSE_SESSION = "hibernate.transaction.auto_close_session"; - - /** - * When enabled, specifies that automatic flushing should occur during the JTA - * {@link jakarta.transaction.Synchronization#beforeCompletion()} callback. - * - * @settingDefault {@code true} unless using JPA bootstrap - * - * @see org.hibernate.boot.SessionFactoryBuilder#applyAutoFlushing(boolean) - */ - String FLUSH_BEFORE_COMPLETION = "hibernate.transaction.flush_before_completion"; } diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/Unsafe.java b/hibernate-core/src/main/java/org/hibernate/cfg/Unsafe.java new file mode 100644 index 000000000000..ca9056957d72 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/cfg/Unsafe.java @@ -0,0 +1,24 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.cfg; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.annotation.Retention; + +/** + * Denotes that a setting is considered unsafe. Generally these are settings + * added for temporary use during porting of applications. Unsafe settings + * are largely considered unsupported. + */ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.CLASS) +@Documented +public @interface Unsafe { +} diff --git a/hibernate-core/src/main/java/org/hibernate/collection/spi/AbstractPersistentCollection.java b/hibernate-core/src/main/java/org/hibernate/collection/spi/AbstractPersistentCollection.java index 1df1aed42822..f90dde51591c 100644 --- a/hibernate-core/src/main/java/org/hibernate/collection/spi/AbstractPersistentCollection.java +++ b/hibernate-core/src/main/java/org/hibernate/collection/spi/AbstractPersistentCollection.java @@ -238,31 +238,30 @@ else if ( !session.isConnected() ) { SharedSessionContractImplementor originalSession = null; boolean isJTA = false; + try { + if ( tempSession != null ) { + isTempSession = true; + originalSession = session; + session = tempSession; + + isJTA = session.getTransactionCoordinator().getTransactionCoordinatorBuilder().isJta(); + + if ( !isJTA ) { + // Explicitly handle the transactions only if we're not in + // a JTA environment. A lazy loading temporary session can + // be created even if a current session and transaction are + // open (ex: session.clear() was used). We must prevent + // multiple transactions. + session.beginTransaction(); + } - if ( tempSession != null ) { - isTempSession = true; - originalSession = session; - session = tempSession; - - isJTA = session.getTransactionCoordinator().getTransactionCoordinatorBuilder().isJta(); - - if ( !isJTA ) { - // Explicitly handle the transactions only if we're not in - // a JTA environment. A lazy loading temporary session can - // be created even if a current session and transaction are - // open (ex: session.clear() was used). We must prevent - // multiple transactions. - session.beginTransaction(); + final CollectionPersister collectionDescriptor = + session.getSessionFactory() + .getMappingMetamodel() + .getCollectionDescriptor( getRole() ); + session.getPersistenceContextInternal() + .addUninitializedDetachedCollection( collectionDescriptor, this ); } - - final CollectionPersister collectionDescriptor = - session.getSessionFactory() - .getMappingMetamodel() - .getCollectionDescriptor( getRole() ); - session.getPersistenceContextInternal().addUninitializedDetachedCollection( collectionDescriptor, this ); - } - - try { return lazyInitializationWork.doWork(); } finally { @@ -1298,8 +1297,8 @@ protected static Collection getOrphans( // iterate over the *old* list for ( E old : oldElements ) { if ( !currentSaving.contains( old ) ) { - final Object oldId = ForeignKeys.getEntityIdentifierIfNotUnsaved( entityName, old, session ); - if ( !currentIds.contains( useIdDirect ? oldId : new TypedValue( idType, oldId ) ) ) { + final Object oldId = ForeignKeys.getEntityIdentifier( entityName, old, session ); + if ( oldId != null && !currentIds.contains( useIdDirect ? oldId : new TypedValue( idType, oldId ) ) ) { res.add( old ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/collection/spi/PersistentArrayHolder.java b/hibernate-core/src/main/java/org/hibernate/collection/spi/PersistentArrayHolder.java index 778dce73413e..4e718070bb5a 100644 --- a/hibernate-core/src/main/java/org/hibernate/collection/spi/PersistentArrayHolder.java +++ b/hibernate-core/src/main/java/org/hibernate/collection/spi/PersistentArrayHolder.java @@ -9,6 +9,7 @@ import java.io.Serializable; import java.lang.reflect.Array; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Iterator; @@ -94,6 +95,9 @@ public boolean isSnapshotEmpty(Serializable snapshot) { public Collection getOrphans(Serializable snapshot, String entityName) throws HibernateException { final Object[] sn = (Object[]) snapshot; final Object[] arr = (Object[]) array; + if ( arr.length == 0 ) { + return Arrays.asList( sn ); + } final ArrayList result = new ArrayList(); Collections.addAll( result, sn ); for ( int i=0; i returningColumns) { // For DB2 we use #renderReturningClause to render a wrapper around the DML statement diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/DB2iDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/DB2iDialect.java index 3b0a9fcc1212..e52bf220646d 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DB2iDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DB2iDialect.java @@ -6,6 +6,7 @@ */ package org.hibernate.dialect; +import org.hibernate.LockOptions; import org.hibernate.boot.model.FunctionContributions; import org.hibernate.dialect.function.CommonFunctionFactory; import org.hibernate.dialect.identity.DB2IdentityColumnSupport; @@ -41,6 +42,9 @@ public class DB2iDialect extends DB2Dialect { private final static DatabaseVersion MINIMUM_VERSION = DatabaseVersion.make( 7, 1 ); final static DatabaseVersion DB2_LUW_VERSION = DB2Dialect.MINIMUM_VERSION; + private static final String FOR_UPDATE_SQL = " for update with rs"; + private static final String FOR_UPDATE_SKIP_LOCKED_SQL = FOR_UPDATE_SQL + " skip locked data"; + public DB2iDialect(DialectResolutionInfo info) { this( info.makeCopyOrDefault( MINIMUM_VERSION ) ); registerKeywords( info ); @@ -124,6 +128,7 @@ public String getQuerySequencesString() { } } + @Override public LimitHandler getLimitHandler() { return getVersion().isSameOrAfter(7, 3) @@ -171,4 +176,35 @@ public int rowIdSqlType() { public String getRowIdColumnString(String rowId) { return rowId( rowId ) + " rowid not null generated always"; } + + @Override + public String getForUpdateString() { + return FOR_UPDATE_SQL; + } + + @Override + public String getForUpdateSkipLockedString() { + return supportsSkipLocked() + ? FOR_UPDATE_SKIP_LOCKED_SQL + : FOR_UPDATE_SQL; + } + + @Override + public String getForUpdateSkipLockedString(String aliases) { + return getForUpdateSkipLockedString(); + } + + @Override + public String getWriteLockString(int timeout) { + return timeout == LockOptions.SKIP_LOCKED && supportsSkipLocked() + ? FOR_UPDATE_SKIP_LOCKED_SQL + : FOR_UPDATE_SQL; + } + + @Override + public String getReadLockString(int timeout) { + return timeout == LockOptions.SKIP_LOCKED && supportsSkipLocked() + ? FOR_UPDATE_SKIP_LOCKED_SQL + : FOR_UPDATE_SQL; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/DB2iSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/DB2iSqlAstTranslator.java index 0ea3a26bfabf..3abf55648652 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DB2iSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DB2iSqlAstTranslator.java @@ -60,4 +60,9 @@ protected void renderComparison(Expression lhs, ComparisonOperator operator, Exp public DatabaseVersion getDB2Version() { return DB2_LUW_VERSION; } + + @Override + protected String getForUpdate() { + return " for update with rs"; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/DerbyDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/DerbyDialect.java index d4a5e9ff15ce..1b0ddc2fee2b 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DerbyDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DerbyDialect.java @@ -1040,6 +1040,11 @@ public boolean supportsWindowFunctions() { return true; } + @Override + public boolean supportsValuesList() { + return true; + } + @Override public IdentifierHelper buildIdentifierHelper(IdentifierHelperBuilder builder, DatabaseMetaData dbMetaData) throws SQLException { @@ -1057,4 +1062,14 @@ public DmlTargetColumnQualifierSupport getDmlTargetColumnQualifierSupport() { return DmlTargetColumnQualifierSupport.TABLE_ALIAS; } + @Override + public String getDual() { + return "(values 0)"; + } + + @Override + public String getFromDualForSelectOnly() { + return " from " + getDual() + " dual"; + } + } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/DerbySqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/DerbySqlAstTranslator.java index 2788fca3a688..a830f79ada6c 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DerbySqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DerbySqlAstTranslator.java @@ -302,16 +302,6 @@ protected boolean supportsRowValueConstructorSyntaxInQuantifiedPredicates() { return false; } - @Override - protected String getDual() { - return "(values 0)"; - } - - @Override - protected String getFromDualForSelectOnly() { - return " from " + getDual() + " dual"; - } - @Override protected boolean needsRowsToSkip() { return !supportsOffsetFetchClause(); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java index 597c2d248677..1d686d28dacd 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java @@ -1441,7 +1441,7 @@ public String castPattern(CastType from, CastType to) { case INTEGER_BOOLEAN: switch ( from ) { case STRING: - return "case ?1 when 'T' then 1 when 'Y' then 1 when 'F' then 0 when 'N' then 0 else null end"; + return buildStringToBooleanCast( "1", "0" ); case INTEGER: case LONG: return "abs(sign(?1))"; @@ -1456,7 +1456,7 @@ public String castPattern(CastType from, CastType to) { case YN_BOOLEAN: switch ( from ) { case STRING: - return "case ?1 when 'T' then 'Y' when 'Y' then 'Y' when 'F' then 'N' when 'N' then 'N' else null end"; + return buildStringToBooleanCast( "'Y'", "'N'" ); case INTEGER_BOOLEAN: return "case ?1 when 1 then 'Y' when 0 then 'N' else null end"; case INTEGER: @@ -1471,7 +1471,7 @@ public String castPattern(CastType from, CastType to) { case TF_BOOLEAN: switch ( from ) { case STRING: - return "case ?1 when 'T' then 'T' when 'Y' then 'T' when 'F' then 'F' when 'N' then 'F' else null end"; + return buildStringToBooleanCast( "'T'", "'F'" ); case INTEGER_BOOLEAN: return "case ?1 when 1 then 'T' when 0 then 'F' else null end"; case INTEGER: @@ -1486,7 +1486,7 @@ public String castPattern(CastType from, CastType to) { case BOOLEAN: switch ( from ) { case STRING: - return "case ?1 when 'T' then true when 'Y' then true when 'F' then false when 'N' then false else null end"; + return buildStringToBooleanCast( "true", "false" ); case INTEGER_BOOLEAN: case INTEGER: case LONG: @@ -1501,6 +1501,153 @@ public String castPattern(CastType from, CastType to) { return "cast(?1 as ?2)"; } + protected static final String[] TRUE_STRING_VALUES = new String[] { "t", "true", "y", "1" }; + protected static final String[] FALSE_STRING_VALUES = new String[] { "f", "false", "n", "0" }; + + protected String buildStringToBooleanCast(String trueValue, String falseValue) { + final boolean supportsValuesList = supportsValuesList(); + final StringBuilder sb = new StringBuilder(); + sb.append( "(select v.x from (" ); + if ( supportsValuesList ) { + sb.append( "values (" ); + sb.append( trueValue ); + sb.append( "),(" ); + sb.append( falseValue ); + sb.append( ")) v(x)" ); + } + else { + sb.append( "select " ); + sb.append( trueValue ); + sb.append( " x"); + sb.append( getFromDualForSelectOnly() ); + sb.append(" union all select " ); + sb.append( falseValue ); + sb.append( getFromDualForSelectOnly() ); + sb.append( ") v" ); + } + sb.append( " left join (" ); + if ( supportsValuesList ) { + sb.append( "values" ); + char separator = ' '; + for ( String trueStringValue : Dialect.TRUE_STRING_VALUES ) { + sb.append( separator ); + sb.append( "('" ); + sb.append( trueStringValue ); + sb.append( "'," ); + sb.append( trueValue ); + sb.append( ')' ); + separator = ','; + } + for ( String falseStringValue : Dialect.FALSE_STRING_VALUES ) { + sb.append( ",('" ); + sb.append( falseStringValue ); + sb.append( "'," ); + sb.append( falseValue ); + sb.append( ')' ); + } + sb.append( ") t(k,v)" ); + } + else { + sb.append( "select '" ); + sb.append( Dialect.TRUE_STRING_VALUES[0] ); + sb.append( "' k," ); + sb.append( trueValue ); + sb.append( " v" ); + sb.append( getFromDualForSelectOnly() ); + for ( int i = 1; i < Dialect.TRUE_STRING_VALUES.length; i++ ) { + sb.append( " union all select '" ); + sb.append( Dialect.TRUE_STRING_VALUES[i] ); + sb.append( "'," ); + sb.append( trueValue ); + sb.append( getFromDualForSelectOnly() ); + } + for ( String falseStringValue : Dialect.FALSE_STRING_VALUES ) { + sb.append( " union all select '" ); + sb.append( falseStringValue ); + sb.append( "'," ); + sb.append( falseValue ); + sb.append( getFromDualForSelectOnly() ); + } + sb.append( ") t" ); + } + sb.append( " on " ); + sb.append( getLowercaseFunction() ); + sb.append( "(?1)=t.k where t.v is null or v.x=t.v)" ); + return sb.toString(); + } + + protected String buildStringToBooleanCastDecode(String trueValue, String falseValue) { + final boolean supportsValuesList = supportsValuesList(); + final StringBuilder sb = new StringBuilder(); + sb.append( "(select v.x from (" ); + if ( supportsValuesList ) { + sb.append( "values (" ); + sb.append( trueValue ); + sb.append( "),(" ); + sb.append( falseValue ); + sb.append( ")) v(x)" ); + } + else { + sb.append( "select " ); + sb.append( trueValue ); + sb.append( " x"); + sb.append( getFromDualForSelectOnly() ); + sb.append(" union all select " ); + sb.append( falseValue ); + sb.append( getFromDualForSelectOnly() ); + sb.append( ") v" ); + } + sb.append( ", (" ); + if ( supportsValuesList ) { + sb.append( "values (" ); + sb.append( buildStringToBooleanDecode( trueValue, falseValue ) ); + sb.append( ")) t(v)" ); + } + else { + sb.append( "select " ); + sb.append( buildStringToBooleanDecode( trueValue, falseValue ) ); + sb.append( " v"); + sb.append( getFromDualForSelectOnly() ); + sb.append(") t" ); + } + sb.append( " where t.v is null or v.x=t.v)" ); + return sb.toString(); + } + + protected String buildStringToBooleanDecode(String trueValue, String falseValue) { + final StringBuilder sb = new StringBuilder(); + sb.append( "decode(" ); + sb.append( getLowercaseFunction() ); + sb.append( "(?1)" ); + for ( String trueStringValue : TRUE_STRING_VALUES ) { + sb.append( ",'" ); + sb.append( trueStringValue ); + sb.append( "'," ); + sb.append( trueValue ); + } + for ( String falseStringValue : FALSE_STRING_VALUES ) { + sb.append( ",'" ); + sb.append( falseStringValue ); + sb.append( "'," ); + sb.append( falseValue ); + } + sb.append( ",null)" ); + return sb.toString(); + } + + /** + * Returns a table expression that has one row. + * + * @return the SQL equivalent to Oracle's {@code dual}. + */ + public String getDual() { + return "(values(0))"; + } + + public String getFromDualForSelectOnly() { + return ""; + } + /** * Obtain a pattern for the SQL equivalent to a * {@code trim()} function call. The resulting diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java index 622479b703e7..6aa5aba5726a 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java @@ -44,6 +44,7 @@ import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.spi.RuntimeModelCreationContext; import org.hibernate.persister.entity.mutation.EntityMutationTarget; +import org.hibernate.query.sqm.CastType; import org.hibernate.query.sqm.FetchClauseType; import org.hibernate.query.sqm.IntervalType; import org.hibernate.query.sqm.TemporalUnit; @@ -445,6 +446,16 @@ public String extractPattern(TemporalUnit unit) { : super.extractPattern(unit); } + @Override + public String castPattern(CastType from, CastType to) { + if ( from == CastType.STRING && to == CastType.BOOLEAN ) { + return "cast(?1 as ?2)"; + } + else { + return super.castPattern( from, to ); + } + } + @Override public String timestampaddPattern(TemporalUnit unit, TemporalType temporalType, IntervalType intervalType) { if ( intervalType != null ) { @@ -1007,4 +1018,14 @@ public boolean supportsCaseInsensitiveLike(){ return true; } + @Override + public boolean supportsValuesList() { + return true; + } + + @Override + public String getDual() { + return "dual"; + } + } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/H2SqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/H2SqlAstTranslator.java index 4554d948dac5..fe824b0dd052 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/H2SqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/H2SqlAstTranslator.java @@ -314,13 +314,9 @@ protected boolean renderPrimaryTableReference(TableGroup tableGroup, LockMode lo final TableReference tableRef = tableGroup.getPrimaryTableReference(); // The H2 parser can't handle a sub-query as first element in a nested join // i.e. `join ( (select ...) alias join ... )`, so we have to introduce a dummy table reference - if ( tableRef instanceof QueryPartTableReference || tableRef.getTableId().startsWith( "(select" ) ) { - final boolean realTableGroup = tableGroup.isRealTableGroup() - && ( isNotEmpty( tableGroup.getTableReferenceJoins() ) - || hasNestedTableGroupsToRender( tableGroup.getNestedTableGroupJoins() ) ); - if ( realTableGroup ) { - appendSql( "dual cross join " ); - } + if ( getSqlBuffer().charAt( getSqlBuffer().length() - 1 ) == '(' + && ( tableRef instanceof QueryPartTableReference || tableRef.getTableId().startsWith( "(select" ) ) ) { + appendSql( "dual cross join " ); } return super.renderPrimaryTableReference( tableGroup, lockMode ); } @@ -365,11 +361,6 @@ protected boolean supportsNullPrecedence() { return true; } - @Override - protected String getDual() { - return "dual"; - } - private boolean supportsOffsetFetchClause() { return true; } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/HANASqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/HANASqlAstTranslator.java index c77eac47d6ce..859e1fa38f26 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/HANASqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/HANASqlAstTranslator.java @@ -252,16 +252,6 @@ protected boolean supportsRowValueConstructorGtLtSyntax() { return false; } - @Override - protected String getDual() { - return "sys.dummy"; - } - - @Override - protected String getFromDualForSelectOnly() { - return " from " + getDual(); - } - @Override protected void renderInsertIntoNoColumns(TableInsertStandard tableInsert) { throw new MappingException( diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/HSQLDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/HSQLDialect.java index fa0ca8189f4a..91f831288e19 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/HSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/HSQLDialect.java @@ -255,25 +255,33 @@ public String castPattern(CastType from, CastType to) { } break; case BOOLEAN: - result = BooleanDecoder.toBoolean( from ); + result = from == CastType.STRING + ? buildStringToBooleanCastDecode( "true", "false" ) + : BooleanDecoder.toBoolean( from ); if ( result != null ) { return result; } break; case INTEGER_BOOLEAN: - result = BooleanDecoder.toIntegerBoolean( from ); + result = from == CastType.STRING + ? buildStringToBooleanCastDecode( "1", "0" ) + : BooleanDecoder.toIntegerBoolean( from ); if ( result != null ) { return result; } break; case YN_BOOLEAN: - result = BooleanDecoder.toYesNoBoolean( from ); + result = from == CastType.STRING + ? buildStringToBooleanCastDecode( "'Y'", "'N'" ) + : BooleanDecoder.toYesNoBoolean( from ); if ( result != null ) { return result; } break; case TF_BOOLEAN: - result = BooleanDecoder.toTrueFalseBoolean( from ); + result = from == CastType.STRING + ? buildStringToBooleanCastDecode( "'T'", "'F'" ) + : BooleanDecoder.toTrueFalseBoolean( from ); if ( result != null ) { return result; } @@ -647,6 +655,11 @@ public boolean requiresFloatCastingOfIntegerDivision() { return true; } + @Override + public boolean supportsValuesList() { + return true; + } + @Override public IdentityColumnSupport getIdentityColumnSupport() { return identityColumnSupport; @@ -727,4 +740,9 @@ public String quoteCollation(String collation) { public DmlTargetColumnQualifierSupport getDmlTargetColumnQualifierSupport() { return DmlTargetColumnQualifierSupport.TABLE_ALIAS; } + + @Override + public String getFromDualForSelectOnly() { + return " from " + getDual(); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/HSQLSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/HSQLSqlAstTranslator.java index f458720b9b7a..1357e12f522c 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/HSQLSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/HSQLSqlAstTranslator.java @@ -12,14 +12,12 @@ import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.metamodel.mapping.JdbcMappingContainer; import org.hibernate.query.IllegalQueryOperationException; -import org.hibernate.query.sqm.BinaryArithmeticOperator; import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.sql.ast.Clause; import org.hibernate.sql.ast.SqlAstNodeRenderingMode; import org.hibernate.sql.ast.spi.AbstractSqlAstTranslator; import org.hibernate.sql.ast.spi.SqlSelection; import org.hibernate.sql.ast.tree.Statement; -import org.hibernate.sql.ast.tree.expression.BinaryArithmeticExpression; import org.hibernate.sql.ast.tree.expression.CaseSearchedExpression; import org.hibernate.sql.ast.tree.expression.CaseSimpleExpression; import org.hibernate.sql.ast.tree.expression.Expression; @@ -152,8 +150,7 @@ protected void visitRecursivePath(Expression recursivePath, int sizeEstimate) { protected void visitAnsiCaseSearchedExpression( CaseSearchedExpression expression, Consumer resultRenderer) { - if ( getParameterRenderingMode() == SqlAstNodeRenderingMode.DEFAULT && areAllResultsParameters( expression ) - || areAllResultsPlainParametersOrLiterals( expression ) ) { + if ( areAllResultsPlainParametersOrStringLiterals( expression ) ) { final List whenFragments = expression.getWhenFragments(); final Expression firstResult = whenFragments.get( 0 ).getResult(); super.visitAnsiCaseSearchedExpression( @@ -177,8 +174,7 @@ protected void visitAnsiCaseSearchedExpression( protected void visitAnsiCaseSimpleExpression( CaseSimpleExpression expression, Consumer resultRenderer) { - if ( getParameterRenderingMode() == SqlAstNodeRenderingMode.DEFAULT && areAllResultsParameters( expression ) - || areAllResultsPlainParametersOrLiterals( expression ) ) { + if ( areAllResultsPlainParametersOrStringLiterals( expression ) ) { final List whenFragments = expression.getWhenFragments(); final Expression firstResult = whenFragments.get( 0 ).getResult(); super.visitAnsiCaseSimpleExpression( @@ -198,11 +194,11 @@ protected void visitAnsiCaseSimpleExpression( } } - protected boolean areAllResultsPlainParametersOrLiterals(CaseSearchedExpression caseSearchedExpression) { + protected boolean areAllResultsPlainParametersOrStringLiterals(CaseSearchedExpression caseSearchedExpression) { final List whenFragments = caseSearchedExpression.getWhenFragments(); final Expression firstResult = whenFragments.get( 0 ).getResult(); if ( isParameter( firstResult ) && getParameterRenderingMode() == SqlAstNodeRenderingMode.DEFAULT - || isLiteral( firstResult ) ) { + || isStringLiteral( firstResult ) ) { for ( int i = 1; i < whenFragments.size(); i++ ) { final Expression result = whenFragments.get( i ).getResult(); if ( isParameter( result ) ) { @@ -210,7 +206,7 @@ protected boolean areAllResultsPlainParametersOrLiterals(CaseSearchedExpression return false; } } - else if ( !isLiteral( result ) ) { + else if ( !isStringLiteral( result ) ) { return false; } } @@ -219,11 +215,11 @@ else if ( !isLiteral( result ) ) { return false; } - protected boolean areAllResultsPlainParametersOrLiterals(CaseSimpleExpression caseSimpleExpression) { + protected boolean areAllResultsPlainParametersOrStringLiterals(CaseSimpleExpression caseSimpleExpression) { final List whenFragments = caseSimpleExpression.getWhenFragments(); final Expression firstResult = whenFragments.get( 0 ).getResult(); if ( isParameter( firstResult ) && getParameterRenderingMode() == SqlAstNodeRenderingMode.DEFAULT - || isLiteral( firstResult ) ) { + || isStringLiteral( firstResult ) ) { for ( int i = 1; i < whenFragments.size(); i++ ) { final Expression result = whenFragments.get( i ).getResult(); if ( isParameter( result ) ) { @@ -231,7 +227,7 @@ protected boolean areAllResultsPlainParametersOrLiterals(CaseSimpleExpression ca return false; } } - else if ( !isLiteral( result ) ) { + else if ( !isStringLiteral( result ) ) { return false; } } @@ -240,6 +236,13 @@ else if ( !isLiteral( result ) ) { return false; } + private boolean isStringLiteral( Expression expression ) { + if ( expression instanceof Literal ) { + return ( (Literal) expression ).getJdbcMapping().getJdbcType().isStringLike(); + } + return false; + } + @Override public boolean supportsFilterClause() { return true; @@ -332,11 +335,6 @@ protected boolean supportsRowValueConstructorSyntaxInQuantifiedPredicates() { return false; } - @Override - protected String getFromDualForSelectOnly() { - return " from " + getDual(); - } - private boolean supportsOffsetFetchClause() { return true; } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBDialect.java index c77ee258a435..75b9335dafbc 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBDialect.java @@ -276,4 +276,9 @@ public IdentifierHelper buildIdentifierHelper(IdentifierHelperBuilder builder, D return super.buildIdentifierHelper( builder, dbMetaData ); } + + @Override + public String getDual() { + return "dual"; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBSqlAstTranslator.java index 78272afe716e..a243e96979f7 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBSqlAstTranslator.java @@ -369,11 +369,6 @@ protected boolean supportsDistinctFromPredicate() { return true; } - @Override - protected String getDual() { - return "dual"; - } - @Override public MariaDBDialect getDialect() { return this.dialect; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java index 08a425ddda31..20ce0f69b5eb 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java @@ -47,6 +47,7 @@ import org.hibernate.exception.spi.TemplatedViolatedConstraintNameExtractor; import org.hibernate.exception.spi.ViolatedConstraintNameExtractor; import org.hibernate.internal.util.JdbcExceptionHelper; +import org.hibernate.internal.util.StringHelper; import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.spi.RuntimeModelCreationContext; import org.hibernate.query.sqm.CastType; @@ -210,7 +211,7 @@ protected static DatabaseVersion createVersion(DialectResolutionInfo info) { protected static DatabaseVersion createVersion(DialectResolutionInfo info, DatabaseVersion defaultVersion) { final String versionString = info.getDatabaseVersion(); if ( versionString != null ) { - final String[] components = versionString.split( "\\." ); + final String[] components = StringHelper.split(".-", versionString); if ( components.length >= 3 ) { try { final int majorVersion = Integer.parseInt( components[0] ); @@ -1571,4 +1572,9 @@ public DmlTargetColumnQualifierSupport getDmlTargetColumnQualifierSupport() { public boolean supportsFromClauseInUpdate() { return true; } + + @Override + public String getDual() { + return "dual"; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLSqlAstTranslator.java index 6be62a55c40e..dd78fb8e0ea2 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLSqlAstTranslator.java @@ -443,11 +443,6 @@ protected boolean supportsWithClause() { return true; } - @Override - protected String getDual() { - return "dual"; - } - @Override public MySQLDialect getDialect() { return (MySQLDialect) DialectDelegateWrapper.extractRealDialect( super.getDialect() ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java index 24e33db59d31..1b9b313b71c7 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java @@ -462,25 +462,33 @@ public String castPattern(CastType from, CastType to) { } break; case INTEGER_BOOLEAN: - result = BooleanDecoder.toIntegerBoolean( from ); + result = from == CastType.STRING + ? buildStringToBooleanCastDecode( "1", "0" ) + : BooleanDecoder.toIntegerBoolean( from ); if ( result != null ) { return result; } break; case YN_BOOLEAN: - result = BooleanDecoder.toYesNoBoolean( from ); + result = from == CastType.STRING + ? buildStringToBooleanCastDecode( "'Y'", "'N'" ) + : BooleanDecoder.toYesNoBoolean( from ); if ( result != null ) { return result; } break; case BOOLEAN: - result = BooleanDecoder.toBoolean( from ); + result = from == CastType.STRING + ? buildStringToBooleanCastDecode( "true", "false" ) + : BooleanDecoder.toBoolean( from ); if ( result != null ) { return result; } break; case TF_BOOLEAN: - result = BooleanDecoder.toTrueFalseBoolean( from ); + result = from == CastType.STRING + ? buildStringToBooleanCastDecode( "'T'", "'F'" ) + : BooleanDecoder.toTrueFalseBoolean( from ); if ( result != null ) { return result; } @@ -1687,4 +1695,14 @@ public boolean useInputStreamToInsertBlob() { return false; } + @Override + public String getDual() { + return "dual"; + } + + @Override + public String getFromDualForSelectOnly() { + return getVersion().isSameOrAfter( 23 ) ? "" : ( " from " + getDual() ); + } + } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/OracleSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/OracleSqlAstTranslator.java index 00558358553b..6f87c4f0cf65 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/OracleSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/OracleSqlAstTranslator.java @@ -617,16 +617,6 @@ protected boolean supportsRowValueConstructorSyntaxInQuantifiedPredicates() { return false; } - @Override - protected String getDual() { - return "dual"; - } - - @Override - protected String getFromDualForSelectOnly() { - return getDialect().getVersion().isSameOrAfter( 23 ) ? "" : ( " from " + getDual() ); - } - private boolean supportsOffsetFetchClause() { return getDialect().supportsFetchClause( FetchClauseType.ROWS_ONLY ); } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java index 67fa1ce56a10..404f10609cba 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java @@ -58,6 +58,7 @@ import org.hibernate.procedure.spi.CallableStatementSupport; import org.hibernate.query.SemanticException; import org.hibernate.query.spi.QueryOptions; +import org.hibernate.query.sqm.CastType; import org.hibernate.query.sqm.FetchClauseType; import org.hibernate.query.sqm.IntervalType; import org.hibernate.query.sqm.TemporalUnit; @@ -467,6 +468,16 @@ public String extractPattern(TemporalUnit unit) { } } + @Override + public String castPattern(CastType from, CastType to) { + if ( from == CastType.STRING && to == CastType.BOOLEAN ) { + return "cast(?1 as ?2)"; + } + else { + return super.castPattern( from, to ); + } + } + /** * {@code microsecond} is the smallest unit for an {@code interval}, * and the highest precision for a {@code timestamp}, so we could diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/StructHelper.java b/hibernate-core/src/main/java/org/hibernate/dialect/StructHelper.java index 1b9ab7c6369e..8458d4adbfc3 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/StructHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/StructHelper.java @@ -12,6 +12,7 @@ import java.sql.NClob; import java.sql.SQLException; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.Internal; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.metamodel.mapping.BasicValuedMapping; @@ -130,13 +131,13 @@ public static Object[] getJdbcValues( private static int injectJdbcValues( EmbeddableMappingType embeddableMappingType, - Object domainValue, + @Nullable Object domainValue, Object[] jdbcValues, int jdbcIndex, WrapperOptions options) throws SQLException { return injectJdbcValues( embeddableMappingType, - embeddableMappingType.getValues( domainValue ), + domainValue == null ? null : embeddableMappingType.getValues( domainValue ), jdbcValues, jdbcIndex, options @@ -145,12 +146,15 @@ private static int injectJdbcValues( private static int injectJdbcValues( EmbeddableMappingType embeddableMappingType, - Object[] values, + @Nullable Object[] values, Object[] jdbcValues, int jdbcIndex, WrapperOptions options) throws SQLException { final int jdbcValueCount = embeddableMappingType.getJdbcValueCount(); final int valueCount = jdbcValueCount + ( embeddableMappingType.isPolymorphic() ? 1 : 0 ); + if ( values == null ) { + return valueCount; + } int offset = 0; for ( int i = 0; i < values.length; i++ ) { offset += injectJdbcValue( @@ -255,22 +259,13 @@ else if ( attributeMapping instanceof EmbeddableValuedModelPart ) { ); } else { - jdbcValueCount = embeddableMappingType.getJdbcValueCount() + ( embeddableMappingType.isPolymorphic() ? 1 : 0 ); - final int numberOfAttributeMappings = embeddableMappingType.getNumberOfAttributeMappings(); - final int numberOfValues = numberOfAttributeMappings + ( embeddableMappingType.isPolymorphic() ? 1 : 0 ); - final Object[] subValues = embeddableMappingType.getValues( attributeValues[attributeIndex] ); - int offset = 0; - for ( int i = 0; i < numberOfValues; i++ ) { - offset += injectJdbcValue( - getEmbeddedPart( embeddableMappingType, i ), - subValues, - i, - jdbcValues, - jdbcIndex + offset, - options - ); - } - assert offset == jdbcValueCount; + jdbcValueCount = injectJdbcValues( + embeddableMappingType, + attributeValues[attributeIndex], + jdbcValues, + jdbcIndex, + options + ); } } else { diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SybaseASEDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseASEDialect.java index 52d320a52b4f..0eaf758b2dd9 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SybaseASEDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseASEDialect.java @@ -142,13 +142,8 @@ protected void registerColumnTypes(TypeContributions typeContributions, ServiceR // But with jTDS we can't use them as the driver can't handle the types if ( getDriverKind() != SybaseDriverKind.JTDS ) { ddlTypeRegistry.addDescriptor( - CapacityDependentDdlType.builder( DATE, "bigdatetime", "bigdatetime", this ) - .withTypeCapacity( 3, "datetime" ) - .build() - ); - ddlTypeRegistry.addDescriptor( - CapacityDependentDdlType.builder( TIME, "bigdatetime", "bigdatetime", this ) - .withTypeCapacity( 3, "datetime" ) + CapacityDependentDdlType.builder( TIME, "bigtime", "bigtime", this ) + .withTypeCapacity( 3, "time" ) .build() ); ddlTypeRegistry.addDescriptor( @@ -712,4 +707,9 @@ public SQLExceptionConversionDelegate buildSQLExceptionConversionDelegate() { public LimitHandler getLimitHandler() { return new TopLimitHandler(false); } + + @Override + public String getDual() { + return "(select 1 c1)"; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SybaseASESqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseASESqlAstTranslator.java index eee92fa20b04..6fd185de98bc 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SybaseASESqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseASESqlAstTranslator.java @@ -508,11 +508,6 @@ protected boolean supportsRowValueConstructorSyntaxInQuantifiedPredicates() { return false; } - @Override - protected String getDual() { - return "(select 1 c1)"; - } - private boolean supportsParameterOffsetFetchExpression() { return false; } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/TiDBDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/TiDBDialect.java index 524128640903..4557bbfb53c8 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/TiDBDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/TiDBDialect.java @@ -190,4 +190,9 @@ public String timestampaddPattern(TemporalUnit unit, TemporalType temporalType, } return super.timestampaddPattern( unit, temporalType, intervalType ); } + + @Override + public String getDual() { + return "dual"; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/TiDBSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/TiDBSqlAstTranslator.java index 1a4320239020..751e3ba6c810 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/TiDBSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/TiDBSqlAstTranslator.java @@ -332,11 +332,6 @@ protected boolean supportsRowValueConstructorSyntaxInQuantifiedPredicates() { return false; } - @Override - protected String getDual() { - return "dual"; - } - @Override protected String getForShare(int timeoutMillis) { if ( timeoutMillis == LockOptions.NO_WAIT ) { diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayConstructorFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayConstructorFunction.java index 233232cfd7d7..b7a658008f5f 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayConstructorFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayConstructorFunction.java @@ -109,13 +109,13 @@ public void validateSqlTypes(List arguments, String functi if ( firstType == null ) { firstType = argumentType; } - else if ( firstType != argumentType ) { + else if ( firstType.getSingleJdbcMapping() != argumentType.getSingleJdbcMapping() ) { throw new FunctionArgumentException( String.format( "All array arguments must have a type compatible to the first argument type [%s], but argument %d has type '%s'", - firstType, + firstType.getSingleJdbcMapping(), i + 1, - argumentType + argumentType.getSingleJdbcMapping() ) ); } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayContainsArgumentTypeResolver.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayContainsArgumentTypeResolver.java index af076cb3aa58..f52ce438e3c5 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayContainsArgumentTypeResolver.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayContainsArgumentTypeResolver.java @@ -47,10 +47,15 @@ public MappingModelExpressible resolveFunctionArgumentType( } } else if ( argumentIndex == 1 ) { + final SqmTypedNode nodeToResolve = function.getArguments().get( 1 ); + if ( nodeToResolve.getExpressible() instanceof MappingModelExpressible ) { + // If the node already has suitable type, don't infer it to be treated as an array + return null; + } final SqmTypedNode node = function.getArguments().get( 0 ); if ( node instanceof SqmExpression ) { final MappingModelExpressible expressible = converter.determineValueMapping( (SqmExpression) node ); - if ( expressible != null ) { + if ( expressible instanceof BasicPluralType ) { return expressible; } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/hint/IndexQueryHintHandler.java b/hibernate-core/src/main/java/org/hibernate/dialect/hint/IndexQueryHintHandler.java index 9a70d556786c..ae4134de5e10 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/hint/IndexQueryHintHandler.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/hint/IndexQueryHintHandler.java @@ -25,8 +25,7 @@ public class IndexQueryHintHandler implements QueryHintHandler { public static final IndexQueryHintHandler INSTANCE = new IndexQueryHintHandler(); - private static final Pattern QUERY_PATTERN = Pattern.compile( "^\\s*(select\\b.+?\\bfrom\\b.+?)(\\bwhere\\b.+?)$" ); - + private static final Pattern QUERY_PATTERN = Pattern.compile( "^\\s*(select\\b.+?\\bfrom\\b.+?)(\\b(where|join)\\b.+?)$" ); @Override public String addQueryHints(String query, String hints) { Matcher matcher = QUERY_PATTERN.matcher( query ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/temptable/TemporaryTableHelper.java b/hibernate-core/src/main/java/org/hibernate/dialect/temptable/TemporaryTableHelper.java index 1f1c6e93f2f4..43cd8d4dc46d 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/temptable/TemporaryTableHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/temptable/TemporaryTableHelper.java @@ -14,6 +14,7 @@ import java.util.function.Function; import org.hibernate.engine.jdbc.internal.FormatStyle; +import org.hibernate.engine.jdbc.spi.JdbcCoordinator; import org.hibernate.engine.jdbc.spi.JdbcServices; import org.hibernate.engine.jdbc.spi.SqlExceptionHelper; import org.hibernate.engine.jdbc.spi.SqlStatementLogger; @@ -149,18 +150,19 @@ public static void cleanTemporaryTableRows( TemporaryTableExporter exporter, Function sessionUidAccess, SharedSessionContractImplementor session) { + final JdbcCoordinator jdbcCoordinator = session.getJdbcCoordinator(); PreparedStatement ps = null; try { final String sql = exporter.getSqlTruncateCommand( temporaryTable, sessionUidAccess, session ); - ps = session.getJdbcCoordinator().getStatementPreparer().prepareStatement( sql, false ); + ps = jdbcCoordinator.getStatementPreparer().prepareStatement( sql, false ); if ( temporaryTable.getSessionUidColumn() != null ) { final String sessionUid = sessionUidAccess.apply( session ); ps.setString( 1, sessionUid ); } - session.getJdbcCoordinator().getResultSetReturn().executeUpdate( ps, sql ); + jdbcCoordinator.getResultSetReturn().executeUpdate( ps, sql ); } catch( Throwable t ) { log.unableToCleanupTemporaryIdTable(t); @@ -168,11 +170,12 @@ public static void cleanTemporaryTableRows( finally { if ( ps != null ) { try { - session.getJdbcCoordinator().getLogicalConnection().getResourceRegistry().release( ps ); + jdbcCoordinator.getLogicalConnection().getResourceRegistry().release( ps ); } catch( Throwable ignore ) { // ignore } + jdbcCoordinator.afterStatementExecution(); } } } diff --git a/hibernate-core/src/main/java/org/hibernate/engine/internal/Cascade.java b/hibernate-core/src/main/java/org/hibernate/engine/internal/Cascade.java index ecf12dcb2af6..1250ba154721 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/internal/Cascade.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/internal/Cascade.java @@ -10,6 +10,7 @@ import java.util.Collection; import java.util.Iterator; import java.util.List; +import java.util.Set; import org.hibernate.HibernateException; import org.hibernate.bytecode.enhance.spi.interceptor.LazyAttributeLoadingInterceptor; @@ -19,6 +20,7 @@ import org.hibernate.engine.spi.CollectionEntry; import org.hibernate.engine.spi.EntityEntry; import org.hibernate.engine.spi.PersistenceContext; +import org.hibernate.engine.spi.SelfDirtinessTracker; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.Status; import org.hibernate.event.spi.DeleteContext; @@ -27,19 +29,22 @@ import org.hibernate.internal.CoreMessageLogger; import org.hibernate.persister.collection.CollectionPersister; import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.type.AnyType; import org.hibernate.type.AssociationType; import org.hibernate.type.CollectionType; import org.hibernate.type.ComponentType; import org.hibernate.type.CompositeType; import org.hibernate.type.EntityType; +import org.hibernate.type.ForeignKeyDirection; import org.hibernate.type.ManyToOneType; import org.hibernate.type.OneToOneType; import org.hibernate.type.Type; +import static org.hibernate.engine.internal.ManagedTypeHelper.asManagedEntity; +import static org.hibernate.engine.internal.ManagedTypeHelper.asSelfDirtinessTrackerOrNull; import static org.hibernate.engine.internal.ManagedTypeHelper.isHibernateProxy; import static org.hibernate.engine.spi.CascadingActions.CHECK_ON_FLUSH; import static org.hibernate.pretty.MessageHelper.infoString; -import static org.hibernate.type.ForeignKeyDirection.TO_PARENT; /** * Delegate responsible for, in conjunction with the various @@ -92,15 +97,29 @@ public static void cascade( final PersistenceContext persistenceContext = eventSource.getPersistenceContextInternal(); final boolean enhancedForLazyLoading = persister.getBytecodeEnhancementMetadata().isEnhancedForLazyLoading(); final EntityEntry entry; + final Set dirtyAttributes; if ( enhancedForLazyLoading ) { entry = persistenceContext.getEntry( parent ); - if ( entry != null - && entry.getLoadedState() == null - && entry.getStatus() == Status.MANAGED ) { - return; + if ( entry != null && entry.getLoadedState() == null && entry.getStatus() == Status.MANAGED ) { + final SelfDirtinessTracker selfDirtinessTracker = asSelfDirtinessTrackerOrNull( parent ); + if ( selfDirtinessTracker == null ) { + return; + } + else { + if ( asManagedEntity( parent ).$$_hibernate_useTracker() ) { + dirtyAttributes = Set.of( selfDirtinessTracker.$$_hibernate_getDirtyAttributes() ); + } + else { + dirtyAttributes = null; + } + } + } + else { + dirtyAttributes = null; } } else { + dirtyAttributes = null; entry = null; } final Type[] types = persister.getPropertyTypes(); @@ -109,8 +128,11 @@ public static void cascade( final boolean hasUninitializedLazyProperties = persister.hasUninitializedLazyProperties( parent ); for ( int i = 0; i < types.length; i++) { - final CascadeStyle style = cascadeStyles[ i ]; final String propertyName = propertyNames[ i ]; + if ( dirtyAttributes != null && !dirtyAttributes.contains( propertyName ) ) { + return; + } + final CascadeStyle style = cascadeStyles[ i ]; final Type type = types[i]; final boolean isUninitializedProperty = hasUninitializedLazyProperties && @@ -130,7 +152,7 @@ public static void cascade( // parent was not in the PersistenceContext continue; } - if ( type.isCollectionType() ) { + if ( type instanceof CollectionType ) { // CollectionType#getCollection gets the PersistentCollection // that corresponds to the uninitialized collection from the // PersistenceContext. If not present, an uninitialized @@ -145,13 +167,13 @@ public static void cascade( null ); } - else if ( type.isComponentType() ) { + else if ( type instanceof AnyType || type instanceof ComponentType ) { // Hibernate does not support lazy embeddables, so this shouldn't happen. throw new UnsupportedOperationException( "Lazy components are not supported." ); } - else if ( action.performOnLazyProperty() && type.isEntityType() ) { + else if ( action.performOnLazyProperty() && type instanceof EntityType ) { // Only need to initialize a lazy entity attribute when action.performOnLazyProperty() // returns true. LazyAttributeLoadingInterceptor interceptor = persister.getBytecodeEnhancementMetadata() @@ -222,7 +244,7 @@ private static void cascadeProperty( final boolean isCascadeDeleteEnabled) throws HibernateException { if ( child != null ) { - if ( type.isAssociationType() ) { + if ( type instanceof EntityType || type instanceof CollectionType || type instanceof AnyType ) { final AssociationType associationType = (AssociationType) type; final boolean unownedTransient = eventSource.getSessionFactory() .getSessionFactoryOptions() @@ -242,7 +264,7 @@ private static void cascadeProperty( ); } } - else if ( type.isComponentType() ) { + else if ( type instanceof ComponentType ) { if ( componentPath == null && propertyName != null ) { componentPath = new ArrayList<>(); } @@ -359,8 +381,8 @@ private static void cascadeLogicalOneToOneOrphanRemoval( ); } - if ( type.isAssociationType() - && ( (AssociationType) type ).getForeignKeyDirection().equals(TO_PARENT) ) { + if ( type instanceof CollectionType + || type instanceof OneToOneType && ( (OneToOneType) type ).getForeignKeyDirection() == ForeignKeyDirection.TO_PARENT ) { // If FK direction is to-parent, we must remove the orphan *before* the queued update(s) // occur. Otherwise, replacing the association on a managed entity, without manually // nulling and flushing, causes FK constraint violations. @@ -400,19 +422,17 @@ private static boolean cascadeAssociationNow( } private static boolean isUnownedAssociation(AssociationType associationType, SessionFactoryImplementor factory) { - if ( associationType.isEntityType() ) { - if ( associationType instanceof ManyToOneType ) { - final ManyToOneType manyToOne = (ManyToOneType) associationType; - // logical one-to-one + non-null unique key property name indicates unowned - return manyToOne.isLogicalOneToOne() && manyToOne.getRHSUniqueKeyPropertyName() != null; - } - else if ( associationType instanceof OneToOneType ) { - final OneToOneType oneToOne = (OneToOneType) associationType; - // constrained false + non-null unique key property name indicates unowned - return oneToOne.isNullable() && oneToOne.getRHSUniqueKeyPropertyName() != null; - } + if ( associationType instanceof ManyToOneType ) { + final ManyToOneType manyToOne = (ManyToOneType) associationType; + // logical one-to-one + non-null unique key property name indicates unowned + return manyToOne.isLogicalOneToOne() && manyToOne.getRHSUniqueKeyPropertyName() != null; } - else if ( associationType.isCollectionType() ) { + else if ( associationType instanceof OneToOneType ) { + final OneToOneType oneToOne = (OneToOneType) associationType; + // constrained false + non-null unique key property name indicates unowned + return oneToOne.isNullable() && oneToOne.getRHSUniqueKeyPropertyName() != null; + } + else if ( associationType instanceof CollectionType ) { // for collections, we can ask the persister if we're on the inverse side return ( (CollectionType) associationType ).isInverse( factory ); } @@ -468,10 +488,10 @@ private static void cascadeAssociation( final CascadeStyle style, final T anything, final boolean isCascadeDeleteEnabled) { - if ( type.isEntityType() || type.isAnyType() ) { + if ( type instanceof EntityType || type instanceof AnyType ) { cascadeToOne( action, eventSource, parent, child, type, style, anything, isCascadeDeleteEnabled ); } - else if ( type.isCollectionType() ) { + else if ( type instanceof CollectionType ) { cascadeCollection( action, cascadePoint, @@ -510,7 +530,7 @@ private static void cascadeCollection( } //cascade to current collection elements - if ( elemType.isEntityType() || elemType.isAnyType() || elemType.isComponentType() ) { + if ( elemType instanceof EntityType || elemType instanceof AnyType || elemType instanceof ComponentType ) { cascadeCollectionElements( action, elementsCascadePoint, @@ -539,7 +559,7 @@ private static void cascadeToOne( final CascadeStyle style, final T anything, final boolean isCascadeDeleteEnabled) { - final String entityName = type.isEntityType() + final String entityName = type instanceof EntityType ? ( (EntityType) type ).getAssociatedEntityName() : null; if ( style.reallyDoCascade( action ) ) { @@ -601,12 +621,22 @@ private static void cascadeCollectionElements( } } + // a newly instantiated collection can't have orphans + final PersistentCollection persistentCollection; + if ( child instanceof PersistentCollection ) { + persistentCollection = (PersistentCollection) child; + } + else { + persistentCollection = eventSource.getPersistenceContext() + .getCollectionHolder( child ); + } + final boolean deleteOrphans = style.hasOrphanDelete() && action.deleteOrphans() - && elemType.isEntityType() - && child instanceof PersistentCollection + && elemType instanceof EntityType + && persistentCollection != null // a newly instantiated collection can't have orphans - && ! ( (PersistentCollection) child ).isNewlyInstantiated(); + && !persistentCollection.isNewlyInstantiated(); if ( deleteOrphans ) { final boolean traceEnabled = LOG.isTraceEnabled(); @@ -617,7 +647,7 @@ private static void cascadeCollectionElements( // 1. newly instantiated collections // 2. arrays (we can't track orphans for detached arrays) final String entityName = collectionType.getAssociatedEntityName( eventSource.getFactory() ); - deleteOrphans( eventSource, entityName, (PersistentCollection) child ); + deleteOrphans( eventSource, entityName, persistentCollection ); if ( traceEnabled ) { LOG.tracev( "Done deleting orphans for collection: {0}", collectionType.getRole() ); diff --git a/hibernate-core/src/main/java/org/hibernate/engine/internal/ForeignKeys.java b/hibernate-core/src/main/java/org/hibernate/engine/internal/ForeignKeys.java index bbf6216e56de..3db99c98e0f5 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/internal/ForeignKeys.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/internal/ForeignKeys.java @@ -16,7 +16,8 @@ import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.proxy.LazyInitializer; -import org.hibernate.type.CompositeType; +import org.hibernate.type.AnyType; +import org.hibernate.type.ComponentType; import org.hibernate.type.EntityType; import org.hibernate.type.Type; @@ -106,21 +107,21 @@ private Object nullifyTransient(Object value, String propertyName, Type type) { if ( value == null ) { return null; } - else if ( type.isEntityType() ) { + else if ( type instanceof EntityType ) { return nullifyEntityType( value, propertyName, (EntityType) type ); } - else if ( type.isAnyType() ) { + else if ( type instanceof AnyType ) { return isNullifiable( null, value) ? null : value; } - else if ( type.isComponentType() ) { - return nullifyCompositeType( value, propertyName, (CompositeType) type ); + else if ( type instanceof ComponentType ) { + return nullifyCompositeType( value, propertyName, (ComponentType) type ); } else { return value; } } - private Object nullifyCompositeType(Object value, String propertyName, CompositeType compositeType) { + private Object nullifyCompositeType(Object value, String propertyName, ComponentType compositeType) { final Object[] subvalues = compositeType.getPropertyValues(value, session ); final Type[] subtypes = compositeType.getSubtypes(); final String[] subPropertyNames = compositeType.getPropertyNames(); @@ -194,7 +195,7 @@ private boolean initializationIsNecessary(Object value, Type type) { // more than just initializing the associated entity. return isDelete && value == UNFETCHED_PROPERTY - && type.isEntityType() + && type instanceof EntityType && !session.getPersistenceContextInternal().isNullifiableEntityKeysEmpty(); } @@ -370,6 +371,21 @@ public static Object getEntityIdentifierIfNotUnsaved( } } + public static Object getEntityIdentifier( + final String entityName, + final Object object, + final SharedSessionContractImplementor session) { + if ( object == null ) { + return null; + } + else { + final Object id = session.getContextEntityIdentifier( object ); + return id == null + ? session.getEntityPersister( entityName, object ).getIdentifier( object, session ) + : id; + } + } + private static void throwIfTransient(String entityName, Object object, SharedSessionContractImplementor session) { if ( isTransient( entityName, object, Boolean.FALSE, session ) ) { throw new TransientObjectException( @@ -432,7 +448,7 @@ private static void collectNonNullableTransientEntities( if ( value == null ) { // do nothing } - else if ( type.isEntityType() ) { + else if ( type instanceof EntityType ) { final EntityType entityType = (EntityType) type; if ( !isNullable && !entityType.isOneToOne() @@ -440,13 +456,13 @@ else if ( type.isEntityType() ) { nonNullableTransientEntities.add( propertyName, value ); } } - else if ( type.isAnyType() ) { + else if ( type instanceof AnyType ) { if ( !isNullable && nullifier.isNullifiable( null, value ) ) { nonNullableTransientEntities.add( propertyName, value ); } } - else if ( type.isComponentType() ) { - final CompositeType compositeType = (CompositeType) type; + else if ( type instanceof ComponentType ) { + final ComponentType compositeType = (ComponentType) type; final boolean[] subValueNullability = compositeType.getPropertyNullability(); if ( subValueNullability != null ) { final String[] subPropertyNames = compositeType.getPropertyNames(); diff --git a/hibernate-core/src/main/java/org/hibernate/engine/internal/ManagedTypeHelper.java b/hibernate-core/src/main/java/org/hibernate/engine/internal/ManagedTypeHelper.java index 5c849ad63d2a..3b280aa78900 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/internal/ManagedTypeHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/internal/ManagedTypeHelper.java @@ -390,6 +390,15 @@ public static SelfDirtinessTracker asSelfDirtinessTracker(final Object entity) { throw new ClassCastException( "Object of type '" + entity.getClass() + "' can't be cast to SelfDirtinessTracker" ); } + public static SelfDirtinessTracker asSelfDirtinessTrackerOrNull(final Object entity) { + Objects.requireNonNull( entity ); + if ( entity instanceof PrimeAmongSecondarySupertypes ) { + PrimeAmongSecondarySupertypes t = (PrimeAmongSecondarySupertypes) entity; + return t.asSelfDirtinessTracker(); + } + return null; + } + /** * Cast the object to an HibernateProxy, or return null in case it is not an instance of HibernateProxy * @param entity the entity to cast diff --git a/hibernate-core/src/main/java/org/hibernate/engine/internal/Nullability.java b/hibernate-core/src/main/java/org/hibernate/engine/internal/Nullability.java index a2ab620a39c8..33cf8067ae1a 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/internal/Nullability.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/internal/Nullability.java @@ -14,7 +14,9 @@ import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.generator.Generator; +import org.hibernate.type.AnyType; import org.hibernate.type.CollectionType; +import org.hibernate.type.ComponentType; import org.hibernate.type.CompositeType; import org.hibernate.type.Type; @@ -144,16 +146,19 @@ private static boolean generated(Generator generator) { * @throws HibernateException error while getting subcomponent values */ private String checkSubElementsNullability(Type propertyType, Object value) throws HibernateException { - if ( propertyType.isComponentType() ) { - return checkComponentNullability( value, (CompositeType) propertyType ); + if ( propertyType instanceof AnyType ) { + return checkComponentNullability( value, (AnyType) propertyType ); + } + if ( propertyType instanceof ComponentType ) { + return checkComponentNullability( value, (ComponentType) propertyType ); } - if ( propertyType.isCollectionType() ) { + if ( propertyType instanceof CollectionType ) { // persistent collections may have components final CollectionType collectionType = (CollectionType) propertyType; final Type collectionElementType = collectionType.getElementType( session.getFactory() ); - if ( collectionElementType.isComponentType() ) { + if ( collectionElementType instanceof ComponentType || collectionElementType instanceof AnyType ) { // check for all components values in the collection final CompositeType componentType = (CompositeType) collectionElementType; final Iterator itr = getLoadedElementsIterator( session, collectionType, value ); @@ -189,7 +194,7 @@ private String checkComponentNullability(Object value, CompositeType compositeTy // // The more correct fix would be to cascade saves of the many-to-any elements before the Nullability checking - if ( compositeType.isAnyType() ) { + if ( compositeType instanceof AnyType ) { return null; } diff --git a/hibernate-core/src/main/java/org/hibernate/engine/internal/StatefulPersistenceContext.java b/hibernate-core/src/main/java/org/hibernate/engine/internal/StatefulPersistenceContext.java index f22ad6c35db0..63bcc03169ca 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/internal/StatefulPersistenceContext.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/internal/StatefulPersistenceContext.java @@ -78,6 +78,7 @@ import static org.hibernate.engine.internal.ManagedTypeHelper.asManagedEntity; import static org.hibernate.engine.internal.ManagedTypeHelper.asPersistentAttributeInterceptable; import static org.hibernate.engine.internal.ManagedTypeHelper.isPersistentAttributeInterceptable; +import static org.hibernate.proxy.HibernateProxy.extractLazyInitializer; /** * A stateful implementation of the {@link PersistenceContext} contract, meaning that we maintain this @@ -231,8 +232,14 @@ public void clear() { if ( entitiesByKey != null ) { //Strictly avoid lambdas in this case for ( EntityHolderImpl value : entitiesByKey.values() ) { - if ( value != null && value.proxy != null ) { - HibernateProxy.extractLazyInitializer( value.proxy ).unsetSession(); + if ( value != null ) { + value.state = EntityHolderState.DETACHED; + if ( value.proxy != null ) { + final LazyInitializer lazyInitializer = extractLazyInitializer( value.proxy ); + if ( lazyInitializer != null ) { + lazyInitializer.unsetSession(); + } + } } } } @@ -2243,6 +2250,11 @@ public boolean isEventuallyInitialized() { return state == EntityHolderState.INITIALIZED || entityInitializer != null; } + @Override + public boolean isDetached() { + return state == EntityHolderState.DETACHED; + } + public static EntityHolderImpl forProxy(EntityKey entityKey, EntityPersister descriptor, Object proxy) { return new EntityHolderImpl( entityKey, descriptor, null, proxy ); } @@ -2255,7 +2267,8 @@ public static EntityHolderImpl forEntity(EntityKey entityKey, EntityPersister de enum EntityHolderState { UNINITIALIZED, ENHANCED_PROXY, - INITIALIZED + INITIALIZED, + DETACHED } // NATURAL ID RESOLUTION HANDLING ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -2271,4 +2284,13 @@ public NaturalIdResolutions getNaturalIdResolutions() { return naturalIdResolutions; } + @Override + public EntityHolder detachEntity(EntityKey key) { + final EntityHolderImpl entityHolder = removeEntityHolder( key ); + if ( entityHolder != null ) { + entityHolder.state = EntityHolderState.DETACHED; + } + return entityHolder; + } + } diff --git a/hibernate-core/src/main/java/org/hibernate/engine/internal/StatisticalLoggingSessionEventListener.java b/hibernate-core/src/main/java/org/hibernate/engine/internal/StatisticalLoggingSessionEventListener.java index 8a6b06744112..374bce7df35e 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/internal/StatisticalLoggingSessionEventListener.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/internal/StatisticalLoggingSessionEventListener.java @@ -250,10 +250,10 @@ public void prePartialFlushStart() { @Override public void prePartialFlushEnd() { - assert prePartialFlushStart > 0 : "Unexpected call to partialFlushEnd; expecting partialFlushStart"; + assert prePartialFlushStart > 0 : "Unexpected call to prePartialFlushEnd; expecting prePartialFlushStart"; prePartialFlushCount++; - prePartialFlushTime += ( System.nanoTime() - partialFlushStart ); + prePartialFlushTime += ( System.nanoTime() - prePartialFlushStart ); prePartialFlushStart = -1; } diff --git a/hibernate-core/src/main/java/org/hibernate/engine/internal/TwoPhaseLoad.java b/hibernate-core/src/main/java/org/hibernate/engine/internal/TwoPhaseLoad.java index 1e6c5109348b..9791b4098a24 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/internal/TwoPhaseLoad.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/internal/TwoPhaseLoad.java @@ -7,10 +7,16 @@ package org.hibernate.engine.internal; import org.hibernate.LockMode; +import org.hibernate.engine.spi.EntityEntry; +import org.hibernate.engine.spi.EntityHolder; import org.hibernate.engine.spi.EntityKey; +import org.hibernate.engine.spi.PersistenceContext; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.engine.spi.Status; import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.proxy.LazyInitializer; + +import static org.hibernate.proxy.HibernateProxy.extractLazyInitializer; /** * Functionality relating to the Hibernate two-phase loading process, that may be reused by persisters @@ -39,16 +45,27 @@ public static void addUninitializedCachedEntity( final LockMode lockMode, final Object version, final SharedSessionContractImplementor session) { - session.getPersistenceContextInternal().addEntity( + final PersistenceContext persistenceContext = session.getPersistenceContextInternal(); + final EntityHolder entityHolder = persistenceContext.addEntityHolder( key, object ); + final EntityEntry entityEntry = persistenceContext.addEntry( object, Status.LOADING, null, - key, + null, + key.getIdentifier(), version, lockMode, true, persister, false ); + entityHolder.setEntityEntry( entityEntry ); + final Object proxy = entityHolder.getProxy(); + if ( proxy != null ) { + // there is already a proxy for this impl + final LazyInitializer lazyInitializer = extractLazyInitializer( proxy ); + assert lazyInitializer != null; + lazyInitializer.setImplementation( object ); + } } } diff --git a/hibernate-core/src/main/java/org/hibernate/engine/internal/UnsavedValueFactory.java b/hibernate-core/src/main/java/org/hibernate/engine/internal/UnsavedValueFactory.java index 3df891f301cd..e582ad0ab0ad 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/internal/UnsavedValueFactory.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/internal/UnsavedValueFactory.java @@ -21,6 +21,8 @@ import org.hibernate.type.descriptor.java.VersionJavaType; import org.hibernate.type.descriptor.java.spi.PrimitiveJavaType; +import static org.hibernate.engine.internal.Versioning.isNullInitialVersion; + /** * Helper for dealing with unsaved value handling * @@ -82,25 +84,20 @@ else if ( "any".equals( unsavedValue ) ) { public static VersionValue getUnsavedVersionValue( KeyValue bootVersionMapping, VersionJavaType jtd, - Long length, - Integer precision, - Integer scale, Getter getter, - Supplier templateInstanceAccess, - SessionFactoryImplementor sessionFactory) { + Supplier templateInstanceAccess) { final String unsavedValue = bootVersionMapping.getNullValue(); if ( unsavedValue == null ) { if ( getter != null && templateInstanceAccess != null ) { - Object templateInstance = templateInstanceAccess.get(); + final Object templateInstance = templateInstanceAccess.get(); @SuppressWarnings("unchecked") final T defaultValue = (T) getter.get( templateInstance ); - - // if the version of a newly instantiated object is not the same - // as the version seed value, use that as the unsaved-value - final T seedValue = jtd.seed( length, precision, scale, mockSession( sessionFactory ) ); - return jtd.areEqual( seedValue, defaultValue ) - ? VersionValue.UNDEFINED - : new VersionValue( defaultValue ); + // if the version of a newly instantiated object is null + // or a negative number, use that value as the unsaved-value, + // otherwise assume it's the initial version set by program + return isNullInitialVersion( defaultValue ) + ? new VersionValue( defaultValue ) + : VersionValue.UNDEFINED; } else { return VersionValue.UNDEFINED; diff --git a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/BlobProxy.java b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/BlobProxy.java index fa70329ce446..556768b41ac9 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/BlobProxy.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/BlobProxy.java @@ -38,6 +38,8 @@ public final class BlobProxy implements Blob, BlobImplementer { // no longer necessary. The class name could be updated to reflect this but that would break APIs. private final BinaryStream binaryStream; + private final int markBytes; + private boolean resetAllowed; private boolean needsReset; /** @@ -47,7 +49,9 @@ public final class BlobProxy implements Blob, BlobImplementer { * @see #generateProxy(byte[]) */ private BlobProxy(byte[] bytes) { - binaryStream = new BinaryStreamImpl( bytes ); + binaryStream = new BinaryStreamImpl(bytes); + markBytes = bytes.length + 1; + setStreamMark(); } /** @@ -59,6 +63,19 @@ private BlobProxy(byte[] bytes) { */ private BlobProxy(InputStream stream, long length) { this.binaryStream = new StreamBackedBinaryStream( stream, length ); + this.markBytes = (int) length + 1; + setStreamMark(); + } + + private void setStreamMark() { + final InputStream inputStream = binaryStream.getInputStream(); + if ( inputStream != null && inputStream.markSupported() ) { + inputStream.mark( markBytes ); + resetAllowed = true; + } + else { + resetAllowed = false; + } } private InputStream getStream() throws SQLException { @@ -73,7 +90,14 @@ public BinaryStream getUnderlyingStream() throws SQLException { private void resetIfNeeded() throws SQLException { try { if ( needsReset ) { - binaryStream.getInputStream().reset(); + final InputStream inputStream = binaryStream.getInputStream(); + if ( !resetAllowed && inputStream != null) { + throw new SQLException( "Underlying stream does not allow reset" ); + } + if ( inputStream != null ) { + inputStream.reset(); + setStreamMark(); + } } } catch ( IOException ioe) { @@ -96,6 +120,11 @@ public static Blob generateProxy(byte[] bytes) { /** * Generates a BlobImpl proxy using a given number of bytes from an InputStream. * + * Be aware that certain database drivers will automatically close the provided InputStream after the + * contents have been written to the database. This may cause unintended side effects if the entity + * is also audited by Envers. In this case, it's recommended to use {@link #generateProxy(byte[])} + * instead as it isn't affected by this non-standard behavior. + * * @param stream The input stream of bytes to be created as a Blob. * @param length The number of bytes from stream to be written to the Blob. * diff --git a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/batch/JdbcBatchLogging.java b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/batch/JdbcBatchLogging.java index 10de5317c012..11edcd01c15e 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/batch/JdbcBatchLogging.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/batch/JdbcBatchLogging.java @@ -15,8 +15,8 @@ import org.jboss.logging.annotations.MessageLogger; import org.jboss.logging.annotations.ValidIdRange; -import static org.jboss.logging.Logger.Level.ERROR; import static org.jboss.logging.Logger.Level.INFO; +import static org.jboss.logging.Logger.Level.WARN; /** * Sub-system logging related to JDBC batch execution @@ -35,11 +35,7 @@ public interface JdbcBatchLogging extends BasicLogger { Logger BATCH_LOGGER = Logger.getLogger( NAME ); JdbcBatchLogging BATCH_MESSAGE_LOGGER = Logger.getMessageLogger( JdbcBatchLogging.class, NAME ); - @LogMessage(level = ERROR) - @Message(id = 100501, value = "Exception executing batch [%s], SQL: %s") - void unableToExecuteBatch(Exception e, String sql ); - - @LogMessage(level = ERROR) + @LogMessage(level = WARN) @Message(id = 100502, value = "Unable to release batch statement...") void unableToReleaseBatchStatement(); diff --git a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/batch/internal/BatchImpl.java b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/batch/internal/BatchImpl.java index 2dcd5363d88d..51c17304cfad 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/batch/internal/BatchImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/batch/internal/BatchImpl.java @@ -290,12 +290,10 @@ protected void performExecution() { } catch (SQLException e) { abortBatch( e ); - BATCH_MESSAGE_LOGGER.unableToExecuteBatch( e, sql ); throw sqlExceptionHelper.convert( e, "could not execute batch", sql ); } catch (RuntimeException re) { abortBatch( re ); - BATCH_MESSAGE_LOGGER.unableToExecuteBatch( re, sql ); throw re; } } ); diff --git a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/internal/DatasourceConnectionProviderImpl.java b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/internal/DatasourceConnectionProviderImpl.java index 76bdbd7f731e..fea57b07fef1 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/internal/DatasourceConnectionProviderImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/internal/DatasourceConnectionProviderImpl.java @@ -139,7 +139,7 @@ public boolean supportsAggressiveRelease() { @Override public DatabaseConnectionInfo getDatabaseConnectionInfo(Dialect dialect) { return new DatabaseConnectionInfoImpl( - "Connecting through datasource" + dataSourceJndiName, + "Connecting through datasource '" + (dataSourceJndiName != null ? dataSourceJndiName : dataSource) + "'", null, dialect.getVersion(), null, diff --git a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/env/internal/JdbcEnvironmentInitiator.java b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/env/internal/JdbcEnvironmentInitiator.java index 7038b1aa9fc3..bcd86a040922 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/env/internal/JdbcEnvironmentInitiator.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/env/internal/JdbcEnvironmentInitiator.java @@ -19,7 +19,6 @@ import org.hibernate.dialect.Dialect; import org.hibernate.engine.config.spi.ConfigurationService; import org.hibernate.engine.jdbc.batch.spi.BatchBuilder; -import org.hibernate.engine.jdbc.connections.internal.ConnectionProviderInitiator; import org.hibernate.engine.jdbc.connections.internal.DatabaseConnectionInfoImpl; import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider; import org.hibernate.engine.jdbc.connections.spi.DatabaseConnectionInfo; @@ -154,10 +153,14 @@ else if ( explicitDialectConfiguration( explicitDatabaseName, configurationValue databaseConnectionInfo = buildDbInfo( configurationValues, jdbcEnvironment.getDialect() ); } + logConnectionInfo( databaseConnectionInfo ); + return jdbcEnvironment; + } + + // For Hibernate Reactive: it needs to disable or customize the log + protected void logConnectionInfo(DatabaseConnectionInfo databaseConnectionInfo) { // Standardized DB info logging ConnectionInfoLogger.INSTANCE.logConnectionInfoDetails( databaseConnectionInfo.toInfoString() ); - - return jdbcEnvironment; } private DatabaseConnectionInfo buildDbInfo(ServiceRegistryImplementor registry, Dialect dialect) { @@ -175,7 +178,8 @@ private DatabaseConnectionInfo buildDbInfo(Map configurationValu return new DatabaseConnectionInfoImpl( configurationValues, dialect ); } - private static JdbcEnvironmentImpl getJdbcEnvironmentWithDefaults( + // Used by Hibernate Reactive + protected JdbcEnvironmentImpl getJdbcEnvironmentWithDefaults( Map configurationValues, ServiceRegistryImplementor registry, DialectFactory dialectFactory) { @@ -183,7 +187,8 @@ private static JdbcEnvironmentImpl getJdbcEnvironmentWithDefaults( return new JdbcEnvironmentImpl( registry, dialect ); } - private static JdbcEnvironmentImpl getJdbcEnvironmentWithExplicitConfiguration( + // Used by Hibernate Reactive + protected JdbcEnvironmentImpl getJdbcEnvironmentWithExplicitConfiguration( Map configurationValues, ServiceRegistryImplementor registry, DialectFactory dialectFactory, @@ -204,6 +209,15 @@ private static JdbcEnvironmentImpl getJdbcEnvironmentWithExplicitConfiguration( null, configurationValues ); + return getJdbcEnvironmentWithExplicitConfiguration( configurationValues, registry, dialectFactory, dialectResolutionInfo ); + } + + // Used by Hibernate Reactive + protected JdbcEnvironmentImpl getJdbcEnvironmentWithExplicitConfiguration( + Map configurationValues, + ServiceRegistryImplementor registry, + DialectFactory dialectFactory, + DialectResolutionInfo dialectResolutionInfo) { final Dialect dialect = dialectFactory.buildDialect( configurationValues, () -> dialectResolutionInfo ); return new JdbcEnvironmentImpl( registry, dialect ); } @@ -216,7 +230,7 @@ private static JdbcEnvironmentImpl getJdbcEnvironmentWithExplicitConfiguration( * * @see JdbcSettings#ALLOW_METADATA_ON_BOOT */ - private static boolean allowJdbcMetadataAccess(Map configurationValues) { + public static boolean allowJdbcMetadataAccess(Map configurationValues) { final Boolean allow = getBooleanWrapper( ALLOW_METADATA_ON_BOOT, configurationValues, null ); if ( allow != null ) { return allow; @@ -296,7 +310,8 @@ private static String getExplicitDatabaseName(Map configurationV ); } - private JdbcEnvironmentImpl getJdbcEnvironmentUsingJdbcMetadata( + // Used by Hibernate Reactive + protected JdbcEnvironmentImpl getJdbcEnvironmentUsingJdbcMetadata( Map configurationValues, ServiceRegistryImplementor registry, DialectFactory dialectFactory, String explicitDatabaseName, diff --git a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/mutation/internal/PreparedStatementDetailsStandard.java b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/mutation/internal/PreparedStatementDetailsStandard.java index 1732484d5694..f4b04aae4ab2 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/mutation/internal/PreparedStatementDetailsStandard.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/mutation/internal/PreparedStatementDetailsStandard.java @@ -12,6 +12,7 @@ import org.hibernate.engine.jdbc.mutation.group.PreparedStatementDetails; import org.hibernate.engine.jdbc.mutation.group.PreparedStatementGroup; +import org.hibernate.engine.jdbc.spi.JdbcCoordinator; import org.hibernate.engine.jdbc.spi.JdbcServices; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.jdbc.Expectation; @@ -66,8 +67,10 @@ public TableMapping getMutatingTableDetails() { @Override public void releaseStatement(SharedSessionContractImplementor session) { if ( statement != null ) { - session.getJdbcCoordinator().getLogicalConnection().getResourceRegistry().release( statement ); + final JdbcCoordinator jdbcCoordinator = session.getJdbcCoordinator(); + jdbcCoordinator.getLogicalConnection().getResourceRegistry().release( statement ); statement = null; + jdbcCoordinator.afterStatementExecution(); } } diff --git a/hibernate-core/src/main/java/org/hibernate/engine/profile/FetchProfile.java b/hibernate-core/src/main/java/org/hibernate/engine/profile/FetchProfile.java index 0cd02922b83c..965cc4e86e71 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/profile/FetchProfile.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/profile/FetchProfile.java @@ -14,6 +14,7 @@ import org.hibernate.internal.CoreMessageLogger; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.type.BagType; +import org.hibernate.type.CollectionType; import org.hibernate.type.Type; import org.checkerframework.checker.nullness.qual.Nullable; @@ -98,7 +99,7 @@ public void addFetch(final Fetch fetch) { final String role = association.getRole(); final Type associationType = association.getOwner().getPropertyType( association.getAssociationPath() ); - if ( associationType.isCollectionType() ) { + if ( associationType instanceof CollectionType ) { LOG.tracev( "Handling request to add collection fetch [{0}]", role ); // couple of things for which to account in the case of collection diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/ActionQueue.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/ActionQueue.java index 332bcefa9b96..9922ac4c1725 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/spi/ActionQueue.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/ActionQueue.java @@ -52,7 +52,7 @@ import org.hibernate.persister.entity.EntityPersister; import org.hibernate.proxy.LazyInitializer; import org.hibernate.type.CollectionType; -import org.hibernate.type.CompositeType; +import org.hibernate.type.ComponentType; import org.hibernate.type.EntityType; import org.hibernate.type.ForeignKeyDirection; import org.hibernate.type.Type; @@ -1167,7 +1167,10 @@ public void addTransitiveDependencies(InsertInfo origin, Set visited } private void addDirectDependency(Type type, @Nullable Object value, IdentityHashMap insertInfosByEntity) { - if ( type.isEntityType() && value != null ) { + if ( value == null ) { + return; + } + if ( type instanceof EntityType ) { final EntityType entityType = (EntityType) type; final InsertInfo insertInfo = insertInfosByEntity.get( value ); if ( insertInfo != null ) { @@ -1188,7 +1191,7 @@ private void addDirectDependency(Type type, @Nullable Object value, IdentityHash } } } - else if ( type.isCollectionType() && value != null ) { + else if ( type instanceof CollectionType ) { CollectionType collectionType = (CollectionType) type; final PluralAttributeMapping pluralAttributeMapping = insertAction.getSession() .getFactory() @@ -1212,9 +1215,9 @@ else if ( type.isCollectionType() && value != null ) { } } } - else if ( type.isComponentType() && value != null ) { + else if ( type instanceof ComponentType ) { // Support recursive checks of composite type properties for associations and collections. - final CompositeType compositeType = (CompositeType) type; + ComponentType compositeType = (ComponentType) type; final SharedSessionContractImplementor session = insertAction.getSession(); final Object[] componentValues = compositeType.getPropertyValues( value, session ); for ( int j = 0; j < componentValues.length; ++j ) { diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/EntityHolder.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/EntityHolder.java index 8502197cfb49..ece1f1c8633b 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/spi/EntityHolder.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/EntityHolder.java @@ -67,4 +67,9 @@ public interface EntityHolder { * Whether the entity is already initialized or will be initialized through an initializer eventually. */ boolean isEventuallyInitialized(); + + /** + * Whether the entity is detached. + */ + boolean isDetached(); } diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/LoadQueryInfluencers.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/LoadQueryInfluencers.java index 4ccabc8d6ccb..b5c4d7ca2829 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/spi/LoadQueryInfluencers.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/LoadQueryInfluencers.java @@ -325,7 +325,7 @@ public int effectiveBatchSize(CollectionPersister persister) { } public boolean effectivelyBatchLoadable(CollectionPersister persister) { - return batchSize > 1 || persister.isBatchLoadable(); + return persister.isBatchLoadable() || effectiveBatchSize( persister ) > 1; } public int effectiveBatchSize(EntityPersister persister) { @@ -336,7 +336,7 @@ public int effectiveBatchSize(EntityPersister persister) { } public boolean effectivelyBatchLoadable(EntityPersister persister) { - return batchSize > 1 || persister.isBatchLoadable(); + return persister.isBatchLoadable() || effectiveBatchSize( persister ) > 1; } public boolean getSubselectFetchEnabled() { diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/PersistenceContext.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/PersistenceContext.java index b60436330bc5..b5198bf3a4a1 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/spi/PersistenceContext.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/PersistenceContext.java @@ -864,4 +864,12 @@ EntityHolder claimEntityHolderIfPossible( * @return This persistence context's natural-id helper */ NaturalIdResolutions getNaturalIdResolutions(); + + /** + Remove the {@link EntityHolder} and set its state to DETACHED + */ + default @Nullable EntityHolder detachEntity(EntityKey key) { + EntityHolder entityHolder = removeEntityHolder( key ); + return entityHolder; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/event/internal/AbstractSaveEventListener.java b/hibernate-core/src/main/java/org/hibernate/event/internal/AbstractSaveEventListener.java index fd218493ebbd..d03b2ae90e5a 100644 --- a/hibernate-core/src/main/java/org/hibernate/event/internal/AbstractSaveEventListener.java +++ b/hibernate-core/src/main/java/org/hibernate/event/internal/AbstractSaveEventListener.java @@ -152,7 +152,8 @@ private static Object generateId( EventSource source, BeforeExecutionGenerator generator, EntityPersister persister) { - final Object id = generator.generate( source, entity, null, INSERT ); + final Object currentValue = generator.allowAssignedIdentifiers() ? persister.getIdentifier( entity, source ) : null; + final Object id = generator.generate( source, entity, currentValue, INSERT ); if ( id == null ) { throw new IdentifierGenerationException( "Null id generated for entity '" + persister.getEntityName() + "'" ); } diff --git a/hibernate-core/src/main/java/org/hibernate/event/internal/AbstractVisitor.java b/hibernate-core/src/main/java/org/hibernate/event/internal/AbstractVisitor.java index 19e2d9768556..1cda6c54bdbd 100644 --- a/hibernate-core/src/main/java/org/hibernate/event/internal/AbstractVisitor.java +++ b/hibernate-core/src/main/java/org/hibernate/event/internal/AbstractVisitor.java @@ -10,7 +10,9 @@ import org.hibernate.bytecode.enhance.spi.LazyPropertyInitializer; import org.hibernate.event.spi.EventSource; import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.type.AnyType; import org.hibernate.type.CollectionType; +import org.hibernate.type.ComponentType; import org.hibernate.type.CompositeType; import org.hibernate.type.EntityType; import org.hibernate.type.Type; @@ -87,15 +89,18 @@ Object processComponent(Object component, CompositeType componentType) throws Hi */ final Object processValue(Object value, Type type) throws HibernateException { - if ( type.isCollectionType() ) { + if ( type instanceof CollectionType ) { //even process null collections return processCollection( value, (CollectionType) type ); } - else if ( type.isEntityType() ) { + else if ( type instanceof EntityType ) { return processEntity( value, (EntityType) type ); } - else if ( type.isComponentType() ) { - return processComponent( value, (CompositeType) type ); + else if ( type instanceof ComponentType ) { + return processComponent( value, (ComponentType) type ); + } + else if ( type instanceof AnyType ) { + return processComponent( value, (AnyType) type ); } else { return null; diff --git a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultDeleteEventListener.java b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultDeleteEventListener.java index 0c5c4eaad3aa..aa74c2a50323 100644 --- a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultDeleteEventListener.java +++ b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultDeleteEventListener.java @@ -47,6 +47,7 @@ import org.hibernate.proxy.HibernateProxy; import org.hibernate.proxy.LazyInitializer; import org.hibernate.type.CollectionType; +import org.hibernate.type.ComponentType; import org.hibernate.type.CompositeType; import org.hibernate.type.Type; import org.hibernate.type.TypeHelper; @@ -137,15 +138,15 @@ && canBeDeletedWithoutLoading( source, persister ) ) { private static void deleteOwnedCollections(Type type, Object key, EventSource session) { final MappingMetamodelImplementor mappingMetamodel = session.getFactory().getMappingMetamodel(); final ActionQueue actionQueue = session.getActionQueue(); - if ( type.isCollectionType() ) { + if ( type instanceof CollectionType ) { final String role = ( (CollectionType) type ).getRole(); final CollectionPersister persister = mappingMetamodel.getCollectionDescriptor(role); if ( !persister.isInverse() && !skipRemoval( session, persister, key ) ) { actionQueue.addAction( new CollectionRemoveAction( persister, key, session ) ); } } - else if ( type.isComponentType() ) { - final Type[] subtypes = ( (CompositeType) type ).getSubtypes(); + else if ( type instanceof ComponentType ) { + final Type[] subtypes = ( (ComponentType) type ).getSubtypes(); for ( Type subtype : subtypes ) { deleteOwnedCollections( subtype, key, session ); } @@ -459,7 +460,7 @@ private Object[] createDeletedState( final String[] propertyNames = persister.getPropertyNames(); final BytecodeEnhancementMetadata enhancementMetadata = persister.getBytecodeEnhancementMetadata(); for ( int i = 0; i < types.length; i++) { - if ( types[i].isCollectionType() && !enhancementMetadata.isAttributeLoaded( parent, propertyNames[i] ) ) { + if ( types[i] instanceof CollectionType && !enhancementMetadata.isAttributeLoaded( parent, propertyNames[i] ) ) { final CollectionType collectionType = (CollectionType) types[i]; final CollectionPersister collectionDescriptor = persister.getFactory().getMappingMetamodel() .getCollectionDescriptor( collectionType.getRole() ); diff --git a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultEvictEventListener.java b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultEvictEventListener.java index 9b6f9714552e..c22929a5c4d1 100644 --- a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultEvictEventListener.java +++ b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultEvictEventListener.java @@ -59,7 +59,7 @@ public void onEvict(EvictEvent event) throws HibernateException { .getMappingMetamodel() .getEntityDescriptor( lazyInitializer.getEntityName() ); final EntityKey key = source.generateEntityKey( id, persister ); - final EntityHolder holder = persistenceContext.removeEntityHolder( key ); + final EntityHolder holder = persistenceContext.detachEntity( key ); // if the entity has been evicted then its holder is null if ( holder != null && !lazyInitializer.isUninitialized() ) { final Object entity = holder.getEntity(); diff --git a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultMergeEventListener.java b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultMergeEventListener.java index 24f7af0f2f6d..3dbcb8dc1174 100644 --- a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultMergeEventListener.java +++ b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultMergeEventListener.java @@ -41,7 +41,9 @@ import org.hibernate.proxy.HibernateProxy; import org.hibernate.proxy.LazyInitializer; import org.hibernate.stat.spi.StatisticsImplementor; +import org.hibernate.type.AnyType; import org.hibernate.type.CollectionType; +import org.hibernate.type.ComponentType; import org.hibernate.type.CompositeType; import org.hibernate.type.EntityType; import org.hibernate.type.ForeignKeyDirection; @@ -162,17 +164,17 @@ private void merge(MergeEvent event, MergeContext copiedAlready, Object entity) final Object originalId; if ( entry == null ) { final EntityPersister persister = source.getEntityPersister( event.getEntityName(), entity ); - originalId = persister.getIdentifier( entity, source ); + originalId = persister.getIdentifier( entity, copiedAlready ); if ( originalId != null ) { final EntityKey entityKey; - if ( persister.getIdentifierType().isComponentType() ) { + if ( persister.getIdentifierType() instanceof ComponentType ) { /* this is needed in case of composite id containing an association with a generated identifier, in such a case generating the EntityKey will cause a NPE when trying to get the hashcode of the null id */ copiedId = copyCompositeTypeId( originalId, - (CompositeType) persister.getIdentifierType(), + (ComponentType) persister.getIdentifierType(), source, copiedAlready ); @@ -250,7 +252,7 @@ private static Object copyCompositeTypeId( final Object[] copyValues = compositeType.getPropertyValues( idCopy ); for ( int i = 0; i < subtypes.length; i++ ) { final Type subtype = subtypes[i]; - if ( subtype.isEntityType() ) { + if ( subtype instanceof EntityType ) { // the value of the copy in the MergeContext has the id assigned final Object o = mergeContext.get( propertyValues[i] ); if ( o != null ) { @@ -260,8 +262,11 @@ private static Object copyCompositeTypeId( copyValues[i] = subtype.deepCopy( propertyValues[i], sessionFactory ); } } - else if ( subtype.isComponentType() ) { - copyValues[i] = copyCompositeTypeId( propertyValues[i], (CompositeType) subtype, session, mergeContext ); + else if ( subtype instanceof AnyType ) { + copyValues[i] = copyCompositeTypeId( propertyValues[i], (AnyType) subtype, session, mergeContext ); + } + else if ( subtype instanceof ComponentType ) { + copyValues[i] = copyCompositeTypeId( propertyValues[i], (ComponentType) subtype, session, mergeContext ); } else { copyValues[i] = subtype.deepCopy( propertyValues[i], sessionFactory ); diff --git a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultRefreshEventListener.java b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultRefreshEventListener.java index a01cd40015bb..797f28b70972 100644 --- a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultRefreshEventListener.java +++ b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultRefreshEventListener.java @@ -12,6 +12,8 @@ import org.hibernate.NonUniqueObjectException; import org.hibernate.TransientObjectException; import org.hibernate.UnresolvableObjectException; +import org.hibernate.bytecode.enhance.spi.interceptor.LazyAttributeLoadingInterceptor; +import org.hibernate.bytecode.spi.BytecodeEnhancementMetadata; import org.hibernate.cache.spi.access.CollectionDataAccess; import org.hibernate.cache.spi.access.EntityDataAccess; import org.hibernate.cache.spi.access.SoftLock; @@ -36,6 +38,7 @@ import org.hibernate.proxy.HibernateProxy; import org.hibernate.proxy.LazyInitializer; import org.hibernate.type.CollectionType; +import org.hibernate.type.ComponentType; import org.hibernate.type.CompositeType; import org.hibernate.type.Type; @@ -193,6 +196,14 @@ private static void refresh( EntityEntry entry, Object id, PersistenceContext persistenceContext) { + final BytecodeEnhancementMetadata instrumentationMetadata = persister.getInstrumentationMetadata(); + if ( object != null && instrumentationMetadata.isEnhancedForLazyLoading() ) { + final LazyAttributeLoadingInterceptor interceptor = instrumentationMetadata.extractInterceptor( object ); + if ( interceptor != null ) { + // The list of initialized lazy fields have to be cleared in order to refresh them from the database. + interceptor.clearInitializedLazyFields(); + } + } final Object result = source.getLoadQueryInfluencers().fromInternalFetchProfile( CascadingFetchProfile.REFRESH, @@ -315,7 +326,7 @@ private static void evictCachedCollections(Type[] types, Object id, EventSource final SessionFactoryImplementor factory = source.getFactory(); final MappingMetamodelImplementor metamodel = factory.getRuntimeMetamodels().getMappingMetamodel(); for ( Type type : types ) { - if ( type.isCollectionType() ) { + if ( type instanceof CollectionType ) { final String role = ((CollectionType) type).getRole(); final CollectionPersister collectionPersister = metamodel.getCollectionDescriptor( role ); if ( collectionPersister.hasCache() ) { @@ -331,8 +342,9 @@ private static void evictCachedCollections(Type[] types, Object id, EventSource actionQueue.registerProcess( (success, session) -> cache.unlockItem( session, ck, lock ) ); } } - else if ( type.isComponentType() ) { - final CompositeType compositeType = (CompositeType) type; + else if ( type instanceof ComponentType ) { + // Only components can contain collections + ComponentType compositeType = (ComponentType) type; evictCachedCollections( compositeType.getSubtypes(), id, source ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/event/spi/MergeContext.java b/hibernate-core/src/main/java/org/hibernate/event/spi/MergeContext.java index c9a83e3e45ab..17cf332e617a 100644 --- a/hibernate-core/src/main/java/org/hibernate/event/spi/MergeContext.java +++ b/hibernate-core/src/main/java/org/hibernate/event/spi/MergeContext.java @@ -366,4 +366,8 @@ private String printEntity(Object entity) { // Entity was not found in current persistence context. Use Object#toString() method. return "[" + entity + "]"; } + + public EventSource getEventSource() { + return session; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/generator/GeneratorCreationContext.java b/hibernate-core/src/main/java/org/hibernate/generator/GeneratorCreationContext.java index 094f0c1e2c9b..e47b146ad8dc 100644 --- a/hibernate-core/src/main/java/org/hibernate/generator/GeneratorCreationContext.java +++ b/hibernate-core/src/main/java/org/hibernate/generator/GeneratorCreationContext.java @@ -12,15 +12,41 @@ import org.hibernate.mapping.Property; import org.hibernate.service.ServiceRegistry; +/** + * Access to information useful during {@linkplain Generator} creation and initialization. + * + * @see AnnotationBasedGenerator + * @see org.hibernate.id.Configurable#create(GeneratorCreationContext) + */ @Incubating public interface GeneratorCreationContext { + /** + * View of the relational database objects (tables, sequences, ...) and namespaces (catalogs and schemas). + */ Database getDatabase(); + + /** + * Access to available services. + */ ServiceRegistry getServiceRegistry(); + /** + * The default catalog name, if one. + */ String getDefaultCatalog(); + + /** + * The default schema name, if one. + */ String getDefaultSchema(); + /** + * Mapping details for the entity; may be null in the case of and id-bag id generator. + */ PersistentClass getPersistentClass(); + /** + * The entity identifier or id-bag property details. + */ Property getProperty(); } diff --git a/hibernate-core/src/main/java/org/hibernate/id/Assigned.java b/hibernate-core/src/main/java/org/hibernate/id/Assigned.java index ffce159958ee..22ebc92abccb 100644 --- a/hibernate-core/src/main/java/org/hibernate/id/Assigned.java +++ b/hibernate-core/src/main/java/org/hibernate/id/Assigned.java @@ -43,4 +43,9 @@ public void configure(Type type, Properties parameters, ServiceRegistry serviceR throw new MappingException("no entity name"); } } + + @Override + public boolean allowAssignedIdentifiers() { + return true; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/id/CompositeNestedGeneratedValueGenerator.java b/hibernate-core/src/main/java/org/hibernate/id/CompositeNestedGeneratedValueGenerator.java index d463248b4868..b140f45b4e44 100644 --- a/hibernate-core/src/main/java/org/hibernate/id/CompositeNestedGeneratedValueGenerator.java +++ b/hibernate-core/src/main/java/org/hibernate/id/CompositeNestedGeneratedValueGenerator.java @@ -16,10 +16,13 @@ import org.hibernate.boot.model.relational.ExportableProducer; import org.hibernate.boot.model.relational.SqlStringGenerationContext; import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.generator.BeforeExecutionGenerator; import org.hibernate.id.factory.spi.StandardGenerator; import org.hibernate.property.access.spi.Setter; import org.hibernate.type.CompositeType; +import static org.hibernate.generator.EventType.INSERT; + /** * For composite identifiers, defines a number of "nested" generations that * need to happen to "fill" the identifier property(s). @@ -74,7 +77,6 @@ public interface GenerationContextLocator { * determined {@linkplain GenerationContextLocator#locateGenerationContext context} */ public interface GenerationPlan extends ExportableProducer { - /** * Initializes this instance, in particular pre-generates SQL as necessary. *

    @@ -85,12 +87,9 @@ public interface GenerationPlan extends ExportableProducer { void initialize(SqlStringGenerationContext context); /** - * Execute the value generation. - * - * @param session The current session - * @param incomingObject The entity for which we are generating id + * Retrieve the generator for this generation plan */ - Object execute(SharedSessionContractImplementor session, Object incomingObject); + BeforeExecutionGenerator getGenerator(); /** * Returns the {@link Setter injector} for the generated property. @@ -132,7 +131,17 @@ public Object generate(SharedSessionContractImplementor session, Object object) null : new ArrayList<>( generationPlans.size() ); for ( GenerationPlan generationPlan : generationPlans ) { - final Object generated = generationPlan.execute( session, object ); + final BeforeExecutionGenerator generator = generationPlan.getGenerator(); + final Object generated; + if ( !generator.generatedOnExecution( object, session ) ) { + final Object currentValue = generator.allowAssignedIdentifiers() + ? compositeType.getPropertyValue( context, generationPlan.getPropertyIndex(), session ) + : null; + generated = generator.generate( session, object, currentValue, INSERT ); + } + else { + throw new IdentifierGenerationException( "Identity generation isn't supported for composite ids" ); + } if ( generatedValues != null ) { generatedValues.add( generated ); } diff --git a/hibernate-core/src/main/java/org/hibernate/id/Configurable.java b/hibernate-core/src/main/java/org/hibernate/id/Configurable.java index a8238430a2e2..b43accdcb224 100644 --- a/hibernate-core/src/main/java/org/hibernate/id/Configurable.java +++ b/hibernate-core/src/main/java/org/hibernate/id/Configurable.java @@ -8,19 +8,38 @@ import java.util.Properties; +import org.hibernate.Incubating; import org.hibernate.MappingException; import org.hibernate.boot.model.relational.Database; import org.hibernate.boot.model.relational.SqlStringGenerationContext; +import org.hibernate.generator.GeneratorCreationContext; import org.hibernate.service.ServiceRegistry; import org.hibernate.type.Type; /** * A {@link org.hibernate.generator.Generator} that supports "configuration". * + * @apiNote Prefer instead using either

      + *
    • {@linkplain org.hibernate.annotations.IdGeneratorType}
    • , or + *
    • {@linkplain org.hibernate.generator.AnnotationBasedGenerator} (in either implementation or constructor-injection form)
    • + *
    + * * @author Gavin King * @author Steve Ebersole */ public interface Configurable { + /** + * Called before {@link #configure(Type, Properties, ServiceRegistry)}, + * with an instance of {@link GeneratorCreationContext}. + * + * @since 6.6 + * @deprecated Added in 6.6 as a short-term work-around, but should really use on + * of the alternatives discussed at {@linkplain Configurable}. + * See HHH-18615. + */ + @Deprecated(forRemoval = true) + default void create(GeneratorCreationContext creationContext) throws MappingException {} + /** * Configure this instance, given the value of parameters * specified by the user as XML {@code } elements and diff --git a/hibernate-core/src/main/java/org/hibernate/id/ForeignGenerator.java b/hibernate-core/src/main/java/org/hibernate/id/ForeignGenerator.java index b02bf8746bf8..aa53cdaced8d 100644 --- a/hibernate-core/src/main/java/org/hibernate/id/ForeignGenerator.java +++ b/hibernate-core/src/main/java/org/hibernate/id/ForeignGenerator.java @@ -99,7 +99,7 @@ public Object generate(SharedSessionContractImplementor sessionImplementor, Obje final EntityType foreignValueSourceType; final Type propertyType = entityDescriptor.getPropertyType( propertyName ); - if ( propertyType.isEntityType() ) { + if ( propertyType instanceof EntityType ) { // the normal case foreignValueSourceType = (EntityType) propertyType; } diff --git a/hibernate-core/src/main/java/org/hibernate/id/IdentityGenerator.java b/hibernate-core/src/main/java/org/hibernate/id/IdentityGenerator.java index 407cab115e84..f07d21409f60 100644 --- a/hibernate-core/src/main/java/org/hibernate/id/IdentityGenerator.java +++ b/hibernate-core/src/main/java/org/hibernate/id/IdentityGenerator.java @@ -6,9 +6,10 @@ */ package org.hibernate.id; +import java.util.List; + import org.hibernate.boot.spi.SessionFactoryOptions; import org.hibernate.dialect.Dialect; -import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.generator.OnExecutionGenerator; import org.hibernate.id.factory.spi.StandardGenerator; import org.hibernate.id.insert.BasicSelectingDelegate; @@ -16,6 +17,7 @@ import org.hibernate.id.insert.InsertGeneratedIdentifierDelegate; import org.hibernate.id.insert.InsertReturningDelegate; import org.hibernate.id.insert.UniqueKeySelectingDelegate; +import org.hibernate.metamodel.mapping.ModelPart; import static org.hibernate.generator.EventType.INSERT; import static org.hibernate.generator.internal.NaturalIdHelper.getNaturalIdPropertyNames; @@ -54,11 +56,16 @@ public String[] getReferencedColumnValues(Dialect dialect) { @Override public InsertGeneratedIdentifierDelegate getGeneratedIdentifierDelegate(PostInsertIdentityPersister persister) { - final SessionFactoryImplementor factory = persister.getFactory(); - final Dialect dialect = factory.getJdbcServices().getDialect(); + final Dialect dialect = persister.getFactory().getJdbcServices().getDialect(); + final SessionFactoryOptions sessionFactoryOptions = persister.getFactory().getSessionFactoryOptions(); + final List generatedProperties = persister.getGeneratedProperties( INSERT ); + if ( generatedProperties.size() == 1 && sessionFactoryOptions.isGetGeneratedKeysEnabled() ) { + // Use Connection#prepareStatement(sql, Statement.RETURN_GENERATED_KEYS) when only retrieving identity + assert generatedProperties.get( 0 ).isEntityIdentifierMapping(); + return dialect.getIdentityColumnSupport().buildGetGeneratedKeysDelegate( persister ); + } // Try to use generic delegates if the dialects supports them - final SessionFactoryOptions sessionFactoryOptions = factory.getSessionFactoryOptions(); - if ( dialect.supportsInsertReturningGeneratedKeys() && sessionFactoryOptions.isGetGeneratedKeysEnabled() ) { + else if ( dialect.supportsInsertReturningGeneratedKeys() && sessionFactoryOptions.isGetGeneratedKeysEnabled() ) { return new GetGeneratedKeysDelegate( persister, false, INSERT ); } else if ( dialect.supportsInsertReturning() && noCustomSql( persister, INSERT ) ) { diff --git a/hibernate-core/src/main/java/org/hibernate/id/enhanced/SequenceStyleGenerator.java b/hibernate-core/src/main/java/org/hibernate/id/enhanced/SequenceStyleGenerator.java index 1e60c4ec7d10..329ccb17713c 100644 --- a/hibernate-core/src/main/java/org/hibernate/id/enhanced/SequenceStyleGenerator.java +++ b/hibernate-core/src/main/java/org/hibernate/id/enhanced/SequenceStyleGenerator.java @@ -15,6 +15,8 @@ import org.hibernate.HibernateException; import org.hibernate.MappingException; import org.hibernate.boot.model.naming.Identifier; +import org.hibernate.boot.model.naming.ObjectNameNormalizer; +import org.hibernate.boot.model.naming.PhysicalNamingStrategy; import org.hibernate.boot.model.relational.Database; import org.hibernate.boot.model.relational.QualifiedName; import org.hibernate.boot.model.relational.QualifiedNameParser; @@ -26,6 +28,7 @@ import org.hibernate.engine.config.spi.ConfigurationService; import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.generator.GeneratorCreationContext; import org.hibernate.id.BulkInsertionCapableIdentifierGenerator; import org.hibernate.id.IdentifierGenerator; import org.hibernate.id.PersistentIdentifierGenerator; @@ -163,6 +166,8 @@ public class SequenceStyleGenerator private Optimizer optimizer; private Type identifierType; + private PhysicalNamingStrategy physicalNamingStrategy; + /** * Getter for property 'databaseStructure'. * @@ -194,6 +199,11 @@ public Type getIdentifierType() { // Configurable implementation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + @Override + public void create(GeneratorCreationContext creationContext) throws MappingException { + physicalNamingStrategy = creationContext.getDatabase().getPhysicalNamingStrategy(); + } + @Override public void configure(Type type, Properties parameters, ServiceRegistry serviceRegistry) throws MappingException { final JdbcEnvironment jdbcEnvironment = serviceRegistry.requireService( JdbcEnvironment.class ); @@ -242,6 +252,9 @@ public void configure(Type type, Properties parameters, ServiceRegistry serviceR getInt( INITIAL_PARAM, parameters, -1 ) ); this.databaseStructure.configure( optimizer ); + + // we don't want or need this after initialization is complete + physicalNamingStrategy = null; } private int adjustIncrementSize( @@ -262,7 +275,9 @@ private int adjustIncrementSize( if ( sequenceMismatchStrategy != SequenceMismatchStrategy.NONE && optimizationStrategy.isPooled() && physicalSequence ) { - final String databaseSequenceName = sequenceName.getObjectName().getText(); + final String databaseSequenceName = physicalNamingStrategy != null + ? physicalNamingStrategy.toPhysicalSequenceName( sequenceName.getObjectName(), jdbcEnvironment ).getText() + : sequenceName.getObjectName().getText(); final Number databaseIncrementValue = isSchemaToBeRecreated( contributor, configurationService ) ? null : getSequenceIncrementValue( jdbcEnvironment, databaseSequenceName ); if ( databaseIncrementValue != null && databaseIncrementValue.intValue() != incrementSize) { final int dbIncrementValue = databaseIncrementValue.intValue(); diff --git a/hibernate-core/src/main/java/org/hibernate/id/factory/IdentifierGeneratorFactory.java b/hibernate-core/src/main/java/org/hibernate/id/factory/IdentifierGeneratorFactory.java index ec21727bbf2a..af06fb73e8ce 100644 --- a/hibernate-core/src/main/java/org/hibernate/id/factory/IdentifierGeneratorFactory.java +++ b/hibernate-core/src/main/java/org/hibernate/id/factory/IdentifierGeneratorFactory.java @@ -9,7 +9,7 @@ import java.util.Properties; import org.hibernate.Incubating; -import org.hibernate.dialect.Dialect; +import org.hibernate.generator.GeneratorCreationContext; import org.hibernate.id.IdentifierGenerator; import org.hibernate.id.factory.spi.GeneratorDefinitionResolver; import org.hibernate.service.Service; @@ -71,5 +71,21 @@ Generator createIdentifierGenerator( * @deprecated use {@link #createIdentifierGenerator(GenerationType, String, String, JavaType, Properties, GeneratorDefinitionResolver)} */ @Deprecated(since = "6.0") - Generator createIdentifierGenerator(String strategy, Type type, Properties parameters); + Generator createIdentifierGenerator(String strategy, Type type, GeneratorCreationContext creationContext, Properties parameters); + + /** + * Given a strategy, retrieve the appropriate identifier generator instance. + * + * @param strategy The generation strategy. + * @param type The mapping type for the identifier values. + * @param parameters Any parameters properties given in the generator mapping. + * + * @return The appropriate generator instance. + * + * @deprecated use {@link #createIdentifierGenerator(GenerationType, String, String, JavaType, Properties, GeneratorDefinitionResolver)} + */ + @Deprecated(since = "6.0") + default Generator createIdentifierGenerator(String strategy, Type type, Properties parameters) { + return createIdentifierGenerator( strategy, type, null, parameters ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/id/factory/internal/IdentifierGeneratorUtil.java b/hibernate-core/src/main/java/org/hibernate/id/factory/internal/IdentifierGeneratorUtil.java index 212cb3970a9e..b2f08bd9e1aa 100644 --- a/hibernate-core/src/main/java/org/hibernate/id/factory/internal/IdentifierGeneratorUtil.java +++ b/hibernate-core/src/main/java/org/hibernate/id/factory/internal/IdentifierGeneratorUtil.java @@ -6,10 +6,12 @@ */ package org.hibernate.id.factory.internal; +import org.hibernate.boot.model.relational.Database; import org.hibernate.cfg.AvailableSettings; import org.hibernate.dialect.Dialect; import org.hibernate.engine.config.spi.ConfigurationService; import org.hibernate.engine.config.spi.StandardConverters; +import org.hibernate.generator.GeneratorCreationContext; import org.hibernate.id.IdentifierGenerator; import org.hibernate.id.OptimizableGenerator; import org.hibernate.id.PersistentIdentifierGenerator; @@ -17,10 +19,13 @@ import org.hibernate.id.enhanced.SingleNamingStrategy; import org.hibernate.id.factory.IdentifierGeneratorFactory; import org.hibernate.mapping.Column; +import org.hibernate.mapping.PersistentClass; +import org.hibernate.mapping.Property; import org.hibernate.mapping.RootClass; import org.hibernate.mapping.SimpleValue; import org.hibernate.mapping.Table; import org.hibernate.generator.Generator; +import org.hibernate.service.ServiceRegistry; import java.util.Map; import java.util.Properties; @@ -37,6 +42,32 @@ public static Generator createLegacyIdentifierGenerator( return identifierGeneratorFactory.createIdentifierGenerator( simpleValue.getIdentifierGeneratorStrategy(), simpleValue.getType(), + new GeneratorCreationContext() { + @Override + public Database getDatabase() { + return simpleValue.getMetadata().getDatabase(); + } + @Override + public ServiceRegistry getServiceRegistry() { + return simpleValue.getServiceRegistry(); + } + @Override + public String getDefaultCatalog() { + return null; + } + @Override + public String getDefaultSchema() { + return null; + } + @Override + public PersistentClass getPersistentClass() { + return rootClass; + } + @Override + public Property getProperty() { + return rootClass.getIdentifierProperty(); + } + }, collectParameters( simpleValue, dialect, defaultCatalog, defaultSchema, rootClass ) ); } @@ -48,7 +79,7 @@ public static Properties collectParameters( String defaultSchema, RootClass rootClass) { final ConfigurationService configService = - simpleValue.getMetadata().getMetadataBuildingOptions().getServiceRegistry() + simpleValue.getServiceRegistry() .requireService( ConfigurationService.class ); final Properties params = new Properties(); diff --git a/hibernate-core/src/main/java/org/hibernate/id/factory/internal/StandardIdentifierGeneratorFactory.java b/hibernate-core/src/main/java/org/hibernate/id/factory/internal/StandardIdentifierGeneratorFactory.java index ed30caee7f94..c24de0b5d99f 100644 --- a/hibernate-core/src/main/java/org/hibernate/id/factory/internal/StandardIdentifierGeneratorFactory.java +++ b/hibernate-core/src/main/java/org/hibernate/id/factory/internal/StandardIdentifierGeneratorFactory.java @@ -19,6 +19,7 @@ import org.hibernate.engine.config.spi.ConfigurationService; import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; import org.hibernate.generator.Generator; +import org.hibernate.generator.GeneratorCreationContext; import org.hibernate.id.Assigned; import org.hibernate.id.Configurable; import org.hibernate.id.ForeignGenerator; @@ -206,7 +207,8 @@ private Dialect getDialect() { } @Override @Deprecated - public Generator createIdentifierGenerator(String strategy, Type type, Properties parameters) { + public Generator createIdentifierGenerator( + String strategy, Type type, GeneratorCreationContext creationContext, Properties parameters) { try { final Class clazz = getIdentifierGeneratorClass( strategy ); final Generator identifierGenerator; @@ -222,7 +224,11 @@ public Generator createIdentifierGenerator(String strategy, Type type, Propertie } if ( identifierGenerator instanceof Configurable ) { - ( (Configurable) identifierGenerator ).configure( type, parameters, serviceRegistry ); + final Configurable configurable = (Configurable) identifierGenerator; + if ( creationContext != null ) { + configurable.create( creationContext ); + } + configurable.configure( type, parameters, serviceRegistry ); } return identifierGenerator; } diff --git a/hibernate-core/src/main/java/org/hibernate/id/insert/AbstractSelectingDelegate.java b/hibernate-core/src/main/java/org/hibernate/id/insert/AbstractSelectingDelegate.java index 4c2ad98a2539..74013f1fafa8 100644 --- a/hibernate-core/src/main/java/org/hibernate/id/insert/AbstractSelectingDelegate.java +++ b/hibernate-core/src/main/java/org/hibernate/id/insert/AbstractSelectingDelegate.java @@ -106,7 +106,6 @@ public GeneratedValues performMutation( statementDetails.releaseStatement( session ); } jdbcValueBindings.afterStatement( statementDetails.getMutatingTableDetails() ); - session.getJdbcCoordinator().afterStatementExecution(); } // the insert is complete, select the generated id... diff --git a/hibernate-core/src/main/java/org/hibernate/id/insert/GetGeneratedKeysDelegate.java b/hibernate-core/src/main/java/org/hibernate/id/insert/GetGeneratedKeysDelegate.java index 63d8c2089453..3d823a9cf9c5 100644 --- a/hibernate-core/src/main/java/org/hibernate/id/insert/GetGeneratedKeysDelegate.java +++ b/hibernate-core/src/main/java/org/hibernate/id/insert/GetGeneratedKeysDelegate.java @@ -153,7 +153,6 @@ public GeneratedValues performMutation( statementDetails.releaseStatement( session ); } jdbcValueBindings.afterStatement( statementDetails.getMutatingTableDetails() ); - jdbcCoordinator.afterStatementExecution(); } } diff --git a/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java b/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java index 3a3e80c088dc..e24c4a54d7c1 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java @@ -19,6 +19,7 @@ import java.util.function.Function; import jakarta.persistence.EntityGraph; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.CacheMode; import org.hibernate.EntityNameResolver; import org.hibernate.Filter; @@ -891,22 +892,8 @@ public QueryImplementor createQuery(String queryString, Class expected // dynamic native (SQL) query handling @Override @SuppressWarnings("rawtypes") - public NativeQueryImpl createNativeQuery(String sqlString) { - checkOpen(); - pulseTransactionCoordinator(); - delayedAfterCompletion(); - - try { - final NativeQueryImpl query = new NativeQueryImpl<>( sqlString, this ); - if ( isEmpty( query.getComment() ) ) { - query.setComment( "dynamic native SQL query" ); - } - applyQuerySettingsAndHints( query ); - return query; - } - catch (RuntimeException he) { - throw getExceptionConverter().convert( he ); - } + public NativeQueryImplementor createNativeQuery(String sqlString) { + return createNativeQuery( sqlString, (Class) null ); } @Override @SuppressWarnings("rawtypes") @@ -939,12 +926,28 @@ protected NamedResultSetMappingMemento getResultSetMappingMemento(String resultS @Override @SuppressWarnings({"rawtypes", "unchecked"}) //note: we're doing something a bit funny here to work around // the clashing signatures declared by the supertypes - public NativeQueryImplementor createNativeQuery(String sqlString, Class resultClass) { - final NativeQueryImpl query = createNativeQuery( sqlString ); - addResultType( resultClass, query ); - return query; + public NativeQueryImplementor createNativeQuery(String sqlString, @Nullable Class resultClass) { + checkOpen(); + pulseTransactionCoordinator(); + delayedAfterCompletion(); + + try { + final NativeQueryImpl query = new NativeQueryImpl<>( sqlString, resultClass, this ); + if ( isEmpty( query.getComment() ) ) { + query.setComment( "dynamic native SQL query" ); + } + applyQuerySettingsAndHints( query ); + return query; + } + catch (RuntimeException he) { + throw getExceptionConverter().convert( he ); + } } + /** + * @deprecated Use {@link NativeQueryImpl#NativeQueryImpl(String, Class, SharedSessionContractImplementor)} instead + */ + @Deprecated(forRemoval = true) protected void addResultType(Class resultClass, NativeQueryImplementor query) { if ( Tuple.class.equals( resultClass ) ) { query.setTupleTransformer( NativeQueryTupleTransformer.INSTANCE ); diff --git a/hibernate-core/src/main/java/org/hibernate/internal/CoordinatingEntityNameResolver.java b/hibernate-core/src/main/java/org/hibernate/internal/CoordinatingEntityNameResolver.java index d325f36cb528..2ba0bfb3e1c4 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/CoordinatingEntityNameResolver.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/CoordinatingEntityNameResolver.java @@ -30,15 +30,15 @@ public String resolveEntityName(Object entity) { return entityName; } - final MappingMetamodelImplementor mappingMetamodel = sessionFactory.getRuntimeMetamodels().getMappingMetamodel(); - for ( EntityNameResolver resolver : mappingMetamodel.getEntityNameResolvers() ) { + for ( EntityNameResolver resolver : sessionFactory.getSessionFactoryOptions().getEntityNameResolvers() ) { entityName = resolver.resolveEntityName( entity ); if ( entityName != null ) { return entityName; } } - for ( EntityNameResolver resolver : sessionFactory.getSessionFactoryOptions().getEntityNameResolvers() ) { + final MappingMetamodelImplementor mappingMetamodel = sessionFactory.getRuntimeMetamodels().getMappingMetamodel(); + for ( EntityNameResolver resolver : mappingMetamodel.getEntityNameResolvers() ) { entityName = resolver.resolveEntityName( entity ); if ( entityName != null ) { return entityName; diff --git a/hibernate-core/src/main/java/org/hibernate/internal/SessionFactoryImpl.java b/hibernate-core/src/main/java/org/hibernate/internal/SessionFactoryImpl.java index 4420b3e672b9..9ca7b6b571eb 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/SessionFactoryImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/SessionFactoryImpl.java @@ -76,6 +76,7 @@ import org.hibernate.jpa.internal.ExceptionMapperLegacyJpaImpl; import org.hibernate.jpa.internal.PersistenceUnitUtilImpl; import org.hibernate.mapping.Collection; +import org.hibernate.mapping.KeyValue; import org.hibernate.mapping.PersistentClass; import org.hibernate.mapping.RootClass; import org.hibernate.mapping.SimpleValue; @@ -462,7 +463,8 @@ private static Map createGenerators( bootMetamodel.getEntityBindings().stream() .filter( model -> !model.isInherited() ) .forEach( model -> { - final Generator generator = model.getIdentifier().createGenerator( + final KeyValue id = model.getIdentifier(); + final Generator generator = id.createGenerator( bootstrapContext.getIdentifierGeneratorFactory(), jdbcServices.getJdbcEnvironment().getDialect(), (RootClass) model @@ -471,8 +473,11 @@ private static Map createGenerators( final Configurable identifierGenerator = (Configurable) generator; identifierGenerator.initialize( sqlStringGenerationContext ); } - if ( generator.allowAssignedIdentifiers() ) { - ( (SimpleValue) model.getIdentifier() ).setNullValue( "undefined" ); + if ( generator.allowAssignedIdentifiers() && id instanceof SimpleValue ) { + final SimpleValue simpleValue = (SimpleValue) id; + if ( simpleValue.getNullValue() == null ) { + simpleValue.setNullValue( "undefined" ); + } } generators.put( model.getEntityName(), generator ); } ); diff --git a/hibernate-core/src/main/java/org/hibernate/internal/StatelessSessionImpl.java b/hibernate-core/src/main/java/org/hibernate/internal/StatelessSessionImpl.java index a7fa4669aa51..fca12f716f74 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/StatelessSessionImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/StatelessSessionImpl.java @@ -131,7 +131,8 @@ public Object insert(String entityName, Object entity) { } final Generator generator = persister.getGenerator(); if ( !generator.generatedOnExecution( entity, this ) ) { - id = ( (BeforeExecutionGenerator) generator).generate( this, entity, null, INSERT ); + final Object currentValue = generator.allowAssignedIdentifiers() ? persister.getIdentifier( entity, this ) : null; + id = ( (BeforeExecutionGenerator) generator ).generate( this, entity, currentValue, INSERT ); if ( firePreInsert(entity, id, state, persister) ) { return id; } diff --git a/hibernate-core/src/main/java/org/hibernate/internal/util/EntityPrinter.java b/hibernate-core/src/main/java/org/hibernate/internal/util/EntityPrinter.java index 12ccc44062e5..78618d829302 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/util/EntityPrinter.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/util/EntityPrinter.java @@ -15,6 +15,7 @@ import org.hibernate.engine.spi.EntityHolder; import org.hibernate.engine.spi.EntityKey; import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.engine.spi.TypedValue; import org.hibernate.internal.CoreLogging; import org.hibernate.internal.CoreMessageLogger; @@ -53,7 +54,7 @@ public String toString(String entityName, Object entity) throws HibernateExcepti result.put( entityPersister.getIdentifierPropertyName(), entityPersister.getIdentifierType().toLoggableString( - entityPersister.getIdentifier( entity, null ), + entityPersister.getIdentifier( entity, (SharedSessionContractImplementor) null ), factory ) ); diff --git a/hibernate-core/src/main/java/org/hibernate/jpa/boot/internal/EntityManagerFactoryBuilderImpl.java b/hibernate-core/src/main/java/org/hibernate/jpa/boot/internal/EntityManagerFactoryBuilderImpl.java index c089097949a0..ff2cbe9d3cf4 100644 --- a/hibernate-core/src/main/java/org/hibernate/jpa/boot/internal/EntityManagerFactoryBuilderImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/jpa/boot/internal/EntityManagerFactoryBuilderImpl.java @@ -57,6 +57,7 @@ import org.hibernate.bytecode.enhance.spi.EnhancementException; import org.hibernate.bytecode.enhance.spi.UnloadedClass; import org.hibernate.bytecode.enhance.spi.UnloadedField; +import org.hibernate.bytecode.spi.BytecodeProvider; import org.hibernate.bytecode.spi.ClassTransformer; import org.hibernate.cfg.AvailableSettings; import org.hibernate.cfg.Environment; @@ -128,6 +129,7 @@ import static org.hibernate.cfg.AvailableSettings.TRANSACTION_COORDINATOR_STRATEGY; import static org.hibernate.cfg.AvailableSettings.URL; import static org.hibernate.cfg.AvailableSettings.USER; +import static org.hibernate.cfg.BytecodeSettings.BYTECODE_PROVIDER_INSTANCE; import static org.hibernate.cfg.BytecodeSettings.ENHANCER_ENABLE_ASSOCIATION_MANAGEMENT; import static org.hibernate.cfg.BytecodeSettings.ENHANCER_ENABLE_DIRTY_TRACKING; import static org.hibernate.cfg.BytecodeSettings.ENHANCER_ENABLE_LAZY_INITIALIZATION; @@ -443,6 +445,11 @@ protected EnhancementContext getEnhancementContext( final boolean dirtyTrackingEnabled, final boolean lazyInitializationEnabled, final boolean associationManagementEnabled ) { + final Object propValue = configurationValues.get( BYTECODE_PROVIDER_INSTANCE ); + if ( propValue != null && ( ! ( propValue instanceof BytecodeProvider ) ) ) { + throw persistenceException( "Property " + BYTECODE_PROVIDER_INSTANCE + " was set to '" + propValue + "', which is not compatible with the expected type " + BytecodeProvider.class ); + } + final BytecodeProvider overriddenBytecodeProvider = (BytecodeProvider) propValue; return new DefaultEnhancementContext() { @Override @@ -483,6 +490,10 @@ public boolean doExtendedEnhancement(UnloadedClass classDescriptor) { return false; } + @Override + public BytecodeProvider getBytecodeProvider() { + return overriddenBytecodeProvider; + } }; } diff --git a/hibernate-core/src/main/java/org/hibernate/jpa/boot/internal/PersistenceUnitInfoDescriptor.java b/hibernate-core/src/main/java/org/hibernate/jpa/boot/internal/PersistenceUnitInfoDescriptor.java index 8b5e2b2a79f0..b3803166cef7 100644 --- a/hibernate-core/src/main/java/org/hibernate/jpa/boot/internal/PersistenceUnitInfoDescriptor.java +++ b/hibernate-core/src/main/java/org/hibernate/jpa/boot/internal/PersistenceUnitInfoDescriptor.java @@ -7,26 +7,29 @@ package org.hibernate.jpa.boot.internal; import java.net.URL; -import java.util.ArrayList; -import java.util.Collection; import java.util.List; import java.util.Properties; +import org.hibernate.bytecode.enhance.spi.EnhancementContext; +import org.hibernate.bytecode.spi.ClassTransformer; +import org.hibernate.internal.CoreLogging; +import org.hibernate.internal.CoreMessageLogger; +import org.hibernate.jpa.boot.spi.PersistenceUnitDescriptor; +import org.hibernate.jpa.internal.enhance.EnhancingClassTransformerImpl; + import jakarta.persistence.PersistenceException; import jakarta.persistence.SharedCacheMode; import jakarta.persistence.ValidationMode; import jakarta.persistence.spi.PersistenceUnitInfo; import jakarta.persistence.spi.PersistenceUnitTransactionType; -import org.hibernate.bytecode.enhance.spi.EnhancementContext; -import org.hibernate.bytecode.spi.ClassTransformer; -import org.hibernate.jpa.boot.spi.PersistenceUnitDescriptor; -import org.hibernate.jpa.internal.enhance.EnhancingClassTransformerImpl; - /** * @author Steve Ebersole */ public class PersistenceUnitInfoDescriptor implements PersistenceUnitDescriptor { + + private static final CoreMessageLogger LOGGER = CoreLogging.messageLogger( PersistenceUnitInfoDescriptor.class ); + private final PersistenceUnitInfo persistenceUnitInfo; private ClassTransformer classTransformer; @@ -121,6 +124,9 @@ public void pushClassTransformer(EnhancementContext enhancementContext) { } // During testing, we will return a null temp class loader in cases where we don't care about enhancement if ( persistenceUnitInfo.getNewTempClassLoader() != null ) { + if ( LOGGER.isDebugEnabled() ) { + LOGGER.debug( "Pushing class transformers for PU named '" + getName() + "' on loading classloader " + enhancementContext.getLoadingClassLoader() ); + } final EnhancingClassTransformerImpl classTransformer = new EnhancingClassTransformerImpl( enhancementContext ); this.classTransformer = classTransformer; persistenceUnitInfo.addTransformer( classTransformer ); diff --git a/hibernate-core/src/main/java/org/hibernate/jpa/internal/PersistenceUnitUtilImpl.java b/hibernate-core/src/main/java/org/hibernate/jpa/internal/PersistenceUnitUtilImpl.java index 2cfbe054cdf8..b3572934bcd7 100644 --- a/hibernate-core/src/main/java/org/hibernate/jpa/internal/PersistenceUnitUtilImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/jpa/internal/PersistenceUnitUtilImpl.java @@ -13,6 +13,7 @@ import org.hibernate.MappingException; import org.hibernate.engine.spi.EntityEntry; import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.jpa.internal.util.PersistenceUtilHelper; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.proxy.LazyInitializer; @@ -106,7 +107,7 @@ private Object getIdentifierFromPersister(Object entity) { catch (MappingException ex) { throw new IllegalArgumentException( entityClass.getName() + " is not an entity", ex ); } - return persister.getIdentifier( entity, null ); + return persister.getIdentifier( entity, (SharedSessionContractImplementor) null ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/jpa/internal/enhance/EnhancingClassTransformerImpl.java b/hibernate-core/src/main/java/org/hibernate/jpa/internal/enhance/EnhancingClassTransformerImpl.java index 77383de47070..bf5368927212 100644 --- a/hibernate-core/src/main/java/org/hibernate/jpa/internal/enhance/EnhancingClassTransformerImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/jpa/internal/enhance/EnhancingClassTransformerImpl.java @@ -11,6 +11,7 @@ import java.util.Objects; import java.util.concurrent.locks.ReentrantLock; +import org.hibernate.bytecode.enhance.internal.bytebuddy.CorePrefixFilter; import org.hibernate.bytecode.enhance.spi.EnhancementContext; import org.hibernate.bytecode.enhance.spi.EnhancementContextWrapper; import org.hibernate.bytecode.enhance.spi.Enhancer; @@ -31,13 +32,11 @@ public class EnhancingClassTransformerImpl implements ClassTransformer { private final ReentrantLock lock = new ReentrantLock(); private volatile WeakReference entryReference; - //This list is matching the constants used by CoreTypePool's default constructor - private static final String[] NO_TRANSFORM_PREFIXES = { "jakarta/", "java/", "org/hibernate/annotations/" }; - public EnhancingClassTransformerImpl(EnhancementContext enhancementContext) { Objects.requireNonNull( enhancementContext ); this.enhancementContext = enhancementContext; - this.bytecodeProvider = BytecodeProviderInitiator.buildDefaultBytecodeProvider(); + final BytecodeProvider overriddenProvider = enhancementContext.getBytecodeProvider(); + this.bytecodeProvider = overriddenProvider == null ? BytecodeProviderInitiator.buildDefaultBytecodeProvider() : overriddenProvider; } @Override @@ -48,14 +47,13 @@ public byte[] transform( ProtectionDomain protectionDomain, byte[] classfileBuffer) throws TransformerException { - //Take care to not transform certain types; this is both an optimisation (we can skip this unnecessary work) - //and a safety precaution as we otherwise risk attempting to redefine classes which have already been loaded: - //see https://hibernate.atlassian.net/browse/HHH-18108 - //N.B. this className doesn't use the dot-format but the slashes for package separators. - for ( String prefix : NO_TRANSFORM_PREFIXES ) { - if ( className.startsWith( prefix ) ) { - return null; - } + //N.B. the "className" argument doesn't use the dot-format but the slashes for package separators. + final String classNameDotFormat = className.replace( '/', '.' ); + if ( CorePrefixFilter.DEFAULT_INSTANCE.isCoreClassName( classNameDotFormat ) ) { + //Take care to not transform certain types; this is both an optimisation (we can skip this unnecessary work) + //and a safety precaution as we otherwise risk attempting to redefine classes which have already been loaded: + //see https://hibernate.atlassian.net/browse/HHH-18108 + return null; } try { @@ -64,9 +62,6 @@ public byte[] transform( catch (final Exception e) { throw new TransformerException( "Error performing enhancement of " + className, e ); } - finally { - bytecodeProvider.resetCaches(); - } } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/jpa/spi/NativeQueryArrayTransformer.java b/hibernate-core/src/main/java/org/hibernate/jpa/spi/NativeQueryArrayTransformer.java new file mode 100644 index 000000000000..bcadd9d5b36e --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/jpa/spi/NativeQueryArrayTransformer.java @@ -0,0 +1,22 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.jpa.spi; + +import org.hibernate.query.TupleTransformer; + +/** + * A {@link TupleTransformer} for handling {@code Object[]} results from native queries. + */ +public class NativeQueryArrayTransformer implements TupleTransformer { + + public static final NativeQueryArrayTransformer INSTANCE = new NativeQueryArrayTransformer(); + + @Override + public Object[] transformTuple(Object[] tuple, String[] aliases) { + return tuple; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CacheEntityLoaderHelper.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CacheEntityLoaderHelper.java index 833cc62b96f4..424c78789c70 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CacheEntityLoaderHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CacheEntityLoaderHelper.java @@ -35,7 +35,7 @@ import org.hibernate.internal.CoreMessageLogger; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.pretty.MessageHelper; -import org.hibernate.proxy.HibernateProxy; +import org.hibernate.proxy.LazyInitializer; import org.hibernate.sql.results.LoadingLogger; import org.hibernate.stat.internal.StatsHelper; import org.hibernate.stat.spi.StatisticsImplementor; @@ -47,6 +47,7 @@ import static org.hibernate.engine.internal.ManagedTypeHelper.isPersistentAttributeInterceptable; import static org.hibernate.engine.internal.Versioning.getVersion; import static org.hibernate.loader.ast.internal.LoaderHelper.upgradeLock; +import static org.hibernate.proxy.HibernateProxy.extractLazyInitializer; /** * @author Vlad Mihalcea @@ -388,7 +389,6 @@ private Object convertCacheEntryToEntity( EntityKey entityKey) { final SessionFactoryImplementor factory = source.getFactory(); - final EntityPersister subclassPersister; if ( LOG.isTraceEnabled() ) { LOG.tracef( @@ -398,19 +398,20 @@ private Object convertCacheEntryToEntity( ); } - final Object entity; + final EntityPersister subclassPersister = + factory.getRuntimeMetamodels().getMappingMetamodel() + .getEntityDescriptor( entry.getSubclass() ); + final PersistenceContext persistenceContext = source.getPersistenceContextInternal(); + final EntityHolder oldHolder = persistenceContext.getEntityHolder( entityKey ); - subclassPersister = factory.getRuntimeMetamodels() - .getMappingMetamodel() - .getEntityDescriptor( entry.getSubclass() ); + final Object entity; if ( instanceToLoad != null ) { entity = instanceToLoad; } else { - final EntityHolder holder = source.getPersistenceContextInternal().getEntityHolder( entityKey ); - if ( holder != null && holder.getEntity() != null ) { + if ( oldHolder != null && oldHolder.getEntity() != null ) { // Use the entity which might already be - entity = holder.getEntity(); + entity = oldHolder.getEntity(); } else { entity = source.instantiate( subclassPersister, entityId ); @@ -430,23 +431,39 @@ private Object convertCacheEntryToEntity( } // make it circular-reference safe - TwoPhaseLoad.addUninitializedCachedEntity( - entityKey, - entity, - subclassPersister, - LockMode.NONE, - entry.getVersion(), - source - ); - - final PersistenceContext persistenceContext = source.getPersistenceContext(); - final Object[] values; - final Object version; + final EntityHolder holder = persistenceContext.addEntityHolder( entityKey, entity ); + final Object proxy = holder.getProxy(); final boolean isReadOnly; + if ( proxy != null ) { + // there is already a proxy for this impl + // only set the status to read-only if the proxy is read-only + final LazyInitializer lazyInitializer = extractLazyInitializer( proxy ); + assert lazyInitializer != null; + lazyInitializer.setImplementation( entity ); + + isReadOnly = lazyInitializer.isReadOnly(); + } + else { + isReadOnly = source.isDefaultReadOnly(); + } + holder.setEntityEntry( + persistenceContext.addEntry( + entity, + Status.LOADING, + null, + null, + entityKey.getIdentifier(), + entry.getVersion(), + LockMode.NONE, + true, + persister, + false + ) + ); final Type[] types = subclassPersister.getPropertyTypes(); // initializes the entity by (desired) side-effect - values = ( (StandardCacheEntryImpl) entry ).assemble( + final Object[] values = ( (StandardCacheEntryImpl) entry ).assemble( entity, entityId, subclassPersister, @@ -462,32 +479,23 @@ private Object convertCacheEntryToEntity( source ); } - version = getVersion( values, subclassPersister ); + final Object version = getVersion( values, subclassPersister ); LOG.tracef( "Cached Version : %s", version ); - final Object proxy = persistenceContext.getProxy( entityKey ); - if ( proxy != null ) { - // there is already a proxy for this impl - // only set the status to read-only if the proxy is read-only - isReadOnly = HibernateProxy.extractLazyInitializer( proxy ).isReadOnly(); - } - else { - isReadOnly = source.isDefaultReadOnly(); - } - - EntityEntry entityEntry = persistenceContext.addEntry( - entity, - ( isReadOnly ? Status.READ_ONLY : Status.MANAGED ), - values, - null, - entityId, - version, - LockMode.NONE, - true, - subclassPersister, - false + holder.setEntityEntry( + persistenceContext.addEntry( + entity, + isReadOnly ? Status.READ_ONLY : Status.MANAGED, + values, + null, + entityId, + version, + LockMode.NONE, + true, + subclassPersister, + false + ) ); - persistenceContext.getEntityHolder( entityKey ).setEntityEntry( entityEntry ); subclassPersister.afterInitialize( entity, source ); persistenceContext.initializeNonLazyCollections(); diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/LoaderSelectBuilder.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/LoaderSelectBuilder.java index 0aacc3f7dc24..e25ec9a52bd5 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/LoaderSelectBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/LoaderSelectBuilder.java @@ -30,7 +30,6 @@ import org.hibernate.metamodel.mapping.AttributeMapping; import org.hibernate.metamodel.mapping.CollectionPart; import org.hibernate.metamodel.mapping.EntityIdentifierMapping; -import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.EntityValuedModelPart; import org.hibernate.metamodel.mapping.ForeignKeyDescriptor; import org.hibernate.metamodel.mapping.ModelPart; @@ -963,19 +962,6 @@ else if ( fetchDepth > maximumFetchDepth + 1 ) { creationState ); - if ( fetch.getTiming() == FetchTiming.IMMEDIATE && joined ) { - if ( isFetchablePluralAttributeMapping ) { - final PluralAttributeMapping pluralAttributeMapping = (PluralAttributeMapping) fetchable; - final QuerySpec querySpec = creationState.getInflightQueryPart().getFirstQuerySpec(); - applyOrdering( - querySpec, - fetchablePath, - pluralAttributeMapping, - creationState - ); - } - } - fetches.add( fetch ); } finally { @@ -1010,19 +996,6 @@ else if ( fetchable instanceof PluralAttributeMapping ) { return true; } - private void applyOrdering( - QuerySpec querySpec, - NavigablePath navigablePath, - PluralAttributeMapping pluralAttributeMapping, - LoaderSqlAstCreationState sqlAstCreationState) { - assert pluralAttributeMapping.getAttributeName().equals( navigablePath.getLocalName() ); - - final TableGroup tableGroup = sqlAstCreationState.getFromClauseAccess().getTableGroup( navigablePath ); - assert tableGroup != null; - - applyOrdering( querySpec, tableGroup, pluralAttributeMapping, sqlAstCreationState ); - } - private SelectStatement generateSelect(SubselectFetch subselect) { // todo (6.0) : we could even convert this to a join by piecing together diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/LoaderSqlAstCreationState.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/LoaderSqlAstCreationState.java index ae03685bcf61..f7377d4dcd1c 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/LoaderSqlAstCreationState.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/LoaderSqlAstCreationState.java @@ -24,6 +24,7 @@ import org.hibernate.metamodel.mapping.AssociationKey; import org.hibernate.metamodel.mapping.ForeignKeyDescriptor; import org.hibernate.metamodel.mapping.ModelPart; +import org.hibernate.metamodel.mapping.ordering.OrderByFragment; import org.hibernate.query.spi.Limit; import org.hibernate.query.sqm.tree.from.SqmFrom; import org.hibernate.spi.NavigablePath; @@ -41,8 +42,10 @@ import org.hibernate.sql.ast.spi.SqlAstQueryPartProcessingState; import org.hibernate.sql.ast.spi.SqlExpressionResolver; import org.hibernate.sql.ast.tree.from.FromClause; +import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.ast.tree.predicate.Predicate; import org.hibernate.sql.ast.tree.select.QueryPart; +import org.hibernate.sql.ast.tree.select.QuerySpec; import org.hibernate.sql.results.graph.DomainResultCreationState; import org.hibernate.sql.results.graph.FetchParent; import org.hibernate.sql.results.graph.internal.ImmutableFetchList; @@ -94,6 +97,13 @@ public LoaderSqlAstCreationState( ); } + @Override + public void applyOrdering(TableGroup tableGroup, OrderByFragment orderByFragment) { + final QuerySpec querySpec = getInflightQueryPart().getFirstQuerySpec(); + assert querySpec.isRoot() : "Illegal attempt to apply order-by fragment to a non-root query spec"; + orderByFragment.apply( querySpec, tableGroup, this ); + } + @Override public SqlAstCreationContext getCreationContext() { return sf; diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/BasicValue.java b/hibernate-core/src/main/java/org/hibernate/mapping/BasicValue.java index e9abeef45b90..f347e5130f43 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/BasicValue.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/BasicValue.java @@ -40,7 +40,6 @@ import org.hibernate.engine.jdbc.Size; import org.hibernate.internal.CoreLogging; import org.hibernate.internal.CoreMessageLogger; -import org.hibernate.internal.util.ReflectHelper; import org.hibernate.internal.util.StringHelper; import org.hibernate.internal.util.collections.CollectionHelper; import org.hibernate.metamodel.mapping.JdbcMapping; @@ -82,6 +81,7 @@ import jakarta.persistence.TemporalType; import static java.lang.Boolean.parseBoolean; +import static org.hibernate.internal.util.ReflectHelper.reflectedPropertyType; import static org.hibernate.internal.util.collections.CollectionHelper.isNotEmpty; import static org.hibernate.mapping.MappingHelper.injectParameters; @@ -103,6 +103,7 @@ public class BasicValue extends SimpleValue implements JdbcTypeIndicators, Resol private Function implicitJavaTypeAccess; private EnumType enumerationStyle; + @SuppressWarnings("deprecation") private TemporalType temporalPrecision; private TimeZoneStorageType timeZoneStorageType; private boolean isSoftDelete; @@ -234,8 +235,7 @@ public java.lang.reflect.Type getResolvedJavaType() { public long getColumnLength() { final Selectable selectable = getColumn(); if ( selectable instanceof Column ) { - final Column column = (Column) selectable; - final Long length = column.getLength(); + final Long length = ( (Column) selectable ).getLength(); return length == null ? NO_COLUMN_LENGTH : length; } else { @@ -248,8 +248,9 @@ public int getColumnPrecision() { final Selectable selectable = getColumn(); if ( selectable instanceof Column ) { final Column column = (Column) selectable; - if ( column.getTemporalPrecision() != null ) { - return column.getTemporalPrecision(); + final Integer temporalPrecision = column.getTemporalPrecision(); + if ( temporalPrecision != null ) { + return temporalPrecision; } final Integer precision = column.getPrecision(); return precision == null ? NO_COLUMN_PRECISION : precision; @@ -263,8 +264,7 @@ public int getColumnPrecision() { public int getColumnScale() { final Selectable selectable = getColumn(); if ( selectable instanceof Column ) { - final Column column = (Column) selectable; - final Integer scale = column.getScale(); + final Integer scale = ( (Column) selectable ).getScale(); return scale == null ? NO_COLUMN_SCALE : scale; } else { @@ -284,8 +284,9 @@ public void copyTypeFrom(SimpleValue sourceValue) { super.copyTypeFrom( sourceValue ); if ( sourceValue instanceof BasicValue ) { final BasicValue basicValue = (BasicValue) sourceValue; - this.resolution = basicValue.resolution; - this.implicitJavaTypeAccess = (typeConfiguration) -> basicValue.implicitJavaTypeAccess.apply( typeConfiguration ); + resolution = basicValue.resolution; + implicitJavaTypeAccess = + typeConfiguration -> basicValue.implicitJavaTypeAccess.apply( typeConfiguration ); } } @@ -447,55 +448,71 @@ && parseBoolean( typeParameters.getProperty(DynamicParameterizedType.IS_DYNAMIC) getBuildingContext() ); } - - if ( isVersion() ) { + else if ( isVersion() ) { return VersionResolution.from( implicitJavaTypeAccess, timeZoneStorageType, getBuildingContext() ); } + else { + // determine JavaType if we can + final BasicJavaType explicitJavaType = getExplicitJavaType(); + final JavaType javaType = determineJavaType( explicitJavaType ); + final ConverterDescriptor converterDescriptor = getConverterDescriptor( javaType ); + return converterDescriptor != null + ? converterResolution( javaType, converterDescriptor ) + : resolution( explicitJavaType, javaType ); + } + } - // determine JavaType if we can - final BasicJavaType explicitJavaType = explicitJavaTypeAccess == null - ? null + private BasicJavaType getExplicitJavaType() { + return explicitJavaTypeAccess == null ? null : explicitJavaTypeAccess.apply( getTypeConfiguration() ); + } - JavaType javaType = determineJavaType( explicitJavaType ); - ConverterDescriptor attributeConverterDescriptor = getAttributeConverterDescriptor(); - + private ConverterDescriptor getConverterDescriptor(JavaType javaType) { + final ConverterDescriptor converterDescriptor = getAttributeConverterDescriptor(); if ( isSoftDelete() ) { - assert attributeConverterDescriptor != null; - final boolean conversionWasUnspecified = SoftDelete.UnspecifiedConversion.class.equals( attributeConverterDescriptor.getAttributeConverterClass() ); - if ( conversionWasUnspecified ) { - final JdbcType jdbcType = BooleanJdbcType.INSTANCE.resolveIndicatedType( this, javaType ); - if ( jdbcType.isNumber() ) { - attributeConverterDescriptor = new InstanceBasedConverterDescriptor( - NumericBooleanConverter.INSTANCE, - getBuildingContext().getBootstrapContext().getClassmateContext() - ); - } - else if ( jdbcType.isString() ) { - // here we pick 'T' / 'F' storage, though 'Y' / 'N' is equally valid - its 50/50 - attributeConverterDescriptor = new InstanceBasedConverterDescriptor( - TrueFalseConverter.INSTANCE, - getBuildingContext().getBootstrapContext().getClassmateContext() - ); - } - else { - // should indicate BIT or BOOLEAN == no conversion needed - // - we still create the converter to properly set up JDBC type, etc - attributeConverterDescriptor = new InstanceBasedConverterDescriptor( - PassThruSoftDeleteConverter.INSTANCE, - getBuildingContext().getBootstrapContext().getClassmateContext() - ); - } - } + assert converterDescriptor != null; + final ConverterDescriptor softDeleteConverterDescriptor = + getSoftDeleteConverterDescriptor( converterDescriptor, javaType); + return getSoftDeleteStrategy() == SoftDeleteType.ACTIVE + ? new ReversedConverterDescriptor<>( softDeleteConverterDescriptor ) + : softDeleteConverterDescriptor; + } + else { + return converterDescriptor; + } + } - if ( getSoftDeleteStrategy() == SoftDeleteType.ACTIVE ) { - attributeConverterDescriptor = new ReversedConverterDescriptor<>( attributeConverterDescriptor ); + private ConverterDescriptor getSoftDeleteConverterDescriptor( + ConverterDescriptor attributeConverterDescriptor, JavaType javaType) { + final boolean conversionWasUnspecified = + SoftDelete.UnspecifiedConversion.class.equals( attributeConverterDescriptor.getAttributeConverterClass() ); + if ( conversionWasUnspecified ) { + final JdbcType jdbcType = BooleanJdbcType.INSTANCE.resolveIndicatedType( this, javaType); + if ( jdbcType.isNumber() ) { + return new InstanceBasedConverterDescriptor( + NumericBooleanConverter.INSTANCE, + getBuildingContext().getBootstrapContext().getClassmateContext() + ); + } + else if ( jdbcType.isString() ) { + // here we pick 'T' / 'F' storage, though 'Y' / 'N' is equally valid - its 50/50 + return new InstanceBasedConverterDescriptor( + TrueFalseConverter.INSTANCE, + getBuildingContext().getBootstrapContext().getClassmateContext() + ); + } + else { + // should indicate BIT or BOOLEAN == no conversion needed + // - we still create the converter to properly set up JDBC type, etc + return new InstanceBasedConverterDescriptor( + PassThruSoftDeleteConverter.INSTANCE, + getBuildingContext().getBootstrapContext().getClassmateContext() + ); } } - - return attributeConverterDescriptor != null - ? converterResolution( javaType, attributeConverterDescriptor ) - : resolution( explicitJavaType, javaType ); + else { + return attributeConverterDescriptor; + } } private static class ReversedConverterDescriptor implements ConverterDescriptor { @@ -680,11 +697,10 @@ private Resolution converterResolution(JavaType javaType, ConverterDescrip && !attributeConverterDescriptor.getDomainValueResolvedType().getErasedType() .isAssignableFrom( javaType.getJavaTypeClass() ) ) { // In this case, the converter applies to the element of a BasicPluralJavaType - final BasicPluralJavaType containerJtd = (BasicPluralJavaType) javaType; final BasicType registeredElementType = converterResolution.getLegacyResolvedBasicType(); final Selectable column = getColumn(); final BasicType registeredType = registeredElementType == null ? null - : containerJtd.resolveType( + : ( (BasicPluralJavaType) javaType ).resolveType( getTypeConfiguration(), getDialect(), registeredElementType, @@ -730,66 +746,76 @@ private JavaType determineJavaType(JavaType explicitJavaType) { } private JavaType determineReflectedJavaType() { - final java.lang.reflect.Type impliedJavaType; - final TypeConfiguration typeConfiguration = getTypeConfiguration(); + final java.lang.reflect.Type impliedJavaType = impliedJavaType( typeConfiguration ); + if ( impliedJavaType == null ) { + return null; + } + else { + resolvedJavaType = impliedJavaType; + return javaType( typeConfiguration, impliedJavaType ); + } + } + + private java.lang.reflect.Type impliedJavaType(TypeConfiguration typeConfiguration) { if ( resolvedJavaType != null ) { - impliedJavaType = resolvedJavaType; + return resolvedJavaType; } else if ( implicitJavaTypeAccess != null ) { - impliedJavaType = implicitJavaTypeAccess.apply( typeConfiguration ); + return implicitJavaTypeAccess.apply(typeConfiguration); } else if ( ownerName != null && propertyName != null ) { - impliedJavaType = ReflectHelper.reflectedPropertyType( - ownerName, - propertyName, - getServiceRegistry().requireService( ClassLoaderService.class ) - ); + return reflectedPropertyType( ownerName, propertyName, + getServiceRegistry().requireService( ClassLoaderService.class ) ); } else { return null; } + } - resolvedJavaType = impliedJavaType; + private JavaType javaType(TypeConfiguration typeConfiguration, java.lang.reflect.Type impliedJavaType) { + final JavaType javaType = typeConfiguration.getJavaTypeRegistry().findDescriptor( impliedJavaType ); + return javaType == null ? specialJavaType( typeConfiguration, impliedJavaType ) : javaType; + } - if ( impliedJavaType == null ) { - return null; + private JavaType specialJavaType( + TypeConfiguration typeConfiguration, + java.lang.reflect.Type impliedJavaType) { + final JavaTypeRegistry javaTypeRegistry = typeConfiguration.getJavaTypeRegistry(); + if ( jdbcTypeCode != null ) { + // Construct special JavaType instances for JSON/XML types which can report recommended JDBC types + // and implement toString/fromString as well as copying based on FormatMapper operations + switch ( jdbcTypeCode ) { + case SqlTypes.JSON: + final JavaType jsonJavaType = + new JsonJavaType<>( impliedJavaType, + mutabilityPlan( typeConfiguration, impliedJavaType ), + typeConfiguration ); + javaTypeRegistry.addDescriptor( jsonJavaType ); + return jsonJavaType; + case SqlTypes.SQLXML: + final JavaType xmlJavaType = + new XmlJavaType<>( impliedJavaType, + mutabilityPlan( typeConfiguration, impliedJavaType ), + typeConfiguration ); + javaTypeRegistry.addDescriptor( xmlJavaType ); + return xmlJavaType; + } } + return javaTypeRegistry.resolveDescriptor( impliedJavaType ); + } - final JavaTypeRegistry javaTypeRegistry = typeConfiguration.getJavaTypeRegistry(); - final JavaType javaType = javaTypeRegistry.findDescriptor( impliedJavaType ); - final MutabilityPlan explicitMutabilityPlan = explicitMutabilityPlanAccess != null - ? explicitMutabilityPlanAccess.apply( typeConfiguration ) - : null; - final MutabilityPlan determinedMutabilityPlan = explicitMutabilityPlan != null + private MutabilityPlan mutabilityPlan( + TypeConfiguration typeConfiguration, java.lang.reflect.Type impliedJavaType) { + final MutabilityPlan explicitMutabilityPlan = getExplicitMutabilityPlan(); + return explicitMutabilityPlan != null ? explicitMutabilityPlan : RegistryHelper.INSTANCE.determineMutabilityPlan( impliedJavaType, typeConfiguration ); - if ( javaType == null ) { - if ( jdbcTypeCode != null ) { - // Construct special JavaType instances for JSON/XML types which can report recommended JDBC types - // and implement toString/fromString as well as copying based on FormatMapper operations - switch ( jdbcTypeCode ) { - case SqlTypes.JSON: - final JavaType jsonJavaType = new JsonJavaType<>( - impliedJavaType, - determinedMutabilityPlan, - typeConfiguration - ); - javaTypeRegistry.addDescriptor( jsonJavaType ); - return jsonJavaType; - case SqlTypes.SQLXML: - final JavaType xmlJavaType = new XmlJavaType<>( - impliedJavaType, - determinedMutabilityPlan, - typeConfiguration - ); - javaTypeRegistry.addDescriptor( xmlJavaType ); - return xmlJavaType; - } - } - return javaTypeRegistry.resolveDescriptor( impliedJavaType ); - } - return javaType; + } + + private MutabilityPlan getExplicitMutabilityPlan() { + return explicitMutabilityPlanAccess == null ? null + : explicitMutabilityPlanAccess.apply( getTypeConfiguration() ); } private static Resolution interpretExplicitlyNamedType( @@ -1066,8 +1092,7 @@ private UserType getConfiguredUserTypeBean(Class> expli : getUserTypeBean( explicitCustomType, properties ).getBeanInstance(); if ( typeInstance instanceof TypeConfigurationAware ) { - final TypeConfigurationAware configurationAware = (TypeConfigurationAware) typeInstance; - configurationAware.setTypeConfiguration( getTypeConfiguration() ); + ( (TypeConfigurationAware) typeInstance ).setTypeConfiguration( getTypeConfiguration() ); } if ( typeInstance instanceof DynamicParameterizedType ) { @@ -1097,11 +1122,12 @@ private ManagedBean getUserTypeBean(Class explicitCustomType, Properti } } + @SuppressWarnings("deprecation") public void setTemporalPrecision(TemporalType temporalPrecision) { this.temporalPrecision = temporalPrecision; } - @Override + @Override @SuppressWarnings("deprecation") public TemporalType getTemporalPrecision() { return temporalPrecision; } diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/Column.java b/hibernate-core/src/main/java/org/hibernate/mapping/Column.java index 71a3034eb365..d040f47a0a2e 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/Column.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/Column.java @@ -350,7 +350,7 @@ private String getSqlTypeName(DdlTypeRegistry ddlTypeRegistry, Dialect dialect, } private static Type getUnderlyingType(Mapping mapping, Type type, int typeIndex) { - if ( type.isComponentType() ) { + if ( type instanceof ComponentType ) { final ComponentType componentType = (ComponentType) type; int cols = 0; for ( Type subtype : componentType.getSubtypes() ) { @@ -362,7 +362,7 @@ private static Type getUnderlyingType(Mapping mapping, Type type, int typeIndex) } throw new IndexOutOfBoundsException(); } - else if ( type.isEntityType() ) { + else if ( type instanceof EntityType ) { final EntityType entityType = (EntityType) type; final Type idType = entityType.getIdentifierOrUniqueKeyType( mapping ); return getUnderlyingType( mapping, idType, typeIndex ); diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/Component.java b/hibernate-core/src/main/java/org/hibernate/mapping/Component.java index 431099e0ca69..8891b5219cac 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/Component.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/Component.java @@ -62,7 +62,6 @@ import static java.util.Collections.unmodifiableList; import static java.util.stream.Collectors.toList; -import static org.hibernate.generator.EventType.INSERT; import static org.hibernate.internal.util.StringHelper.qualify; import static org.hibernate.mapping.MappingHelper.checkPropertyColumnDuplication; import static org.hibernate.metamodel.mapping.EntityDiscriminatorMapping.DISCRIMINATOR_ROLE_NAME; @@ -719,7 +718,8 @@ else if ( rootClass.getIdentifierProperty() != null ) { if ( property.getValue().isSimpleValue() ) { final SimpleValue value = (SimpleValue) property.getValue(); - if ( !DEFAULT_ID_GEN_STRATEGY.equals( value.getIdentifierGeneratorStrategy() ) ) { + if ( !DEFAULT_ID_GEN_STRATEGY.equals( value.getIdentifierGeneratorStrategy() ) + || value.getCustomIdGeneratorCreator() != null ) { // skip any 'assigned' generators, they would have been handled by // the StandardGenerationContextLocator generator.addGeneratedValuePlan( new ValueGenerationPlan( @@ -797,10 +797,9 @@ public int getPropertyIndex() { } @Override - public Object execute(SharedSessionContractImplementor session, Object incomingObject) { - if ( !subgenerator.generatedOnExecution( incomingObject, session ) ) { - return ( (BeforeExecutionGenerator) subgenerator) - .generate( session, incomingObject, null, INSERT ); + public BeforeExecutionGenerator getGenerator() { + if ( subgenerator instanceof BeforeExecutionGenerator ) { + return (BeforeExecutionGenerator) subgenerator; } else { throw new IdentifierGenerationException( "Identity generation isn't supported for composite ids" ); diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/DenormalizedTable.java b/hibernate-core/src/main/java/org/hibernate/mapping/DenormalizedTable.java index 0eb210f38a17..b1668d2b71e6 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/DenormalizedTable.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/DenormalizedTable.java @@ -70,10 +70,12 @@ public void createForeignKeys(MetadataBuildingContext context) { if ( foreignKey.getReferencedTable() == null ) { foreignKey.setReferencedTable( referencedClass.getTable() ); } + + final ForeignKey denormalizedForeignKey = createDenormalizedForeignKey( foreignKey ); createForeignKey( context.getBuildingOptions() .getImplicitNamingStrategy() - .determineForeignKeyName( new ForeignKeyNameSource( foreignKey, this, context ) ) + .determineForeignKeyName( new ForeignKeyNameSource( denormalizedForeignKey, this, context ) ) .render( context.getMetadataCollector().getDatabase().getDialect() ), foreignKey.getColumns(), foreignKey.getReferencedEntityName(), @@ -83,6 +85,18 @@ public void createForeignKeys(MetadataBuildingContext context) { } } + private ForeignKey createDenormalizedForeignKey(ForeignKey includedTableFk) { + final ForeignKey denormalizedForeignKey = new ForeignKey(); + denormalizedForeignKey.setReferencedEntityName( includedTableFk.getReferencedEntityName() ); + denormalizedForeignKey.setKeyDefinition( includedTableFk.getKeyDefinition() ); + denormalizedForeignKey.setReferencedTable( includedTableFk.getReferencedTable() ); + denormalizedForeignKey.addReferencedColumns( includedTableFk.getReferencedColumns() ); + for ( Column keyColumn : includedTableFk.getColumns() ) { + denormalizedForeignKey.addColumn( keyColumn ); + } + return denormalizedForeignKey; + } + @Override public Column getColumn(Column column) { Column superColumn = super.getColumn( column ); diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/Property.java b/hibernate-core/src/main/java/org/hibernate/mapping/Property.java index 55bf308400e8..732eddc9193e 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/Property.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/Property.java @@ -30,6 +30,9 @@ import org.hibernate.service.ServiceRegistry; import org.hibernate.generator.Generator; import org.hibernate.generator.GeneratorCreationContext; +import org.hibernate.type.AnyType; +import org.hibernate.type.CollectionType; +import org.hibernate.type.ComponentType; import org.hibernate.type.CompositeType; import org.hibernate.type.Type; import org.hibernate.type.WrapperArrayHandling; @@ -144,10 +147,13 @@ public boolean isPrimitive(Class clazz) { public CascadeStyle getCascadeStyle() throws MappingException { final Type type = value.getType(); - if ( type.isComponentType() ) { - return getCompositeCascadeStyle( (CompositeType) type, cascade ); + if ( type instanceof AnyType ) { + return getCascadeStyle( cascade ); + } + if ( type instanceof ComponentType ) { + return getCompositeCascadeStyle( (ComponentType) type, cascade ); } - else if ( type.isCollectionType() ) { + else if ( type instanceof CollectionType ) { final Collection collection = (Collection) value; return getCollectionCascadeStyle( collection.getElement().getType(), cascade ); } @@ -157,9 +163,15 @@ else if ( type.isCollectionType() ) { } private static CascadeStyle getCompositeCascadeStyle(CompositeType compositeType, String cascade) { - if ( compositeType.isAnyType() ) { + if ( compositeType instanceof AnyType ) { return getCascadeStyle( cascade ); } + else { + return getCompositeCascadeStyle( (ComponentType) compositeType, cascade ); + } + } + + private static CascadeStyle getCompositeCascadeStyle(ComponentType compositeType, String cascade) { final int length = compositeType.getSubtypes().length; for ( int i=0; i predicateConsumer, + @Nullable Consumer predicateConsumer, SqlAstCreationState creationState) { return new StandardVirtualTableGroup( navigablePath, this, lhs, fetched ); } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/AttributeFactory.java b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/AttributeFactory.java index 351500a46392..890384a453a9 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/AttributeFactory.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/AttributeFactory.java @@ -61,6 +61,9 @@ import org.hibernate.property.access.internal.PropertyAccessMapImpl; import org.hibernate.property.access.spi.Getter; import org.hibernate.type.AnyType; +import org.hibernate.type.BasicType; +import org.hibernate.type.CollectionType; +import org.hibernate.type.ComponentType; import org.hibernate.type.BasicPluralType; import org.hibernate.type.EntityType; import org.hibernate.type.descriptor.java.JavaType; @@ -461,7 +464,7 @@ private static AttributeMetadata determineAttributeMetadata( final org.hibernate.type.Type type = value.getType(); LOG.tracef( " Determined type [name=%s, class=%s]", type.getName(), type.getClass().getName() ); - if ( type.isAnyType() ) { + if ( type instanceof AnyType ) { return new SingularAttributeMetadataImpl<>( propertyMapping, attributeContext.getOwnerType(), @@ -470,18 +473,17 @@ private static AttributeMetadata determineAttributeMetadata( context ); } - else if ( type.isAssociationType() ) { - // collection or entity - if ( type.isEntityType() ) { - // entity - return new SingularAttributeMetadataImpl<>( - propertyMapping, - attributeContext.getOwnerType(), - member, - determineSingularAssociationClassification( member ), - context - ); - } + else if ( type instanceof EntityType ) { + // entity + return new SingularAttributeMetadataImpl<>( + propertyMapping, + attributeContext.getOwnerType(), + member, + determineSingularAssociationClassification( member ), + context + ); + } + else if ( type instanceof CollectionType ) { // collection if ( value instanceof Collection ) { final Collection collValue = (Collection) value; @@ -519,7 +521,7 @@ else if ( value instanceof OneToMany ) { // ); } } - else if ( propertyMapping.isComposite() ) { + else if ( type instanceof ComponentType ) { // component return new SingularAttributeMetadataImpl<>( propertyMapping, @@ -530,6 +532,7 @@ else if ( propertyMapping.isComposite() ) { ); } else { + assert type instanceof BasicType; // basic type return new SingularAttributeMetadataImpl<>( propertyMapping, @@ -557,14 +560,15 @@ else if ( value instanceof List ) { private static AttributeClassification elementClassification( org.hibernate.type.Type elementType, Value elementValue, boolean isManyToMany) { - final AttributeClassification elementClassification; - if ( elementType.isAnyType() ) { + // First, determine the type of the elements and use that to help determine the + // collection type + if ( elementType instanceof AnyType ) { return AttributeClassification.ANY; } - else if ( elementValue instanceof Component ) { + else if ( elementType instanceof ComponentType ) { return AttributeClassification.EMBEDDED; } - else if ( elementType.isAssociationType() ) { + else if ( elementType instanceof EntityType ) { return isManyToMany ? AttributeClassification.MANY_TO_MANY : AttributeClassification.ONE_TO_MANY; @@ -576,13 +580,7 @@ else if ( elementType.isAssociationType() ) { private static AttributeClassification collectionClassification( org.hibernate.type.Type elementType, Value elementValue, boolean isManyToMany) { - if ( elementType.isAnyType() ) { - return AttributeClassification.ELEMENT_COLLECTION; - } - else if ( elementValue instanceof Component ) { - return AttributeClassification.ELEMENT_COLLECTION; - } - else if ( elementType.isAssociationType() ) { + if ( elementType instanceof EntityType ) { return isManyToMany ? AttributeClassification.MANY_TO_MANY : AttributeClassification.ONE_TO_MANY; @@ -593,13 +591,13 @@ else if ( elementType.isAssociationType() ) { } private static AttributeClassification keyClassification(org.hibernate.type.Type keyType, Value keyValue) { - if ( keyType.isAnyType() ) { + if ( keyType instanceof AnyType ) { return AttributeClassification.ANY; } - else if ( keyValue instanceof Component ) { + else if ( keyType instanceof ComponentType ) { return AttributeClassification.EMBEDDED; } - else if ( keyType.isAssociationType() ) { + else if ( keyType instanceof EntityType ) { return AttributeClassification.MANY_TO_ONE; } else { diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/BaseAttributeMetadata.java b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/BaseAttributeMetadata.java index 5cfea74ddc66..a12ce485bb7d 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/BaseAttributeMetadata.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/BaseAttributeMetadata.java @@ -14,6 +14,7 @@ import org.hibernate.metamodel.AttributeClassification; import org.hibernate.metamodel.model.domain.ManagedDomainType; import org.hibernate.metamodel.model.domain.internal.MapMember; +import org.hibernate.type.CollectionType; /** * @author Steve Ebersole @@ -87,7 +88,7 @@ public ManagedDomainType getOwnerType() { } public boolean isPlural() { - return propertyMapping.getType().isCollectionType(); + return propertyMapping.getType() instanceof CollectionType; } public Property getPropertyMapping() { diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/MetadataContext.java b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/MetadataContext.java index 864af9d455d1..be53ad3a5425 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/MetadataContext.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/MetadataContext.java @@ -25,6 +25,7 @@ import org.hibernate.internal.EntityManagerMessageLogger; import org.hibernate.internal.HEMLogging; import org.hibernate.internal.util.ReflectHelper; +import org.hibernate.internal.util.collections.ArrayHelper; import org.hibernate.internal.util.collections.CollectionHelper; import org.hibernate.mapping.Component; import org.hibernate.mapping.MappedSuperclass; @@ -276,8 +277,10 @@ public Map> getIdentifiableTypesByName() { attribute = factoryFunction.apply( entityType, genericProperty ); if ( !property.isGeneric() ) { final PersistentAttribute concreteAttribute = factoryFunction.apply( entityType, property ); - //noinspection unchecked - ( (AttributeContainer) entityType ).getInFlightAccess().addConcreteGenericAttribute( concreteAttribute ); + if ( concreteAttribute != null ) { + //noinspection unchecked + ( (AttributeContainer) entityType ).getInFlightAccess().addConcreteGenericAttribute( concreteAttribute ); + } } } else { @@ -362,7 +365,13 @@ else if ( MappedSuperclass.class.isAssignableFrom( mapping.getClass() ) ) { // applyNaturalIdAttribute( safeMapping, jpaType ); for ( Property property : safeMapping.getDeclaredProperties() ) { - if ( safeMapping.isVersioned() && property == safeMapping.getVersion() ) { + if ( isIdentifierProperty( property, safeMapping ) ) { + // property represents special handling for id-class mappings but we have already + // accounted for the embedded property mappings in #applyIdMetadata && + // #buildIdClassAttributes + continue; + } + else if ( safeMapping.isVersioned() && property == safeMapping.getVersion() ) { // skip the version property, it was already handled previously. continue; } @@ -374,7 +383,7 @@ else if ( MappedSuperclass.class.isAssignableFrom( mapping.getClass() ) ) { if ( attribute != null ) { addAttribute( jpaType, attribute ); if ( property.isNaturalIdentifier() ) { - ( ( AttributeContainer) jpaType ).getInFlightAccess() + ( (AttributeContainer) jpaType ).getInFlightAccess() .applyNaturalIdAttribute( attribute ); } } @@ -415,7 +424,16 @@ else if ( MappedSuperclass.class.isAssignableFrom( mapping.getClass() ) ) { final PersistentAttribute attribute = attributeFactory.buildAttribute( (ManagedDomainType) embeddable, property ); if ( attribute != null ) { - addAttribute( embeddable, attribute ); + final Property superclassProperty = getMappedSuperclassProperty( + property.getName(), + component.getMappedSuperclass() + ); + if ( superclassProperty != null && superclassProperty.isGeneric() ) { + ( (AttributeContainer) embeddable ).getInFlightAccess().addConcreteGenericAttribute( attribute ); + } + else { + addAttribute( embeddable, attribute ); + } } } @@ -433,6 +451,14 @@ else if ( MappedSuperclass.class.isAssignableFrom( mapping.getClass() ) ) { } } + private static boolean isIdentifierProperty(Property property, MappedSuperclass mappedSuperclass) { + final Component identifierMapper = mappedSuperclass.getIdentifierMapper(); + return identifierMapper != null && ArrayHelper.contains( + identifierMapper.getPropertyNames(), + property.getName() + ); + } + private void addAttribute(ManagedDomainType type, PersistentAttribute attribute) { //noinspection unchecked AttributeContainer container = (AttributeContainer) type; @@ -549,15 +575,24 @@ private Property getMappedSuperclassIdentifier(PersistentClass persistentClass) return null; } - private EmbeddableTypeImpl applyIdClassMetadata(Component idClassComponent) { - final JavaTypeRegistry registry = getTypeConfiguration() - .getJavaTypeRegistry(); - final Class componentClass = idClassComponent.getComponentClass(); - final JavaType javaType = registry.resolveManagedTypeDescriptor( componentClass ); + private EmbeddableTypeImpl applyIdClassMetadata(Component idClassComponent) { + final JavaType javaType = + getTypeConfiguration().getJavaTypeRegistry() + .resolveManagedTypeDescriptor( idClassComponent.getComponentClass() ); + + final MappedSuperclass mappedSuperclass = idClassComponent.getMappedSuperclass(); + final MappedSuperclassDomainType superType; + if ( mappedSuperclass != null ) { + //noinspection unchecked + superType = (MappedSuperclassDomainType) locateMappedSuperclassType( mappedSuperclass ); + } + else { + superType = null; + } - final EmbeddableTypeImpl embeddableType = new EmbeddableTypeImpl<>( + final EmbeddableTypeImpl embeddableType = new EmbeddableTypeImpl<>( javaType, - null, + superType, null, false, getJpaMetamodel() @@ -567,27 +602,29 @@ private EmbeddableTypeImpl applyIdClassMetadata(Component idClassComponent) { } private void applyIdMetadata(MappedSuperclass mappingType, MappedSuperclassDomainType jpaMappingType) { + @SuppressWarnings("unchecked") + final AttributeContainer attributeContainer = (AttributeContainer) jpaMappingType; if ( mappingType.hasIdentifierProperty() ) { final Property declaredIdentifierProperty = mappingType.getDeclaredIdentifierProperty(); if ( declaredIdentifierProperty != null ) { - //noinspection unchecked - final SingularPersistentAttribute attribute = (SingularPersistentAttribute) buildAttribute( - declaredIdentifierProperty, - jpaMappingType, - attributeFactory::buildIdAttribute - ); - //noinspection unchecked - ( (AttributeContainer) jpaMappingType ).getInFlightAccess().applyIdAttribute( attribute ); + final SingularPersistentAttribute attribute = + (SingularPersistentAttribute) + buildAttribute( + declaredIdentifierProperty, + jpaMappingType, + attributeFactory::buildIdAttribute + ); + attributeContainer.getInFlightAccess().applyIdAttribute( attribute ); } } //a MappedSuperclass can have no identifier if the id is set below in the hierarchy else if ( mappingType.getIdentifierMapper() != null ) { - Set> attributes = buildIdClassAttributes( - jpaMappingType, - mappingType.getIdentifierMapper().getProperties() - ); - //noinspection unchecked - ( ( AttributeContainer) jpaMappingType ).getInFlightAccess().applyIdClassAttributes( attributes ); + final Set> attributes = + buildIdClassAttributes( + jpaMappingType, + mappingType.getIdentifierMapper().getProperties() + ); + attributeContainer.getInFlightAccess().applyIdClassAttributes( attributes ); } } @@ -643,6 +680,32 @@ private MappedSuperclass getMappedSuperclass(MappedSuperclass mappedSuperclass) : getMappedSuperclass( mappedSuperclass.getSuperPersistentClass() ); } + private Property getMappedSuperclassProperty(String propertyName, MappedSuperclass mappedSuperclass) { + if ( mappedSuperclass == null ) { + return null; + } + + for ( Property property : mappedSuperclass.getDeclaredProperties() ) { + if ( property.getName().equals( propertyName ) ) { + return property; + } + } + + final Property property = getMappedSuperclassProperty( + propertyName, + mappedSuperclass.getSuperMappedSuperclass() + ); + if ( property != null ) { + return property; + } + + if ( mappedSuperclass.getSuperPersistentClass() != null ) { + return mappedSuperclass.getSuperPersistentClass().getProperty( propertyName ); + } + + return null; + } + private Set> buildIdClassAttributes( IdentifiableDomainType ownerType, List properties) { @@ -770,15 +833,28 @@ private static void injectField( // + "; expected type : " + attribute.getClass().getName() // + "; encountered type : " + field.getType().getName() // ); - LOG.illegalArgumentOnStaticMetamodelFieldInjection( - metamodelClass.getName(), - name, - model.getClass().getName(), - field.getType().getName() - ); + // Avoid logging an error for Enver's default revision classes that are both entities and mapped-supers. + // This is a workaround for https://hibernate.atlassian.net/browse/HHH-17612 + if ( !isDefaultEnversRevisionType( metamodelClass ) ) { + LOG.illegalArgumentOnStaticMetamodelFieldInjection( + metamodelClass.getName(), + name, + model.getClass().getName(), + field.getType().getName() + ); + } } } + private static boolean isDefaultEnversRevisionType(Class metamodelClass) { + return Set.of( + "org.hibernate.envers.DefaultRevisionEntity_", + "org.hibernate.envers.DefaultTrackingModifiedEntitiesRevisionEntity_", + "org.hibernate.envers.enhanced.SequenceIdRevisionEntity_", + "org.hibernate.envers.enhanced.SequenceIdTrackingModifiedEntitiesRevisionEntity_" + ).contains( metamodelClass.getName() ); + } + public MappedSuperclassDomainType locateMappedSuperclassType(MappedSuperclass mappedSuperclass) { return mappedSuperclassByMappedSuperclassMapping.get( mappedSuperclass ); } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/EntityIdentifierMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/EntityIdentifierMapping.java index f6612255e8aa..92ea4430b7f8 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/EntityIdentifierMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/EntityIdentifierMapping.java @@ -10,6 +10,7 @@ import org.hibernate.engine.internal.ForeignKeys; import org.hibernate.engine.spi.IdentifierValue; import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.event.spi.MergeContext; import jakarta.persistence.EmbeddedId; import jakarta.persistence.Id; @@ -68,6 +69,15 @@ default String getPartName() { */ Object getIdentifier(Object entity); + /** + * Extract the identifier from an instance of the entity + * + * It's supposed to be use during the merging process + */ + default Object getIdentifier(Object entity, MergeContext mergeContext){ + return getIdentifier( entity ); + } + /** * Return the identifier of the persistent or transient object, or throw * an exception if the instance is "unsaved" diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/EntityMappingType.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/EntityMappingType.java index 7c7b0071b56c..8de1d5886ea9 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/EntityMappingType.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/EntityMappingType.java @@ -321,6 +321,14 @@ default void pruneForSubclasses(TableGroup tableGroup, Set treatedEntity */ EntityIdentifierMapping getIdentifierMapping(); + /** + * Mapping details for the entity's identifier. This is shared across all + * entity mappings within an inheritance hierarchy. + */ + default EntityIdentifierMapping getIdentifierMappingForJoin() { + return getIdentifierMapping(); + } + /** * Mapping details for the entity's discriminator. This is shared across all * entity mappings within an inheritance hierarchy. diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/MappedDiscriminatorConverter.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/MappedDiscriminatorConverter.java index b7aa1a857ac3..17e877d05f03 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/MappedDiscriminatorConverter.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/MappedDiscriminatorConverter.java @@ -13,7 +13,9 @@ import org.hibernate.metamodel.model.domain.NavigableRole; import org.hibernate.metamodel.spi.MappingMetamodelImplementor; import org.hibernate.type.BasicType; +import org.hibernate.type.descriptor.java.CharacterJavaType; import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.java.StringJavaType; import java.util.List; import java.util.Map; @@ -113,6 +115,23 @@ public DiscriminatorValueDetails getDetailsForDiscriminatorValue(Object value) { return notNullMatch; } + if ( value.getClass().isEnum() ) { + final Object enumValue; + if ( getRelationalJavaType() instanceof StringJavaType ) { + enumValue = ( (Enum) value ).name(); + } + else if ( getRelationalJavaType() instanceof CharacterJavaType ) { + enumValue = ( (Enum) value ).name().charAt( 0 ); + } + else { + enumValue = ( (Enum) value ).ordinal(); + } + final DiscriminatorValueDetails enumMatch = discriminatorValueToEntityNameMap.get( enumValue ); + if ( enumMatch != null ) { + return enumMatch; + } + } + throw new HibernateException( "Unrecognized discriminator value: " + value ); } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AbstractEntityCollectionPart.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AbstractEntityCollectionPart.java index a7d354025f87..e5f7d5faae32 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AbstractEntityCollectionPart.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AbstractEntityCollectionPart.java @@ -45,6 +45,7 @@ import org.hibernate.sql.results.graph.collection.internal.EagerCollectionFetch; import org.hibernate.sql.results.graph.entity.EntityFetch; import org.hibernate.sql.results.graph.entity.internal.EntityFetchJoinedImpl; +import org.hibernate.type.ComponentType; import org.hibernate.type.CompositeType; import org.hibernate.type.Type; @@ -389,8 +390,8 @@ private static Set resolveTargetKeyPropertyNames( propertyType = entityBinding.getIdentifierMapper().getType(); } if ( entityBinding.getIdentifierProperty() == null ) { - final CompositeType compositeType; - if ( propertyType.isComponentType() && ( compositeType = (CompositeType) propertyType ).isEmbedded() + final ComponentType compositeType; + if ( propertyType instanceof ComponentType && ( compositeType = (ComponentType) propertyType ).isEmbedded() && compositeType.getPropertyNames().length == 1 ) { ToOneAttributeMapping.addPrefixedPropertyPaths( targetKeyPropertyNames, @@ -443,8 +444,8 @@ else if ( bootModelValue instanceof OneToMany ) { } else { final Type propertyType = entityBinding.getRecursiveProperty( referencedPropertyName ).getType(); - final CompositeType compositeType; - if ( propertyType.isComponentType() && ( compositeType = (CompositeType) propertyType ).isEmbedded() + final ComponentType compositeType; + if ( propertyType instanceof ComponentType && ( compositeType = (ComponentType) propertyType ).isEmbedded() && compositeType.getPropertyNames().length == 1 ) { final Set targetKeyPropertyNames = new HashSet<>( 2 ); ToOneAttributeMapping.addPrefixedPropertyPaths( diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/CaseStatementDiscriminatorMappingImpl.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/CaseStatementDiscriminatorMappingImpl.java index 08b66db39c1d..892754ab1791 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/CaseStatementDiscriminatorMappingImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/CaseStatementDiscriminatorMappingImpl.java @@ -6,7 +6,9 @@ */ package org.hibernate.metamodel.mapping.internal; +import java.util.ArrayList; import java.util.LinkedHashMap; +import java.util.List; import org.hibernate.engine.FetchTiming; import org.hibernate.engine.spi.SessionFactoryImplementor; @@ -255,6 +257,24 @@ public CaseStatementDiscriminatorExpression(TableGroup entityTableGroup) { this.entityTableGroup = entityTableGroup; } + public List getUsedTableReferences() { + final ArrayList usedTableReferences = new ArrayList<>( tableDiscriminatorDetailsMap.size() ); + tableDiscriminatorDetailsMap.forEach( + (tableName, tableDiscriminatorDetails) -> { + final TableReference tableReference = entityTableGroup.getTableReference( + entityTableGroup.getNavigablePath(), + tableName, + false + ); + + if ( tableReference != null ) { + usedTableReferences.add( tableReference ); + } + } + ); + return usedTableReferences; + } + @Override public void renderToSql( SqlAppender sqlAppender, diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/CompoundNaturalIdMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/CompoundNaturalIdMapping.java index 37c931e9499e..0083b7a40ba9 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/CompoundNaturalIdMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/CompoundNaturalIdMapping.java @@ -7,7 +7,6 @@ package org.hibernate.metamodel.mapping.internal; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.function.BiConsumer; @@ -61,6 +60,11 @@ public class CompoundNaturalIdMapping extends AbstractNaturalIdMapping implement private final List attributes; private List jdbcMappings; + /* + This value is used to determine the size of the array used to create the ImmutableFetchList (see org.hibernate.sql.results.graph.internal.ImmutableFetchList#Builder) + The Fetch is inserted into the array at a position corresponding to its Fetchable key value. + */ + private final int maxFetchableKeyIndex; public CompoundNaturalIdMapping( EntityMappingType declaringType, @@ -69,6 +73,14 @@ public CompoundNaturalIdMapping( super( declaringType, isMutable( attributes ) ); this.attributes = attributes; + int maxIndex = 0; + for ( SingularAttributeMapping attribute : attributes ) { + if ( attribute.getFetchableKey() > maxIndex ) { + maxIndex = attribute.getFetchableKey(); + } + } + this.maxFetchableKeyIndex = maxIndex + 1; + creationProcess.registerInitializationCallback( "Determine compound natural-id JDBC mappings ( " + declaringType.getEntityName() + ")", () -> { @@ -235,7 +247,15 @@ public void verifyFlushState(Object id, Object[] currentState, Object[] loadedSt @Override public boolean areEqual(Object one, Object other, SharedSessionContractImplementor session) { - return Arrays.equals( (Object[]) one, (Object[]) other ); + final Object[] one1 = (Object[]) one; + final Object[] other1 = (Object[]) other; + final List naturalIdAttributes = getNaturalIdAttributes(); + for ( int i = 0; i < naturalIdAttributes.size(); i++ ) { + if ( !naturalIdAttributes.get( i ).areEqual( one1[i], other1[i], session ) ) { + return false; + } + } + return true; } @Override @@ -531,6 +551,10 @@ public void visitSubParts(Consumer consumer, EntityMappingType treatT attributes.forEach( consumer ); } + @Override + public int getNumberOfFetchableKeys() { + return maxFetchableKeyIndex; + } public static class DomainResultImpl implements DomainResult, FetchParent { private final NavigablePath navigablePath; diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/DiscriminatedAssociationAttributeMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/DiscriminatedAssociationAttributeMapping.java index 3c40af99c00d..c539daeaaba5 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/DiscriminatedAssociationAttributeMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/DiscriminatedAssociationAttributeMapping.java @@ -52,6 +52,8 @@ import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.descriptor.java.MutabilityPlan; +import org.checkerframework.checker.nullness.qual.Nullable; + /** * Singular, any-valued attribute * @@ -487,9 +489,9 @@ public Object assemble(Serializable cached, SharedSessionContract session) { public TableGroupJoin createTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType requestedJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType requestedJoinType, boolean fetched, boolean addsPredicate, SqlAstCreationState creationState) { @@ -512,11 +514,11 @@ public TableGroupJoin createTableGroupJoin( public TableGroup createRootTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType sqlAstJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType sqlAstJoinType, boolean fetched, - Consumer predicateConsumer, + @Nullable Consumer predicateConsumer, SqlAstCreationState creationState) { return new StandardVirtualTableGroup( navigablePath, this, lhs, fetched ); } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/DiscriminatedCollectionPart.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/DiscriminatedCollectionPart.java index 280c782c9cd2..81d86e6f5b3e 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/DiscriminatedCollectionPart.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/DiscriminatedCollectionPart.java @@ -45,6 +45,10 @@ import org.hibernate.type.AnyType; import org.hibernate.type.descriptor.java.JavaType; +import org.checkerframework.checker.nullness.qual.Nullable; + +import static java.util.Objects.requireNonNullElse; + /** * @author Steve Ebersole */ @@ -343,19 +347,13 @@ public int forEachJdbcType(int offset, IndexedConsumer action) { public TableGroupJoin createTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType requestedJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType requestedJoinType, boolean fetched, boolean addsPredicate, SqlAstCreationState creationState) { - final SqlAstJoinType joinType; - if ( requestedJoinType == null ) { - joinType = SqlAstJoinType.INNER; - } - else { - joinType = requestedJoinType; - } + final SqlAstJoinType joinType = requireNonNullElse( requestedJoinType, SqlAstJoinType.INNER ); final TableGroup tableGroup = createRootTableGroupJoin( navigablePath, lhs, @@ -374,11 +372,11 @@ public TableGroupJoin createTableGroupJoin( public TableGroup createRootTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType sqlAstJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType sqlAstJoinType, boolean fetched, - Consumer predicateConsumer, + @Nullable Consumer predicateConsumer, SqlAstCreationState creationState) { return new StandardVirtualTableGroup( navigablePath, this, lhs, fetched ); } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EmbeddableMappingTypeImpl.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EmbeddableMappingTypeImpl.java index 3c0af66c2580..5e9b280e2ca6 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EmbeddableMappingTypeImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EmbeddableMappingTypeImpl.java @@ -764,7 +764,7 @@ private EmbeddableDiscriminatorMapping generateDiscriminatorMapping( return new ExplicitColumnDiscriminatorMappingImpl( this, name, - bootDescriptor.getTable().getName(), + bootDescriptor.getTable().getQualifiedName( creationContext.getSqlStringGenerationContext() ), discriminatorColumnExpression, isFormula, !isFormula, diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EmbeddedAttributeMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EmbeddedAttributeMapping.java index 9308988f039f..a47872909d0d 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EmbeddedAttributeMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EmbeddedAttributeMapping.java @@ -53,6 +53,10 @@ import org.hibernate.sql.results.graph.embeddable.internal.EmbeddableFetchImpl; import org.hibernate.sql.results.graph.embeddable.internal.EmbeddableResultImpl; +import org.checkerframework.checker.nullness.qual.Nullable; + +import static java.util.Objects.requireNonNullElse; + /** * @author Steve Ebersole */ @@ -318,13 +322,13 @@ public SqlTuple toSqlExpression( public TableGroupJoin createTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType requestedJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType requestedJoinType, boolean fetched, boolean addsPredicate, SqlAstCreationState creationState) { - final SqlAstJoinType joinType = requestedJoinType == null ? SqlAstJoinType.INNER : requestedJoinType; + final SqlAstJoinType joinType = requireNonNullElse( requestedJoinType, SqlAstJoinType.INNER ); final TableGroup tableGroup = createRootTableGroupJoin( navigablePath, lhs, @@ -343,11 +347,11 @@ public TableGroupJoin createTableGroupJoin( public TableGroup createRootTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType sqlAstJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType sqlAstJoinType, boolean fetched, - Consumer predicateConsumer, + @Nullable Consumer predicateConsumer, SqlAstCreationState creationState) { return new StandardVirtualTableGroup( navigablePath, this, lhs, fetched ); } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EmbeddedCollectionPart.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EmbeddedCollectionPart.java index 190b5fc13865..aa49eb298cd1 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EmbeddedCollectionPart.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EmbeddedCollectionPart.java @@ -13,7 +13,6 @@ import org.hibernate.engine.FetchStyle; import org.hibernate.engine.FetchTiming; -import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.metamodel.mapping.CollectionPart; import org.hibernate.metamodel.mapping.EmbeddableMappingType; import org.hibernate.metamodel.mapping.EntityMappingType; @@ -21,7 +20,6 @@ import org.hibernate.metamodel.mapping.MappingType; import org.hibernate.metamodel.mapping.ModelPart; import org.hibernate.metamodel.mapping.PluralAttributeMapping; -import org.hibernate.metamodel.mapping.SelectableConsumer; import org.hibernate.metamodel.model.domain.NavigableRole; import org.hibernate.persister.collection.CollectionPersister; import org.hibernate.property.access.internal.PropertyAccessStrategyBasicImpl; @@ -41,7 +39,6 @@ import org.hibernate.sql.ast.tree.from.StandardVirtualTableGroup; import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.ast.tree.from.TableGroupJoin; -import org.hibernate.sql.ast.tree.from.TableGroupProducer; import org.hibernate.sql.ast.tree.from.TableReference; import org.hibernate.sql.ast.tree.predicate.Predicate; import org.hibernate.sql.results.graph.DomainResult; @@ -49,12 +46,15 @@ import org.hibernate.sql.results.graph.Fetch; import org.hibernate.sql.results.graph.FetchOptions; import org.hibernate.sql.results.graph.FetchParent; -import org.hibernate.sql.results.graph.Fetchable; import org.hibernate.sql.results.graph.embeddable.EmbeddableValuedFetchable; import org.hibernate.sql.results.graph.embeddable.internal.EmbeddableFetchImpl; import org.hibernate.sql.results.graph.embeddable.internal.EmbeddableResultImpl; import org.hibernate.type.descriptor.java.JavaType; +import org.checkerframework.checker.nullness.qual.Nullable; + +import static java.util.Objects.requireNonNullElse; + /** * @author Steve Ebersole */ @@ -231,19 +231,13 @@ public SqlTuple toSqlExpression( public TableGroupJoin createTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType requestedJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType requestedJoinType, boolean fetched, boolean addsPredicate, SqlAstCreationState creationState) { - final SqlAstJoinType joinType; - if ( requestedJoinType == null ) { - joinType = SqlAstJoinType.INNER; - } - else { - joinType = requestedJoinType; - } + final SqlAstJoinType joinType = requireNonNullElse( requestedJoinType, SqlAstJoinType.INNER ); final TableGroup tableGroup = createRootTableGroupJoin( navigablePath, lhs, @@ -262,11 +256,11 @@ public TableGroupJoin createTableGroupJoin( public TableGroup createRootTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType sqlAstJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType sqlAstJoinType, boolean fetched, - Consumer predicateConsumer, + @Nullable Consumer predicateConsumer, SqlAstCreationState creationState) { assert lhs.getModelPart() instanceof PluralAttributeMapping; return new StandardVirtualTableGroup( navigablePath, this, lhs, fetched ); diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EntityVersionMappingImpl.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EntityVersionMappingImpl.java index 87d40f704166..59465c4c4f69 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EntityVersionMappingImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EntityVersionMappingImpl.java @@ -54,7 +54,7 @@ public class EntityVersionMappingImpl implements EntityVersionMapping, FetchOpti private final Integer scale; private final Integer temporalPrecision; - private final BasicType versionBasicType; + private final BasicType versionBasicType; private final VersionValue unsavedValueStrategy; @@ -90,15 +90,10 @@ public EntityVersionMappingImpl( unsavedValueStrategy = UnsavedValueFactory.getUnsavedVersionValue( (KeyValue) bootEntityDescriptor.getVersion().getValue(), (VersionJavaType) versionBasicType.getJavaTypeDescriptor(), - length, - precision, - scale, - declaringType - .getRepresentationStrategy() + declaringType.getRepresentationStrategy() .resolvePropertyAccess( bootEntityDescriptor.getVersion() ) .getGetter(), - templateInstanceAccess, - creationProcess.getCreationContext().getSessionFactory() + templateInstanceAccess ); } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/FetchOptionsHelper.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/FetchOptionsHelper.java index 8c856a9121dd..602bc1cabf8b 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/FetchOptionsHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/FetchOptionsHelper.java @@ -16,7 +16,10 @@ import org.hibernate.persister.collection.CollectionPersister; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.sql.results.graph.FetchOptions; +import org.hibernate.type.AnyType; import org.hibernate.type.AssociationType; +import org.hibernate.type.CollectionType; +import org.hibernate.type.EntityType; /** * @author Steve Ebersole @@ -40,7 +43,7 @@ public static FetchStyle determineFetchStyleByMetadata( FetchMode mappingFetchMode, AssociationType type, SessionFactoryImplementor sessionFactory) { - if ( !type.isEntityType() && !type.isCollectionType() ) { + if ( !( type instanceof EntityType ) && !( type instanceof CollectionType ) ) { return FetchStyle.SELECT; } @@ -48,7 +51,7 @@ public static FetchStyle determineFetchStyleByMetadata( return FetchStyle.JOIN; } - if ( type.isEntityType() ) { + if ( type instanceof EntityType ) { EntityPersister persister = (EntityPersister) type.getAssociatedJoinable( sessionFactory ); if ( persister.isBatchLoadable() ) { return FetchStyle.BATCH; @@ -116,11 +119,11 @@ public static FetchTiming determineFetchTiming( } private static boolean isSubsequentSelectDelayed(AssociationType type, SessionFactoryImplementor sessionFactory) { - if ( type.isAnyType() ) { + if ( type instanceof AnyType ) { // we'd need more context here. this is only kept as part of the property state on the owning entity return false; } - else if ( type.isEntityType() ) { + else if ( type instanceof EntityType ) { final EntityPersister entityPersister = (EntityPersister) type.getAssociatedJoinable( sessionFactory ); return entityPersister.getEntityMetamodel().isLazy(); } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/GeneratedValuesProcessor.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/GeneratedValuesProcessor.java index a4a67035ddf3..ab0ad491c558 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/GeneratedValuesProcessor.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/GeneratedValuesProcessor.java @@ -153,8 +153,8 @@ public void processGeneratedValues( assert results.size() == 1; setEntityAttributes( entity, state, results.get( 0 ) ); } - else { - castNonNull( generatedValues ); + else if ( generatedValues != null ) { + // can be null when an update action resulted in a no-op (e.g. only changes to unowned association) final List results = generatedValues.getGeneratedValues( generatedValuesToSelect ); setEntityAttributes( entity, state, results.toArray( new Object[0] ) ); } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/InverseNonAggregatedIdentifierMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/InverseNonAggregatedIdentifierMapping.java index 5e47f34d21a8..46589709d974 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/InverseNonAggregatedIdentifierMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/InverseNonAggregatedIdentifierMapping.java @@ -14,6 +14,7 @@ import org.hibernate.engine.spi.EntityKey; import org.hibernate.engine.spi.PersistenceContext; import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.event.spi.MergeContext; import org.hibernate.internal.util.collections.CollectionHelper; import org.hibernate.metamodel.mapping.AttributeMapping; import org.hibernate.metamodel.mapping.EmbeddableMappingType; @@ -196,6 +197,11 @@ public SqlTuple toSqlExpression( @Override public Object getIdentifier(Object entity) { + return getIdentifier( entity, null ); + } + + @Override + public Object getIdentifier(Object entity, MergeContext mergeContext) { if ( hasContainingClass() ) { final Object id = identifierValueMapper.getRepresentationStrategy().getInstantiator().instantiate( null, @@ -218,16 +224,17 @@ public Object getIdentifier(Object entity) { //JPA 2 @MapsId + @IdClass points to the pk of the entity else if ( attributeMapping instanceof ToOneAttributeMapping && !( identifierValueMapper.getAttributeMapping( i ) instanceof ToOneAttributeMapping ) ) { + final Object toOne = getIfMerged( o, mergeContext ); final ToOneAttributeMapping toOneAttributeMapping = (ToOneAttributeMapping) attributeMapping; final ModelPart targetPart = toOneAttributeMapping.getForeignKeyDescriptor().getPart( toOneAttributeMapping.getSideNature().inverse() ); if ( targetPart.isEntityIdentifierMapping() ) { - propertyValues[i] = ( (EntityIdentifierMapping) targetPart ).getIdentifier( o ); + propertyValues[i] = ( (EntityIdentifierMapping) targetPart ) + .getIdentifier( toOne, mergeContext ); } else { - propertyValues[i] = o; - assert false; + propertyValues[i] = toOne; } } else { @@ -242,6 +249,16 @@ else if ( attributeMapping instanceof ToOneAttributeMapping } } + private static Object getIfMerged(Object o, MergeContext mergeContext) { + if ( mergeContext != null ) { + final Object merged = mergeContext.get( o ); + if ( merged != null ) { + return merged; + } + } + return o; + } + @Override public void setIdentifier(Object entity, Object id, SharedSessionContractImplementor session) { final Object[] propertyValues = new Object[identifierValueMapper.getNumberOfAttributeMappings()]; diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ManyToManyCollectionPart.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ManyToManyCollectionPart.java index 4e262fc3fc71..f0818f97df87 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ManyToManyCollectionPart.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ManyToManyCollectionPart.java @@ -56,6 +56,8 @@ import org.hibernate.sql.ast.tree.predicate.Predicate; import org.hibernate.type.EntityType; +import org.checkerframework.checker.nullness.qual.Nullable; + import static java.util.Objects.requireNonNullElse; import static org.hibernate.metamodel.mapping.internal.MappingModelCreationHelper.createInverseModelPart; import static org.hibernate.metamodel.mapping.internal.MappingModelCreationHelper.getPropertyOrder; @@ -258,14 +260,13 @@ public ModelPart getKeyTargetMatchPart() { public TableGroupJoin createTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType requestedJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType requestedJoinType, boolean fetched, boolean addsPredicate, SqlAstCreationState creationState) { final SqlAstJoinType joinType = requireNonNullElse( requestedJoinType, SqlAstJoinType.INNER ); - final LazyTableGroup lazyTableGroup = createRootTableGroupJoin( navigablePath, lhs, @@ -302,11 +303,11 @@ public TableGroupJoin createTableGroupJoin( public LazyTableGroup createRootTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType requestedJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType requestedJoinType, boolean fetched, - Consumer predicateConsumer, + @Nullable Consumer predicateConsumer, SqlAstCreationState creationState) { final SqlAstJoinType joinType = requireNonNullElse( requestedJoinType, SqlAstJoinType.INNER ); final boolean canUseInnerJoin = joinType == SqlAstJoinType.INNER || lhs.canUseInnerJoins(); @@ -393,7 +394,8 @@ else if ( getNature() == Nature.INDEX ) { fkTargetModelPart = resolveNamedTargetPart( mapKeyPropertyName, entityMappingType, collectionDescriptor ); } else { - fkTargetModelPart = getAssociatedEntityMappingType().getIdentifierMapping(); + fkTargetModelPart = getAssociatedEntityMappingType().getIdentifierMappingForJoin(); +// fkTargetModelPart = getAssociatedEntityMappingType().getIdentifierMapping(); } } else if ( StringHelper.isNotEmpty( bootCollectionDescriptor.getMappedByProperty() ) ) { @@ -449,7 +451,8 @@ else if ( StringHelper.isNotEmpty( bootCollectionDescriptor.getMappedByProperty( } else { // non-inverse @ManyToMany - fkTargetModelPart = getAssociatedEntityMappingType().getIdentifierMapping(); + fkTargetModelPart = getAssociatedEntityMappingType().getIdentifierMappingForJoin(); +// fkTargetModelPart = getAssociatedEntityMappingType().getIdentifierMapping(); } if ( getNature() == Nature.ELEMENT ) { diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/MappingModelCreationHelper.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/MappingModelCreationHelper.java index 0d9d96459aab..0d942799bb9b 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/MappingModelCreationHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/MappingModelCreationHelper.java @@ -120,6 +120,8 @@ import org.hibernate.type.descriptor.java.spi.JavaTypeRegistry; import org.hibernate.type.spi.TypeConfiguration; +import org.checkerframework.checker.nullness.qual.Nullable; + import static org.hibernate.internal.util.NullnessUtil.castNonNull; import static org.hibernate.metamodel.mapping.MappingModelCreationLogging.MAPPING_MODEL_CREATION_MESSAGE_LOGGER; import static org.hibernate.sql.ast.spi.SqlExpressionResolver.createColumnReferenceKey; @@ -769,7 +771,7 @@ private static void interpretPluralAttributeMappingKeyDescriptor( final ManagedMappingType keyDeclaringType; final String collectionTableName = ((AbstractCollectionPersister) collectionDescriptor).getTableName(); - if ( collectionDescriptor.getElementType().isEntityType() ) { + if ( collectionDescriptor.getElementType() instanceof EntityType ) { keyDeclaringType = ( (QueryableCollection) collectionDescriptor ).getElementPersister(); } else { @@ -781,7 +783,8 @@ private static void interpretPluralAttributeMappingKeyDescriptor( } if ( isReferenceToPrimaryKey ) { - fkTargetPart = collectionDescriptor.getOwnerEntityPersister().getIdentifierMapping(); + fkTargetPart = collectionDescriptor.getOwnerEntityPersister().getIdentifierMappingForJoin(); +// fkTargetPart = collectionDescriptor.getOwnerEntityPersister().getIdentifierMapping(); } else { fkTargetPart = declaringType.findContainingEntityMapping().findSubPart( lhsPropertyName ); @@ -934,7 +937,8 @@ else if ( modelPart == null ) { final ModelPart fkTarget; if ( bootValueMapping.isReferenceToPrimaryKey() ) { - fkTarget = referencedEntityDescriptor.getIdentifierMapping(); + fkTarget = referencedEntityDescriptor.getIdentifierMappingForJoin(); +// fkTarget = referencedEntityDescriptor.getIdentifierMapping(); } else { fkTarget = referencedEntityDescriptor.findByPath( bootValueMapping.getReferencedPropertyName() ); @@ -1683,9 +1687,9 @@ public boolean isSimpleJoinPredicate(Predicate predicate) { public TableGroupJoin createTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType sqlAstJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType sqlAstJoinType, boolean fetched, boolean addsPredicate, SqlAstCreationState creationState) { @@ -1696,11 +1700,11 @@ public TableGroupJoin createTableGroupJoin( public TableGroup createRootTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType sqlAstJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType sqlAstJoinType, boolean fetched, - Consumer predicateConsumer, + @Nullable Consumer predicateConsumer, SqlAstCreationState creationState) { return null; } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/NonAggregatedIdentifierMappingImpl.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/NonAggregatedIdentifierMappingImpl.java index 54871a77f99e..243fb06e6faa 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/NonAggregatedIdentifierMappingImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/NonAggregatedIdentifierMappingImpl.java @@ -15,6 +15,7 @@ import org.hibernate.engine.spi.EntityKey; import org.hibernate.engine.spi.PersistenceContext; import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.event.spi.MergeContext; import org.hibernate.internal.util.collections.CollectionHelper; import org.hibernate.mapping.Component; import org.hibernate.mapping.RootClass; @@ -237,6 +238,11 @@ public String getAttributeName() { @Override public Object getIdentifier(Object entity) { + return getIdentifier( entity, null ); + } + + @Override + public Object getIdentifier(Object entity, MergeContext mergeContext) { if ( hasContainingClass() ) { final LazyInitializer lazyInitializer = HibernateProxy.extractLazyInitializer( entity ); if ( lazyInitializer != null ) { @@ -259,16 +265,16 @@ public Object getIdentifier(Object entity) { //JPA 2 @MapsId + @IdClass points to the pk of the entity else if ( attributeMapping instanceof ToOneAttributeMapping && !( identifierValueMapper.getAttributeMapping( i ) instanceof ToOneAttributeMapping ) ) { + final Object toOne = getIfMerged( o, mergeContext ); final ToOneAttributeMapping toOneAttributeMapping = (ToOneAttributeMapping) attributeMapping; final ModelPart targetPart = toOneAttributeMapping.getForeignKeyDescriptor().getPart( toOneAttributeMapping.getSideNature().inverse() ); if ( targetPart.isEntityIdentifierMapping() ) { - propertyValues[i] = ( (EntityIdentifierMapping) targetPart ).getIdentifier( o ); + propertyValues[i] = ( (EntityIdentifierMapping) targetPart ).getIdentifier( toOne, mergeContext ); } else { - propertyValues[i] = o; - assert false; + propertyValues[i] = toOne; } } else { @@ -285,6 +291,16 @@ else if ( attributeMapping instanceof ToOneAttributeMapping } } + private static Object getIfMerged(Object o, MergeContext mergeContext) { + if ( mergeContext != null ) { + final Object merged = mergeContext.get( o ); + if ( merged != null ) { + return merged; + } + } + return o; + } + @Override public void setIdentifier(Object entity, Object id, SharedSessionContractImplementor session) { final Object[] propertyValues = new Object[identifierValueMapper.getNumberOfAttributeMappings()]; diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/OneToManyCollectionPart.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/OneToManyCollectionPart.java index d6a082027d57..495641d6c657 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/OneToManyCollectionPart.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/OneToManyCollectionPart.java @@ -31,6 +31,8 @@ import org.hibernate.sql.ast.tree.from.TableGroupJoinProducer; import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.checkerframework.checker.nullness.qual.Nullable; + import static java.util.Objects.requireNonNullElse; /** @@ -154,9 +156,9 @@ public boolean isSimpleJoinPredicate(Predicate predicate) { public TableGroupJoin createTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType requestedJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType requestedJoinType, boolean fetched, boolean addsPredicate, SqlAstCreationState creationState) { @@ -193,11 +195,11 @@ public TableGroupJoin createTableGroupJoin( public TableGroup createRootTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType sqlAstJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType sqlAstJoinType, boolean fetched, - Consumer predicateConsumer, + @Nullable Consumer predicateConsumer, SqlAstCreationState creationState) { return createTableGroupInternal( true, diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/PluralAttributeMappingImpl.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/PluralAttributeMappingImpl.java index 5e60be959f0e..9a4ebf81d065 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/PluralAttributeMappingImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/PluralAttributeMappingImpl.java @@ -76,6 +76,8 @@ import org.jboss.logging.Logger; +import org.checkerframework.checker.nullness.qual.Nullable; + import static org.hibernate.boot.model.internal.SoftDeleteHelper.createNonSoftDeletedRestriction; import static org.hibernate.boot.model.internal.SoftDeleteHelper.resolveSoftDeleteMapping; @@ -684,9 +686,9 @@ public boolean isSimpleJoinPredicate(Predicate predicate) { public TableGroupJoin createTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType requestedJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType requestedJoinType, boolean fetched, boolean addsPredicate, SqlAstCreationState creationState) { @@ -736,6 +738,16 @@ public TableGroupJoin createTableGroupJoin( creationState ); + if ( fetched ) { + if ( orderByFragment != null ) { + creationState.applyOrdering( tableGroup, orderByFragment ); + } + + if ( manyToManyOrderByFragment != null ) { + creationState.applyOrdering( tableGroup, manyToManyOrderByFragment ); + } + } + final TableGroupJoin tableGroupJoin = new TableGroupJoin( navigablePath, determineSqlJoinType( lhs, requestedJoinType, fetched ), @@ -803,7 +815,7 @@ private void applySoftDeleteRestriction( } } - public SqlAstJoinType determineSqlJoinType(TableGroup lhs, SqlAstJoinType requestedJoinType, boolean fetched) { + public SqlAstJoinType determineSqlJoinType(TableGroup lhs, @Nullable SqlAstJoinType requestedJoinType, boolean fetched) { if ( hasSoftDelete() ) { return SqlAstJoinType.LEFT; } @@ -825,11 +837,11 @@ public SqlAstJoinType determineSqlJoinType(TableGroup lhs, SqlAstJoinType reques public TableGroup createRootTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType requestedJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType requestedJoinType, boolean fetched, - Consumer predicateConsumer, + @Nullable Consumer predicateConsumer, SqlAstCreationState creationState) { return createRootTableGroupJoin( navigablePath, @@ -862,8 +874,10 @@ private TableGroup createRootTableGroupJoin( if ( collectionDescriptor.isOneToMany() ) { tableGroup = createOneToManyTableGroup( lhs.canUseInnerJoins() && joinType == SqlAstJoinType.INNER, + joinType, navigablePath, fetched, + addsPredicate, explicitSourceAlias, sqlAliasBase, creationState @@ -897,8 +911,10 @@ public void setForeignKeyDescriptor(ForeignKeyDescriptor fkDescriptor) { private TableGroup createOneToManyTableGroup( boolean canUseInnerJoins, + SqlAstJoinType joinType, NavigablePath navigablePath, boolean fetched, + boolean addsPredicate, String sourceAlias, SqlAliasBase explicitSqlAliasBase, SqlAstCreationState creationState) { @@ -921,6 +937,12 @@ private TableGroup createOneToManyTableGroup( elementTableGroup, creationState.getCreationContext().getSessionFactory() ); + // For inner joins we never need join nesting + final boolean nestedJoin = joinType != SqlAstJoinType.INNER + // For outer joins we need nesting if there might be an on-condition that refers to the element table + && ( addsPredicate + || isAffectedByEnabledFilters( creationState.getLoadQueryInfluencers(), creationState.applyOnlyLoadByKeyFilters() ) + || collectionDescriptor.hasWhereRestrictions() ); if ( indexDescriptor instanceof TableGroupJoinProducer ) { final TableGroupJoin tableGroupJoin = ( (TableGroupJoinProducer) indexDescriptor ).createTableGroupJoin( @@ -928,12 +950,12 @@ private TableGroup createOneToManyTableGroup( tableGroup, null, sqlAliasBase, - SqlAstJoinType.INNER, + joinType, fetched, false, creationState ); - tableGroup.registerIndexTableGroup( tableGroupJoin ); + tableGroup.registerIndexTableGroup( tableGroupJoin, nestedJoin ); } return tableGroup; @@ -1023,8 +1045,10 @@ public TableGroup createRootTableGroup( if ( getCollectionDescriptor().isOneToMany() ) { return createOneToManyTableGroup( canUseInnerJoins, + SqlAstJoinType.INNER, navigablePath, false, + false, explicitSourceAlias, explicitSqlAliasBase, creationState diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ToOneAttributeMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ToOneAttributeMapping.java index 66eda808aed8..58a1b7d79d63 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ToOneAttributeMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ToOneAttributeMapping.java @@ -67,6 +67,7 @@ import org.hibernate.property.access.spi.PropertyAccess; import org.hibernate.proxy.HibernateProxy; import org.hibernate.proxy.LazyInitializer; +import org.hibernate.query.sqm.sql.SqmToSqlAstConverter; import org.hibernate.spi.EntityIdentifierNavigablePath; import org.hibernate.spi.NavigablePath; import org.hibernate.spi.TreatedNavigablePath; @@ -114,6 +115,8 @@ import org.hibernate.type.Type; import org.hibernate.type.descriptor.java.JavaType; +import org.checkerframework.checker.nullness.qual.Nullable; + import static org.hibernate.boot.model.internal.SoftDeleteHelper.createNonSoftDeletedRestriction; /** @@ -458,8 +461,8 @@ the navigable path is NavigablePath(Card.fields.{element}.{id}.card) and it does propertyType = entityBinding.getIdentifierMapper().getType(); } if ( entityBinding.getIdentifierProperty() == null ) { - final CompositeType compositeType; - if ( propertyType.isComponentType() && ( compositeType = (CompositeType) propertyType ).isEmbedded() + final ComponentType compositeType; + if ( propertyType instanceof ComponentType && ( compositeType = (ComponentType) propertyType ).isEmbedded() && compositeType.getPropertyNames().length == 1 ) { this.targetKeyPropertyName = compositeType.getPropertyNames()[0]; addPrefixedPropertyPaths( @@ -518,8 +521,8 @@ the navigable path is NavigablePath(Card.fields.{element}.{id}.card) and it does this.targetKeyPropertyNames = targetKeyPropertyNames; } else { - final CompositeType compositeType; - if ( propertyType.isComponentType() && ( compositeType = (CompositeType) propertyType ).isEmbedded() + final ComponentType compositeType; + if ( propertyType instanceof ComponentType && ( compositeType = (ComponentType) propertyType ).isEmbedded() && compositeType.getPropertyNames().length == 1 ) { final Set targetKeyPropertyNames = new HashSet<>( 2 ); this.targetKeyPropertyName = compositeType.getPropertyNames()[0]; @@ -769,7 +772,7 @@ public static void addPrefixedPropertyNames( if ( prefix != null ) { targetKeyPropertyNames.add( prefix ); } - if ( type.isComponentType() ) { + if ( type instanceof ComponentType ) { final CompositeType componentType = (CompositeType) type; final String[] propertyNames = componentType.getPropertyNames(); final Type[] componentTypeSubtypes = componentType.getSubtypes(); @@ -784,7 +787,7 @@ public static void addPrefixedPropertyNames( addPrefixedPropertyNames( targetKeyPropertyNames, newPrefix, componentTypeSubtypes[i], factory ); } } - else if ( type.isEntityType() ) { + else if ( type instanceof EntityType ) { final EntityType entityType = (EntityType) type; final Type identifierOrUniqueKeyType = entityType.getIdentifierOrUniqueKeyType( factory ); final String propertyName; @@ -858,6 +861,7 @@ public void setForeignKeyDescriptor(ForeignKeyDescriptor foreignKeyDescriptor) { // * the association does not force a join (`@NotFound`, nullable 1-1, ...) // Otherwise we need to join to the associated entity table(s) final boolean forceJoin = hasNotFoundAction() + || entityMappingType.getSoftDeleteMapping() != null || ( cardinality == Cardinality.ONE_TO_ONE && isNullable() ); this.canUseParentTableGroup = ! forceJoin && sideNature == ForeignKeyDescriptor.Nature.KEY @@ -914,6 +918,10 @@ public Cardinality getCardinality() { return cardinality; } + public boolean hasJoinTable() { + return hasJoinTable; + } + @Override public EntityMappingType getMappedType() { return getEntityMappingType(); @@ -1330,14 +1338,7 @@ else if ( CollectionPart.Nature.fromNameExact( parentNavigablePath.getLocalName( // We get here is this is a lazy collection initialization for which we know the owner is in the PC // So we create a delayed fetch, as we are sure to find the entity in the PC final FromClauseAccess fromClauseAccess = creationState.getSqlAstCreationState().getFromClauseAccess(); - final NavigablePath realParent; - if ( CollectionPart.Nature.fromNameExact( parentNavigablePath.getLocalName() ) != null ) { - realParent = parentNavigablePath.getParent(); - } - else { - realParent = parentNavigablePath; - } - final TableGroup parentTableGroup = fromClauseAccess.getTableGroup( realParent ); + final TableGroup parentTableGroup = fromClauseAccess.getTableGroup( parentNavigablePath ); final DomainResult domainResult; if ( sideNature == ForeignKeyDescriptor.Nature.KEY ) { domainResult = foreignKeyDescriptor.createKeyDomainResult( @@ -1560,7 +1561,19 @@ public static class EntityB { having the left join we don't want to add an extra implicit join that will be translated into an SQL inner join (see HHH-15342) */ - if ( fetchTiming == FetchTiming.IMMEDIATE && selected ) { + + final ForeignKeyDescriptor.Nature resolvingKeySideOfForeignKey = creationState.getCurrentlyResolvingForeignKeyPart(); + final ForeignKeyDescriptor.Nature side; + if ( resolvingKeySideOfForeignKey == ForeignKeyDescriptor.Nature.KEY && this.sideNature == ForeignKeyDescriptor.Nature.TARGET ) { + // If we are currently resolving the key part of a foreign key we do not want to add joins. + // So if the lhs of this association is the target of the FK, we have to use the KEY part to avoid a join + side = ForeignKeyDescriptor.Nature.KEY; + } + else { + side = this.sideNature; + } + + if ( ( fetchTiming == FetchTiming.IMMEDIATE && selected ) || needsJoinFetch( side ) ) { final TableGroup tableGroup = determineTableGroupForFetch( fetchablePath, fetchParent, @@ -1590,8 +1603,7 @@ public static class EntityB { } } else if ( hasNotFoundAction() - || getAssociatedEntityMappingType().getSoftDeleteMapping() != null - || affectedByEnabledFilters ) { + || getAssociatedEntityMappingType().getSoftDeleteMapping() != null ) { // For the target side only add keyResult when a not-found action is present keyResult = foreignKeyDescriptor.createTargetDomainResult( fetchablePath, @@ -1639,16 +1651,6 @@ else if ( hasNotFoundAction() */ - final ForeignKeyDescriptor.Nature resolvingKeySideOfForeignKey = creationState.getCurrentlyResolvingForeignKeyPart(); - final ForeignKeyDescriptor.Nature side; - if ( resolvingKeySideOfForeignKey == ForeignKeyDescriptor.Nature.KEY && this.sideNature == ForeignKeyDescriptor.Nature.TARGET ) { - // If we are currently resolving the key part of a foreign key we do not want to add joins. - // So if the lhs of this association is the target of the FK, we have to use the KEY part to avoid a join - side = ForeignKeyDescriptor.Nature.KEY; - } - else { - side = this.sideNature; - } final DomainResult keyResult; if ( side == ForeignKeyDescriptor.Nature.KEY ) { final TableGroup tableGroup = sideNature == ForeignKeyDescriptor.Nature.KEY @@ -1700,6 +1702,22 @@ else if ( hasNotFoundAction() ); } + private boolean needsJoinFetch(ForeignKeyDescriptor.Nature side) { + if ( side == ForeignKeyDescriptor.Nature.TARGET ) { + // The target model part doesn't correspond to the identifier of the target entity mapping + // so we must eagerly fetch with a join (subselect would still cause problems). + final EntityIdentifierMapping identifier = entityMappingType.getIdentifierMapping(); + final ValuedModelPart targetPart = foreignKeyDescriptor.getTargetPart(); + if ( identifier != targetPart ) { + // If the identifier and the target part of the same class, we can preserve laziness as deferred loading will still work + return identifier.getExpressibleJavaType().getJavaTypeClass() != targetPart.getExpressibleJavaType() + .getJavaTypeClass(); + } + } + + return false; + } + private boolean isAffectedByEnabledFilters(DomainResultCreationState creationState) { final LoadQueryInfluencers loadQueryInfluencers = creationState.getSqlAstCreationState() .getLoadQueryInfluencers(); @@ -1981,9 +1999,9 @@ public Fetchable getFetchable(int position) { public TableGroupJoin createTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType requestedJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType requestedJoinType, boolean fetched, boolean addsPredicate, SqlAstCreationState creationState) { @@ -2075,7 +2093,11 @@ public TableGroupJoin createTableGroupJoin( final TableGroupJoin join = new TableGroupJoin( navigablePath, - joinType, + // Avoid checking for nested joins in here again, since this is already done in createRootTableGroupJoin + // and simply rely on the canUseInnerJoins flag instead for override the join type to LEFT + requestedJoinType == null && !lazyTableGroup.canUseInnerJoins() + ? SqlAstJoinType.LEFT + : joinType, lazyTableGroup, null ); @@ -2147,7 +2169,7 @@ public TableGroupJoin createTableGroupJoin( } @Override - public SqlAstJoinType determineSqlJoinType(TableGroup lhs, SqlAstJoinType requestedJoinType, boolean fetched) { + public SqlAstJoinType determineSqlJoinType(TableGroup lhs, @Nullable SqlAstJoinType requestedJoinType, boolean fetched) { if ( requestedJoinType != null ) { return requestedJoinType; } @@ -2163,11 +2185,11 @@ public SqlAstJoinType determineSqlJoinType(TableGroup lhs, SqlAstJoinType reques public LazyTableGroup createRootTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType requestedJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType requestedJoinType, boolean fetched, - Consumer predicateConsumer, + @Nullable Consumer predicateConsumer, SqlAstCreationState creationState) { final SqlAliasBase sqlAliasBase = SqlAliasBase.from( explicitSqlAliasBase, @@ -2177,18 +2199,16 @@ public LazyTableGroup createRootTableGroupJoin( ); final SoftDeleteMapping softDeleteMapping = getAssociatedEntityMappingType().getSoftDeleteMapping(); - final boolean canUseInnerJoin; - if ( ! lhs.canUseInnerJoins() ) { - canUseInnerJoin = false; - } - else if ( isNullable - || hasNotFoundAction() - || softDeleteMapping != null ) { + final SqlAstJoinType currentlyProcessingJoinType = creationState instanceof SqmToSqlAstConverter + ? ( (SqmToSqlAstConverter) creationState ).getCurrentlyProcessingJoinType() + : null; + if ( currentlyProcessingJoinType != null && currentlyProcessingJoinType != SqlAstJoinType.INNER ) { + // Don't change the join type though, as that has implications for eager initialization of a LazyTableGroup canUseInnerJoin = false; } else { - canUseInnerJoin = requestedJoinType == SqlAstJoinType.INNER; + canUseInnerJoin = determineSqlJoinType( lhs, requestedJoinType, fetched ) == SqlAstJoinType.INNER; } TableGroup realParentTableGroup = lhs; diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/AbstractIdentifiableType.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/AbstractIdentifiableType.java index 7686ca7e3eb3..8115f5b29889 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/AbstractIdentifiableType.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/AbstractIdentifiableType.java @@ -420,11 +420,10 @@ else if ( id != null ) { } else { assert type instanceof EmbeddableDomainType; - final EmbeddableDomainType compositeType = (EmbeddableDomainType) type; return new EmbeddedSqmPathSource<>( EntityIdentifierMapping.ID_ROLE_NAME, (SqmPathSource) id, - compositeType, + (EmbeddableDomainType) type, Bindable.BindableType.SINGULAR_ATTRIBUTE, id.isGeneric() ); diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AbstractPluralAttribute.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AbstractPluralAttribute.java index 116230ccf1f1..f7c9f2aa8cf9 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AbstractPluralAttribute.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AbstractPluralAttribute.java @@ -153,9 +153,12 @@ public Class getBindableJavaType() { return getElementType().getJavaType(); } + @SuppressWarnings("unchecked") @Override public SqmPath createSqmPath(SqmPath lhs, SqmPathSource intermediatePathSource) { - return new SqmPluralValuedSimplePath<>( + // We need an unchecked cast here : PluralPersistentAttribute implements path source with its element type + // but resolving paths from it must produce collection-typed expressions. + return (SqmPath) new SqmPluralValuedSimplePath<>( PathHelper.append( lhs, this, intermediatePathSource ), this, lhs, diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/JpaMetamodelImpl.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/JpaMetamodelImpl.java index 4367821420c9..a53d5921c62b 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/JpaMetamodelImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/JpaMetamodelImpl.java @@ -289,7 +289,21 @@ public Set> getEmbeddables() { @Override public EnumJavaType getEnumType(String className) { - return enumJavaTypes.get( className ); + final EnumJavaType enumJavaType = enumJavaTypes.get( className ); + if ( enumJavaType != null ) { + return enumJavaType; + } + final ClassLoaderService classLoaderService = serviceRegistry.getService( ClassLoaderService.class ); + try { + final Class clazz = classLoaderService.classForName( className ); + if ( clazz == null || !clazz.isEnum() ) { + return null; + } + return new EnumJavaType( clazz ); + } + catch (ClassLoadingException e) { + throw new RuntimeException( e ); + } } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/MappingMetamodelImpl.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/MappingMetamodelImpl.java index 8a73cad47ec2..05f3d784bab4 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/MappingMetamodelImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/MappingMetamodelImpl.java @@ -47,6 +47,7 @@ import org.hibernate.metamodel.mapping.MappingModelExpressible; import org.hibernate.metamodel.mapping.internal.MappingModelCreationProcess; import org.hibernate.metamodel.model.domain.BasicDomainType; +import org.hibernate.metamodel.model.domain.DomainType; import org.hibernate.metamodel.model.domain.EmbeddableDomainType; import org.hibernate.metamodel.model.domain.EntityDomainType; import org.hibernate.metamodel.model.domain.ManagedDomainType; @@ -316,7 +317,7 @@ private void processBootCollections( ); collectionPersisterMap.put( model.getRole(), persister ); Type indexType = persister.getIndexType(); - if ( indexType != null && indexType.isEntityType() && !indexType.isAnyType() ) { + if ( indexType instanceof org.hibernate.type.EntityType ) { String entityName = ( (org.hibernate.type.EntityType) indexType ).getAssociatedEntityName(); Set roles = collectionRolesByEntityParticipant.get( entityName ); //noinspection Java8MapApi @@ -327,7 +328,7 @@ private void processBootCollections( roles.add( persister.getRole() ); } Type elementType = persister.getElementType(); - if ( elementType.isEntityType() && !elementType.isAnyType() ) { + if ( elementType instanceof org.hibernate.type.EntityType ) { String entityName = ( (org.hibernate.type.EntityType) elementType ).getAssociatedEntityName(); Set roles = collectionRolesByEntityParticipant.get( entityName ); //noinspection Java8MapApi @@ -791,8 +792,12 @@ private String[] doGetImplementors(Class clazz) throws MappingException { public MappingModelExpressible resolveMappingExpressible( SqmExpressible sqmExpressible, Function tableGroupLocator) { - if ( sqmExpressible instanceof SqmPath ) { + if ( sqmExpressible instanceof SqmPath ) { final SqmPath sqmPath = (SqmPath) sqmExpressible; + final DomainType sqmPathType = sqmPath.getResolvedModel().getSqmPathType(); + if ( sqmPathType instanceof MappingModelExpressible ) { + return (MappingModelExpressible) sqmPathType; + } final NavigablePath navigablePath = sqmPath.getNavigablePath(); if ( navigablePath.getParent() != null ) { final TableGroup parentTableGroup = tableGroupLocator.apply( navigablePath.getParent() ); diff --git a/hibernate-core/src/main/java/org/hibernate/persister/collection/AbstractCollectionPersister.java b/hibernate-core/src/main/java/org/hibernate/persister/collection/AbstractCollectionPersister.java index 55a16ed66a1e..fc4628d84126 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/collection/AbstractCollectionPersister.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/collection/AbstractCollectionPersister.java @@ -140,6 +140,7 @@ import org.hibernate.type.AnyType; import org.hibernate.type.BasicType; import org.hibernate.type.CollectionType; +import org.hibernate.type.ComponentType; import org.hibernate.type.CompositeType; import org.hibernate.type.EntityType; import org.hibernate.type.MetaType; @@ -386,7 +387,7 @@ public AbstractCollectionPersister( // ELEMENT - if ( elementType.isEntityType() ) { + if ( elementType instanceof EntityType ) { String entityName = ( (EntityType) elementType ).getAssociatedEntityName(); elementPersister = creationContext.getDomainModel().getEntityDescriptor( entityName ); // NativeSQL: collect element column and auto-aliases @@ -437,7 +438,7 @@ public AbstractCollectionPersister( creationContext.getFunctionRegistry() ); elementColumnIsGettable[j] = true; - if ( elementType.isComponentType() ) { + if ( elementType instanceof ComponentType || elementType instanceof AnyType ) { // Implements desired behavior specifically for @ElementCollection mappings. elementColumnIsSettable[j] = columnInsertability[j]; } @@ -555,7 +556,7 @@ else if ( indexedCollection instanceof org.hibernate.mapping.Map elementClass = null; // elementType.returnedClass(); } - if ( elementType.isComponentType() ) { + if ( elementType instanceof ComponentType || elementType instanceof AnyType ) { elementPropertyMapping = new CompositeElementPropertyMapping( elementColumnNames, elementColumnReaders, @@ -565,7 +566,7 @@ else if ( indexedCollection instanceof org.hibernate.mapping.Map creationContext.getMetadata() ); } - else if ( !elementType.isEntityType() ) { + else if ( !( elementType instanceof EntityType ) ) { elementPropertyMapping = new ElementPropertyMapping( elementColumnNames, elementType ); } else { @@ -1534,7 +1535,7 @@ private void initCollectionPropertyMap(String aliasName, Type type, String[] col collectionPropertyColumnAliases.put( aliasName, columnAliases ); //TODO: this code is almost certainly obsolete and can be removed - if ( type.isComponentType() ) { + if ( type instanceof ComponentType || type instanceof AnyType ) { CompositeType ct = (CompositeType) type; String[] propertyNames = ct.getPropertyNames(); for ( int i = 0; i < propertyNames.length; i++ ) { diff --git a/hibernate-core/src/main/java/org/hibernate/persister/collection/BasicCollectionPersister.java b/hibernate-core/src/main/java/org/hibernate/persister/collection/BasicCollectionPersister.java index e948281080bb..873d8bcbb7df 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/collection/BasicCollectionPersister.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/collection/BasicCollectionPersister.java @@ -60,6 +60,7 @@ import org.hibernate.sql.model.ast.builder.TableUpdateBuilderStandard; import org.hibernate.sql.model.internal.TableUpdateStandard; import org.hibernate.sql.model.jdbc.JdbcMutationOperation; +import org.hibernate.type.EntityType; import static org.hibernate.internal.util.collections.CollectionHelper.arrayList; import static org.hibernate.sql.model.ModelMutationLogging.MODEL_MUTATION_LOGGER; @@ -770,7 +771,7 @@ public boolean isOneToMany() { @Override public boolean isManyToMany() { - return elementType.isEntityType(); //instanceof AssociationType; + return elementType instanceof EntityType; //instanceof AssociationType; } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java index f06efdcaf3d8..abef1ac61a24 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java @@ -100,6 +100,7 @@ import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.event.spi.EventSource; import org.hibernate.event.spi.LoadEvent; +import org.hibernate.event.spi.MergeContext; import org.hibernate.generator.BeforeExecutionGenerator; import org.hibernate.generator.EventType; import org.hibernate.generator.Generator; @@ -293,8 +294,10 @@ import org.hibernate.type.AssociationType; import org.hibernate.type.BasicType; import org.hibernate.type.CollectionType; +import org.hibernate.type.ComponentType; import org.hibernate.type.CompositeType; import org.hibernate.type.EntityType; +import org.hibernate.type.ManyToOneType; import org.hibernate.type.Type; import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.descriptor.java.MutabilityPlan; @@ -396,6 +399,7 @@ public abstract class AbstractEntityPersister private final int[] lazyPropertyNumbers; private final Type[] lazyPropertyTypes; private final String[][] lazyPropertyColumnAliases; + private final Set nonLazyPropertyNames; //information about all properties in class hierarchy private final String[] subclassPropertyNameClosure; @@ -494,6 +498,7 @@ public abstract class AbstractEntityPersister private final boolean implementsLifecycle; private List uniqueKeyEntries = null; //lazily initialized + private ConcurrentHashMap nonLazyPropertyLoadPlansByName; @Deprecated(since = "6.0") public AbstractEntityPersister( @@ -644,6 +649,7 @@ public AbstractEntityPersister( propertyColumnUpdateable = new boolean[hydrateSpan][]; propertyColumnInsertable = new boolean[hydrateSpan][]; sharedColumnNames = new HashSet<>(); + nonLazyPropertyNames = new HashSet<>(); final HashSet thisClassProperties = new HashSet<>(); final ArrayList lazyNames = new ArrayList<>(); @@ -709,6 +715,9 @@ public AbstractEntityPersister( lazyTypes.add( prop.getValue().getType() ); lazyColAliases.add( colAliases ); } + else { + nonLazyPropertyNames.add( prop.getName() ); + } propertyColumnUpdateable[i] = prop.getValue().getColumnUpdateability(); propertyColumnInsertable[i] = prop.getValue().getColumnInsertability(); @@ -738,6 +747,15 @@ public AbstractEntityPersister( final ArrayList definedBySubclass = new ArrayList<>(); final ArrayList propNullables = new ArrayList<>(); + if ( persistentClass.hasSubclasses() ) { + for ( Selectable selectable : persistentClass.getIdentifier().getSelectables() ) { + if ( !selectable.isFormula() ) { + // Identifier columns are always shared between subclasses + sharedColumnNames.add( ( (Column) selectable ).getQuotedName( dialect ) ); + } + } + } + for ( Property prop : persistentClass.getSubclassPropertyClosure() ) { names.add( prop.getName() ); types.add( prop.getType() ); @@ -963,7 +981,7 @@ else if ( entityMetamodel.isMutable() ) { // 2) have no associations. // Eventually we want to be a little more lenient with associations. for ( Type type : getSubclassPropertyTypeClosure() ) { - if ( type.isAssociationType() ) { + if ( type instanceof AnyType || type instanceof CollectionType || type instanceof EntityType ) { return false; } } @@ -1294,6 +1312,17 @@ private static List initUniqueKeyEntries(final AbstractEntityPer uniqueKeys.add( new UniqueKeyEntry( ukName, index, type ) ); } } + else if ( associationType instanceof ManyToOneType ) { + final ManyToOneType manyToOneType = ( (ManyToOneType) associationType ); + if ( manyToOneType.isLogicalOneToOne() && manyToOneType.isReferenceToPrimaryKey() ) { + final AttributeMapping attributeMapping = aep.findAttributeMapping( manyToOneType.getPropertyName() ); + if ( attributeMapping != null ) { + final int index = attributeMapping.getStateArrayPosition(); + final Type type = aep.getPropertyTypes()[index]; + uniqueKeys.add( new UniqueKeyEntry( manyToOneType.getPropertyName(), index, type ) ); + } + } + } } } return CollectionHelper.toSmallList( uniqueKeys ); @@ -1329,6 +1358,10 @@ private SingleIdArrayLoadPlan createLazyLoadPlan(List f partsToSelect.add( getAttributeMapping( getSubclassPropertyIndex( lazyAttributeDescriptor.getName() ) ) ); } + return createLazyLoanPlan( partsToSelect ); + } + + private SingleIdArrayLoadPlan createLazyLoanPlan(List partsToSelect) { if ( partsToSelect.isEmpty() ) { // only one-to-one is lazily fetched return null; @@ -1540,7 +1573,7 @@ public Object initializeLazyProperty(String fieldName, Object entity, SharedSess if ( hasCollections() ) { final Type type = getPropertyType( fieldName ); - if ( type.isCollectionType() ) { + if ( type instanceof CollectionType ) { // we have a condition where a collection attribute is being access via enhancement: // we can circumvent all the rest and just return the PersistentCollection final CollectionType collectionType = (CollectionType) type; @@ -1654,75 +1687,117 @@ protected Object initializeLazyPropertiesFromDatastore( final EntityEntry entry, final String fieldName, final SharedSessionContractImplementor session) { - - if ( !hasLazyProperties() ) { - throw new AssertionFailure( "no lazy properties" ); + if ( nonLazyPropertyNames.contains( fieldName ) ) { + // An eager property can be lazy because of an applied EntityGraph + final List partsToSelect = new ArrayList<>(1); + int propertyIndex = getPropertyIndex( fieldName ); + partsToSelect.add( getAttributeMapping( propertyIndex ) ); + SingleIdArrayLoadPlan lazyLoanPlan; + ConcurrentHashMap propertyLoadPlansByName = this.nonLazyPropertyLoadPlansByName; + if ( propertyLoadPlansByName == null ) { + propertyLoadPlansByName = new ConcurrentHashMap<>(); + lazyLoanPlan = createLazyLoanPlan( partsToSelect ); + propertyLoadPlansByName.put( fieldName, lazyLoanPlan ); + this.nonLazyPropertyLoadPlansByName = propertyLoadPlansByName; + } + else { + lazyLoanPlan = nonLazyPropertyLoadPlansByName.get( fieldName ); + if ( lazyLoanPlan == null ) { + lazyLoanPlan = createLazyLoanPlan( partsToSelect ); + nonLazyPropertyLoadPlansByName.put( fieldName, lazyLoanPlan ); + } + } + try { + final Object[] values = lazyLoanPlan.load( id, session ); + final Object selectedValue = values[0]; + initializeLazyProperty( + entity, + entry, + selectedValue, + propertyIndex, + getPropertyTypes()[propertyIndex] + ); + return selectedValue; + } + catch (JDBCException ex) { + throw session.getJdbcServices().getSqlExceptionHelper().convert( + ex.getSQLException(), + "could not initialize lazy properties: " + infoString( this, id, getFactory() ), + lazyLoanPlan.getJdbcSelect().getSqlString() + ); + } } + else { + if ( !hasLazyProperties() ) { + throw new AssertionFailure( "no lazy properties" ); + } - final PersistentAttributeInterceptor interceptor = asPersistentAttributeInterceptable( entity ).$$_hibernate_getInterceptor(); - assert interceptor != null : "Expecting bytecode interceptor to be non-null"; + final PersistentAttributeInterceptor interceptor = asPersistentAttributeInterceptable( entity ).$$_hibernate_getInterceptor(); + assert interceptor != null : "Expecting bytecode interceptor to be non-null"; - LOG.tracef( "Initializing lazy properties from datastore (triggered for `%s`)", fieldName ); + LOG.tracef( "Initializing lazy properties from datastore (triggered for `%s`)", fieldName ); - final String fetchGroup = getEntityMetamodel().getBytecodeEnhancementMetadata() - .getLazyAttributesMetadata() - .getFetchGroupName( fieldName ); - final List fetchGroupAttributeDescriptors = getEntityMetamodel().getBytecodeEnhancementMetadata() - .getLazyAttributesMetadata() - .getFetchGroupAttributeDescriptors( fetchGroup ); + final String fetchGroup = getEntityMetamodel().getBytecodeEnhancementMetadata() + .getLazyAttributesMetadata() + .getFetchGroupName( fieldName ); + final List fetchGroupAttributeDescriptors = getEntityMetamodel().getBytecodeEnhancementMetadata() + .getLazyAttributesMetadata() + .getFetchGroupAttributeDescriptors( fetchGroup ); - final Set initializedLazyAttributeNames = interceptor.getInitializedLazyAttributeNames(); + final Set initializedLazyAttributeNames = interceptor.getInitializedLazyAttributeNames(); - final SingleIdArrayLoadPlan lazySelect = getSQLLazySelectLoadPlan( fetchGroup ); + final SingleIdArrayLoadPlan lazySelect = getSQLLazySelectLoadPlan( fetchGroup ); - try { - Object result = null; - final Object[] values = lazySelect.load( id, session ); - int i = 0; - for ( LazyAttributeDescriptor fetchGroupAttributeDescriptor : fetchGroupAttributeDescriptors ) { - final boolean previousInitialized = initializedLazyAttributeNames.contains( fetchGroupAttributeDescriptor.getName() ); - - if ( previousInitialized ) { - // todo : one thing we should consider here is potentially un-marking an attribute as dirty based on the selected value - // we know the current value - getPropertyValue( entity, fetchGroupAttributeDescriptor.getAttributeIndex() ); - // we know the selected value (see selectedValue below) - // we can use the attribute Type to tell us if they are the same - // - // assuming entity is a SelfDirtinessTracker we can also know if the attribute is - // currently considered dirty, and if really not dirty we would do the un-marking - // - // of course that would mean a new method on SelfDirtinessTracker to allow un-marking - - // its already been initialized (e.g. by a write) so we don't want to overwrite - i++; - continue; - } + try { + Object result = null; + final Object[] values = lazySelect.load( id, session ); + int i = 0; + for ( LazyAttributeDescriptor fetchGroupAttributeDescriptor : fetchGroupAttributeDescriptors ) { + final boolean previousInitialized = initializedLazyAttributeNames.contains( + fetchGroupAttributeDescriptor.getName() ); + + if ( previousInitialized ) { + // todo : one thing we should consider here is potentially un-marking an attribute as dirty based on the selected value + // we know the current value - getPropertyValue( entity, fetchGroupAttributeDescriptor.getAttributeIndex() ); + // we know the selected value (see selectedValue below) + // we can use the attribute Type to tell us if they are the same + // + // assuming entity is a SelfDirtinessTracker we can also know if the attribute is + // currently considered dirty, and if really not dirty we would do the un-marking + // + // of course that would mean a new method on SelfDirtinessTracker to allow un-marking + + // its already been initialized (e.g. by a write) so we don't want to overwrite + i++; + continue; + } - final Object selectedValue = values[i++]; - final boolean set = initializeLazyProperty( - fieldName, - entity, - entry, - fetchGroupAttributeDescriptor.getLazyIndex(), - selectedValue - ); - if ( set ) { - result = selectedValue; - interceptor.attributeInitialized( fetchGroupAttributeDescriptor.getName() ); + final Object selectedValue = values[i++]; + final boolean set = initializeLazyProperty( + fieldName, + entity, + entry, + fetchGroupAttributeDescriptor, + selectedValue + ); + if ( set ) { + result = selectedValue; + interceptor.attributeInitialized( fetchGroupAttributeDescriptor.getName() ); + } } - } + LOG.trace( "Done initializing lazy properties" ); - LOG.trace( "Done initializing lazy properties" ); + return result; - return result; - } - catch ( JDBCException ex ) { - throw session.getJdbcServices().getSqlExceptionHelper().convert( - ex.getSQLException(), - "could not initialize lazy properties: " + infoString( this, id, getFactory() ), - lazySelect.getJdbcSelect().getSqlString() - ); + } + catch (JDBCException ex) { + throw session.getJdbcServices().getSqlExceptionHelper().convert( + ex.getSQLException(), + "could not initialize lazy properties: " + infoString( this, id, getFactory() ), + lazySelect.getJdbcSelect().getSqlString() + ); + } } } @@ -1781,6 +1856,43 @@ protected boolean initializeLazyProperty( return fieldName.equals( lazyPropertyNames[index] ); } + + + protected boolean initializeLazyProperty( + final String fieldName, + final Object entity, + final EntityEntry entry, + LazyAttributeDescriptor fetchGroupAttributeDescriptor, + final Object propValue) { + final String name = fetchGroupAttributeDescriptor.getName(); + initializeLazyProperty( + entity, + entry, + propValue, + getPropertyIndex( name ), + fetchGroupAttributeDescriptor.getType() + ); + return fieldName.equals( name ); + } + + private void initializeLazyProperty(Object entity, EntityEntry entry, Object propValue, int index, Type type) { + setPropertyValue( entity, index, propValue ); + if ( entry.getLoadedState() != null ) { + // object have been loaded with setReadOnly(true); HHH-2236 + entry.getLoadedState()[index] = type.deepCopy( + propValue, + factory + ); + } + // If the entity has deleted state, then update that as well + if ( entry.getDeletedState() != null ) { + entry.getDeletedState()[index] = type.deepCopy( + propValue, + factory + ); + } + } + @Override public NavigableRole getNavigableRole() { return navigableRole; @@ -2292,7 +2404,7 @@ public int getSubclassPropertyTableNumber(String propertyPath) { // // performance op to avoid the array search // return 0; // } -// else if ( type.isCollectionType() ) { +// else if ( type instanceof CollectionType ) { // // properly handle property-ref-based associations // rootPropertyName = assocType.getLHSPropertyName(); // } @@ -3226,7 +3338,7 @@ else if ( discriminatorValue == NOT_NULL_DISCRIMINATOR ) { return new NullnessPredicate( sqlExpression, true ); } else if ( hasNull ) { - junction.add( new NullnessPredicate( sqlExpression ) ); + junction.add( new NullnessPredicate( sqlExpression ) ); } junction.add( predicate ); @@ -3920,7 +4032,7 @@ public boolean isSubclassPropertyNullable(int i) { public int[] findDirty(Object[] currentState, Object[] previousState, Object entity, SharedSessionContractImplementor session) throws HibernateException { int[] props = DirtyHelper.findDirty( - entityMetamodel.getProperties(), + entityMetamodel.getDirtyCheckablePropertyTypes(), currentState, previousState, propertyColumnUpdateable, @@ -4622,6 +4734,11 @@ public Object getIdentifier(Object entity, SharedSessionContractImplementor sess return identifierMapping.getIdentifier( entity ); } + @Override + public Object getIdentifier(Object entity, MergeContext mergeContext) { + return identifierMapping.getIdentifier( entity, mergeContext ); + } + @Override public void setIdentifier(Object entity, Object id, SharedSessionContractImplementor session) { identifierMapping.setIdentifier( entity, id, session ); @@ -6176,6 +6293,7 @@ public ModelPart findSubTypesSubPart(String name, EntityMappingType treatTargetT } private ModelPart getIdentifierModelPart(String name, EntityMappingType treatTargetType) { + final EntityIdentifierMapping identifierMapping = getIdentifierMappingForJoin(); if ( identifierMapping instanceof NonAggregatedIdentifierMapping ) { NonAggregatedIdentifierMapping mapping = (NonAggregatedIdentifierMapping) identifierMapping; final ModelPart subPart = mapping.findSubPart( name, treatTargetType ); @@ -6776,9 +6894,9 @@ public String[] getSubclassPropertyColumnAliases(String propertyName, String suf } // aliases for composite-id's - if ( getIdentifierType().isComponentType() ) { + if ( getIdentifierType() instanceof ComponentType ) { // Fetch embedded identifiers property names from the "virtual" identifier component - final CompositeType componentId = (CompositeType) getIdentifierType(); + final ComponentType componentId = (ComponentType) getIdentifierType(); final String[] idPropertyNames = componentId.getPropertyNames(); final String[] idAliases = getIdentifierAliases(); final String[] idColumnNames = getIdentifierColumnNames(); diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractPropertyMapping.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractPropertyMapping.java index fb46b751239b..4a330b6fae0f 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractPropertyMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractPropertyMapping.java @@ -27,6 +27,7 @@ import org.hibernate.type.AnyType; import org.hibernate.type.AssociationType; import org.hibernate.type.CollectionType; +import org.hibernate.type.ComponentType; import org.hibernate.type.CompositeType; import org.hibernate.type.EntityType; import org.hibernate.type.ManyToOneType; @@ -282,7 +283,7 @@ protected void initPropertyPaths( ); } - if ( type.isAssociationType() ) { + if ( type instanceof AnyType || type instanceof CollectionType || type instanceof EntityType ) { AssociationType actype = (AssociationType) type; if ( actype.useLHSPrimaryKey() ) { columns = getIdentifierColumnNames(); @@ -308,8 +309,20 @@ protected void initPropertyPaths( addPropertyPath( path, type, columns, columnReaders, columnReaderTemplates, formulaTemplates, factory ); } - if ( type.isComponentType() ) { - CompositeType actype = (CompositeType) type; + if ( type instanceof AnyType ) { + AnyType actype = (AnyType) type; + initComponentPropertyPaths( + path, + actype, + columns, + columnReaders, + columnReaderTemplates, + formulaTemplates, + factory + ); + } + else if ( type instanceof ComponentType ) { + ComponentType actype = (ComponentType) type; initComponentPropertyPaths( path, actype, @@ -331,7 +344,7 @@ protected void initPropertyPaths( ); } } - else if ( type.isEntityType() ) { + else if ( type instanceof EntityType ) { initIdentifierPropertyPaths( path, (EntityType) type, diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/DirtyHelper.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/DirtyHelper.java index 289677fcf241..124a16185199 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/DirtyHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/DirtyHelper.java @@ -11,6 +11,14 @@ import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.internal.util.collections.ArrayHelper; import org.hibernate.tuple.NonIdentifierAttribute; +import org.hibernate.type.AnyType; +import org.hibernate.type.BasicType; +import org.hibernate.type.CollectionType; +import org.hibernate.type.ComponentType; +import org.hibernate.type.ManyToOneType; +import org.hibernate.type.Type; + +import org.checkerframework.checker.nullness.qual.Nullable; /** * Operations for searching an array of property values for modified elements. @@ -72,6 +80,62 @@ else if ( previousState[i] == LazyPropertyInitializer.UNFETCHED_PROPERTY ) { } } + /** + * Determine if any of the given field values are dirty, returning an array containing + * indices of the dirty fields. + *

    + * If it is determined that no fields are dirty, null is returned. + * + * @param propertyTypes The property types that are dirty checkable. null entry for non-dirty checkable properties + * @param currentState The current state of the entity + * @param previousState The baseline state of the entity + * @param includeColumns Columns to be included in the dirty checking, per property + * @param session The session from which the dirty check request originated. + * + * @return Array containing indices of the dirty properties, or null if no properties considered dirty. + */ + public static int[] findDirty( + @Nullable Type[] propertyTypes, + final Object[] currentState, + final Object[] previousState, + final boolean[][] includeColumns, + final SharedSessionContractImplementor session) { + int[] results = null; + int count = 0; + int span = propertyTypes.length; + + for ( int i = 0; i < span; i++ ) { + + if ( isDirty( propertyTypes, currentState, previousState, includeColumns, session, i ) ) { + if ( results == null ) { + results = new int[span]; + } + results[count++] = i; + } + } + + return count == 0 ? null : ArrayHelper.trim( results, count ); + } + + private static boolean isDirty( + @Nullable Type[] propertyTypes, + Object[] currentState, + Object[] previousState, + boolean[][] includeColumns, + SharedSessionContractImplementor session, int i) { + final Type propertyType; + if ( currentState[i] == LazyPropertyInitializer.UNFETCHED_PROPERTY + || ( propertyType = propertyTypes[i] ) == null ) { + return false; + } + else if ( previousState[i] == LazyPropertyInitializer.UNFETCHED_PROPERTY ) { + return true; + } + else { + return propertyType.isDirty( previousState[i], currentState[i], includeColumns[i], session ); + } + } + /** * Determine if any of the given field values are modified, returning an array containing * indices of the modified fields. diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/EntityPersister.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/EntityPersister.java index a86e5f133dc5..05011c3b0a7a 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/EntityPersister.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/EntityPersister.java @@ -30,6 +30,7 @@ import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.event.spi.EventSource; +import org.hibernate.event.spi.MergeContext; import org.hibernate.generator.BeforeExecutionGenerator; import org.hibernate.generator.EventType; import org.hibernate.generator.Generator; @@ -1130,8 +1131,18 @@ default Object getValue(Object object, int i) { */ Object getIdentifier(Object entity, SharedSessionContractImplementor session); - /** - * Inject the identifier value into the given entity. + /** + * Get the identifier of an instance from the object's identifier property. + * Throw an exception if it has no identifier property. + * + * It's supposed to be use during the merging process + */ + default Object getIdentifier(Object entity, MergeContext mergeContext) { + return getIdentifier( entity, mergeContext.getEventSource() ); + } + + /** + * Inject the identifier value into the given entity. */ void setIdentifier(Object entity, Object id, SharedSessionContractImplementor session); diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/JoinedSubclassEntityPersister.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/JoinedSubclassEntityPersister.java index d7e88fe0c576..3326a87656d2 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/JoinedSubclassEntityPersister.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/JoinedSubclassEntityPersister.java @@ -127,7 +127,7 @@ public class JoinedSubclassEntityPersister extends AbstractEntityPersister { // the closure of all columns used by the entire hierarchy including // subclasses and superclasses of this class - private final int[] subclassColumnTableNumberClosure; + private final int[] subclassColumnNaturalOrderTableNumberClosure; // private final int[] subclassFormulaTableNumberClosure; private final String[] subclassColumnClosure; @@ -470,11 +470,12 @@ public JoinedSubclassEntityPersister( final String tableName = property.getValue().getTable(). getQualifiedName( creationContext.getSqlStringGenerationContext() ); final Integer tableNumber = getTableId( tableName, subclassTableNameClosure ); + final Integer naturalTableNumber = getTableId( tableName, naturalOrderSubclassTableNameClosure ); propTableNumbers.add( tableNumber ); for ( Selectable selectable : property.getSelectables() ) { if ( !selectable.isFormula() ) { - columnTableNumbers.add( tableNumber ); + columnTableNumbers.add( naturalTableNumber ); Column column = (Column) selectable; columns.add( column.getQuotedName( dialect ) ); } @@ -484,7 +485,7 @@ public JoinedSubclassEntityPersister( } } - subclassColumnTableNumberClosure = toIntArray( columnTableNumbers ); + subclassColumnNaturalOrderTableNumberClosure = toIntArray( columnTableNumbers ); subclassPropertyTableNumberClosure = toIntArray( propTableNumbers ); // subclassFormulaTableNumberClosure = ArrayHelper.toIntArray( formulaTableNumbers ); subclassColumnClosure = toStringArray( columns ); @@ -1069,12 +1070,12 @@ public int determineTableNumberForColumn(String columnName) { && subclassColumnClosure[i].endsWith( "\"" ); if ( quoted ) { if ( subclassColumnClosure[i].equals( columnName ) ) { - return subclassColumnTableNumberClosure[i]; + return subclassColumnNaturalOrderTableNumberClosure[i]; } } else { if ( subclassColumnClosure[i].equalsIgnoreCase( columnName ) ) { - return subclassColumnTableNumberClosure[i]; + return subclassColumnNaturalOrderTableNumberClosure[i]; } } } @@ -1421,6 +1422,15 @@ public void pruneForSubclasses(TableGroup tableGroup, Map } } + @Override + public EntityIdentifierMapping getIdentifierMappingForJoin() { + // If the joined subclass has a physical discriminator and has subtypes + // we must use the root table identifier mapping for joining to allow table group elimination to work + return isPhysicalDiscriminator() && !getSubMappingTypes().isEmpty() + ? getRootEntityDescriptor().getIdentifierMapping() + : super.getIdentifierMappingForJoin(); + } + private boolean applyDiscriminatorPredicate( TableReferenceJoin join, NamedTableReference tableReference, @@ -1431,9 +1441,11 @@ private boolean applyDiscriminatorPredicate( final String discriminatorPredicate = getPrunedDiscriminatorPredicate( entityNameUses, metamodel, - tableReference.getIdentificationVariable() + "t" +// tableReference.getIdentificationVariable() ); - join.applyPredicate( new SqlFragmentPredicate( discriminatorPredicate ) ); + tableReference.setPrunedTableExpression( "(select * from " + getRootTableName() + " t where " + discriminatorPredicate + ")" ); +// join.applyPredicate( new SqlFragmentPredicate( discriminatorPredicate ) ); return true; } return false; diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/AbstractDeleteCoordinator.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/AbstractDeleteCoordinator.java index 0e5f64267d98..a2f5bbb72a3d 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/AbstractDeleteCoordinator.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/AbstractDeleteCoordinator.java @@ -276,22 +276,25 @@ protected void doStaticDelete( session ); - mutationExecutor.execute( - entity, - null, - null, - (statementDetails, affectedRowCount, batchPosition) -> identifiedResultsCheck( - statementDetails, - affectedRowCount, - batchPosition, - entityPersister(), - id, - factory() - ), - session - ); - - mutationExecutor.release(); + try { + mutationExecutor.execute( + entity, + null, + null, + (statementDetails, affectedRowCount, batchPosition) -> identifiedResultsCheck( + statementDetails, + affectedRowCount, + batchPosition, + entityPersister(), + id, + factory() + ), + session + ); + } + finally { + mutationExecutor.release(); + } } protected void applyStaticDeleteTableDetails( diff --git a/hibernate-core/src/main/java/org/hibernate/procedure/internal/NamedCallableQueryMementoImpl.java b/hibernate-core/src/main/java/org/hibernate/procedure/internal/NamedCallableQueryMementoImpl.java index d79f9d4aba49..e7ffc89325dd 100644 --- a/hibernate-core/src/main/java/org/hibernate/procedure/internal/NamedCallableQueryMementoImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/procedure/internal/NamedCallableQueryMementoImpl.java @@ -64,6 +64,7 @@ public NamedCallableQueryMementoImpl( Map hints) { super( name, + Object.class, cacheable, cacheRegion, cacheMode, diff --git a/hibernate-core/src/main/java/org/hibernate/procedure/internal/ProcedureCallImpl.java b/hibernate-core/src/main/java/org/hibernate/procedure/internal/ProcedureCallImpl.java index c39b58c05a53..65e13e0c8dd7 100644 --- a/hibernate-core/src/main/java/org/hibernate/procedure/internal/ProcedureCallImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/procedure/internal/ProcedureCallImpl.java @@ -711,6 +711,7 @@ private ProcedureOutputsImpl buildOutputs() { } catch (SQLException e) { getSession().getJdbcCoordinator().getLogicalConnection().getResourceRegistry().release( statement ); + getSession().getJdbcCoordinator().afterStatementExecution(); throw getSession().getJdbcServices().getSqlExceptionHelper().convert( e, "Error registering CallableStatement parameters", diff --git a/hibernate-core/src/main/java/org/hibernate/procedure/internal/ProcedureOutputsImpl.java b/hibernate-core/src/main/java/org/hibernate/procedure/internal/ProcedureOutputsImpl.java index a691724a59d3..e4f5d26888ed 100644 --- a/hibernate-core/src/main/java/org/hibernate/procedure/internal/ProcedureOutputsImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/procedure/internal/ProcedureOutputsImpl.java @@ -147,10 +147,4 @@ protected Output buildFunctionReturn() { return buildResultSetOutput( () -> results ); } } - - @Override - public void release() { - super.release(); - getResultContext().getSession().getJdbcCoordinator().getLogicalConnection().getResourceRegistry().release( callableStatement ); - } } diff --git a/hibernate-core/src/main/java/org/hibernate/property/access/internal/AccessStrategyHelper.java b/hibernate-core/src/main/java/org/hibernate/property/access/internal/AccessStrategyHelper.java index a46a0d2c1034..61f29f9b93b9 100644 --- a/hibernate-core/src/main/java/org/hibernate/property/access/internal/AccessStrategyHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/property/access/internal/AccessStrategyHelper.java @@ -6,14 +6,11 @@ */ package org.hibernate.property.access.internal; -import java.beans.Introspector; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; -import java.util.Locale; -import org.hibernate.MappingException; import org.hibernate.PropertyNotFoundException; import org.hibernate.bytecode.enhance.spi.interceptor.BytecodeLazyAttributeInterceptor; import org.hibernate.engine.spi.CompositeOwner; @@ -33,6 +30,7 @@ import static org.hibernate.engine.internal.ManagedTypeHelper.isPersistentAttributeInterceptableType; import static org.hibernate.internal.util.ReflectHelper.NO_PARAM_SIGNATURE; import static org.hibernate.internal.util.ReflectHelper.findField; +import static org.hibernate.internal.util.ReflectHelper.getterMethodOrNull; import static org.hibernate.internal.util.ReflectHelper.isRecord; /** @@ -87,89 +85,9 @@ public static AccessType getAccessType(Class containerJavaType, String proper return AccessType.FIELD; } - for ( Method method : containerClass.getDeclaredMethods() ) { - // if the method has parameters, skip it - if ( method.getParameterCount() != 0 ) { - continue; - } - - // if the method is a "bridge", skip it - if ( method.isBridge() ) { - continue; - } - - if ( method.isAnnotationPresent( Transient.class ) ) { - continue; - } - - if ( Modifier.isStatic( method.getModifiers() ) ) { - continue; - } - - final String methodName = method.getName(); - - // try "get" - if ( methodName.startsWith( "get" ) ) { - final String stemName = methodName.substring( 3 ); - final String decapitalizedStemName = Introspector.decapitalize( stemName ); - if ( stemName.equals( propertyName ) || decapitalizedStemName.equals( propertyName ) ) { - if ( method.isAnnotationPresent( Access.class ) ) { - return AccessType.PROPERTY; - } - else { - checkIsMethodVariant( containerClass, propertyName, method, stemName ); - } - } - } - - // if not "get", then try "is" - if ( methodName.startsWith( "is" ) ) { - final String stemName = methodName.substring( 2 ); - String decapitalizedStemName = Introspector.decapitalize( stemName ); - if ( stemName.equals( propertyName ) || decapitalizedStemName.equals( propertyName ) ) { - if ( method.isAnnotationPresent( Access.class ) ) { - return AccessType.PROPERTY; - } - } - } - } - - return null; - } - - private static void checkIsMethodVariant( - Class containerClass, - String propertyName, - Method method, - String stemName) { - final Method isMethodVariant = findIsMethodVariant( containerClass, stemName ); - if ( isMethodVariant == null ) { - return; - } - - if ( !isMethodVariant.isAnnotationPresent( Access.class ) ) { - throw new MappingException( - String.format( - Locale.ROOT, - "Class '%s' declares both 'get' [%s] and 'is' [%s] variants of getter for property '%s'", - containerClass.getName(), - method.toString(), - isMethodVariant, - propertyName - ) - ); - } - } - - public static @Nullable Method findIsMethodVariant(Class containerClass, String stemName) { - // verify that the Class does not also define a method with the same stem name with 'is' - try { - final Method isMethod = containerClass.getDeclaredMethod( "is" + stemName ); - if ( !Modifier.isStatic( isMethod.getModifiers() ) && isMethod.getAnnotation( Transient.class ) == null ) { - return isMethod; - } - } - catch (NoSuchMethodException ignore) { + final Method getter = getterMethodOrNull( containerClass, propertyName ); + if ( getter != null && getter.isAnnotationPresent( Access.class ) ) { + return AccessType.PROPERTY; } return null; diff --git a/hibernate-core/src/main/java/org/hibernate/property/access/internal/PropertyAccessEnhancedImpl.java b/hibernate-core/src/main/java/org/hibernate/property/access/internal/PropertyAccessEnhancedImpl.java index 3a6e3e51b476..6782d1e39ec3 100644 --- a/hibernate-core/src/main/java/org/hibernate/property/access/internal/PropertyAccessEnhancedImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/property/access/internal/PropertyAccessEnhancedImpl.java @@ -6,6 +6,9 @@ */ package org.hibernate.property.access.internal; +import jakarta.persistence.AccessType; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.hibernate.property.access.spi.EnhancedGetterFieldImpl; import org.hibernate.property.access.spi.EnhancedSetterImpl; import org.hibernate.property.access.spi.EnhancedSetterMethodImpl; import org.hibernate.property.access.spi.Getter; @@ -19,9 +22,7 @@ import java.lang.reflect.Field; import java.lang.reflect.Method; -import jakarta.persistence.AccessType; -import org.checkerframework.checker.nullness.qual.Nullable; - +import static org.hibernate.internal.util.ReflectHelper.findField; import static org.hibernate.internal.util.ReflectHelper.findSetterMethod; import static org.hibernate.internal.util.ReflectHelper.getterMethodOrNull; import static org.hibernate.property.access.internal.AccessStrategyHelper.fieldOrNull; @@ -43,10 +44,12 @@ public PropertyAccessEnhancedImpl( PropertyAccessStrategy strategy, Class containerJavaType, String propertyName, - @Nullable AccessType getterAccessType) { + @Nullable AccessType classAccessType) { this.strategy = strategy; - final AccessType propertyAccessType = resolveAccessType( getterAccessType, containerJavaType, propertyName ); + final AccessType propertyAccessType = classAccessType == null ? + AccessStrategyHelper.getAccessType( containerJavaType, propertyName ) : + classAccessType; switch ( propertyAccessType ) { case FIELD: { @@ -67,10 +70,8 @@ public PropertyAccessEnhancedImpl( "Could not locate getter for property named [" + containerJavaType.getName() + "#" + propertyName + "]" ); } - final Method setterMethod = findSetterMethod( containerJavaType, propertyName, getterMethod.getReturnType() ); - - this.getter = new GetterMethodImpl( containerJavaType, propertyName, getterMethod ); - this.setter = new EnhancedSetterMethodImpl( containerJavaType, propertyName, setterMethod ); + this.getter = propertyGetter( classAccessType, containerJavaType, propertyName, getterMethod ); + this.setter = propertySetter( classAccessType, containerJavaType, propertyName, getterMethod.getReturnType() ); break; } default: { @@ -81,12 +82,31 @@ public PropertyAccessEnhancedImpl( } } - private static AccessType resolveAccessType(@Nullable AccessType getterAccessType, Class containerJavaType, String propertyName) { - if ( getterAccessType != null ) { - // this should indicate FIELD access - return getterAccessType; + private static Getter propertyGetter(@Nullable AccessType classAccessType, Class containerJavaType, String propertyName, Method getterMethod) { + if ( classAccessType != null ) { + final AccessType explicitAccessType = AccessStrategyHelper.getAccessType( containerJavaType, propertyName ); + if ( explicitAccessType == AccessType.FIELD ) { + // We need to default to FIELD unless we have an explicit AccessType to avoid unnecessary initializations + final Field field = findField( containerJavaType, propertyName ); + return new EnhancedGetterFieldImpl( containerJavaType, propertyName, field, getterMethod ); + } + } + // when classAccessType is null know PROPERTY is the explicit access type + return new GetterMethodImpl( containerJavaType, propertyName, getterMethod ); + } + + private static Setter propertySetter(@Nullable AccessType classAccessType, Class containerJavaType, String propertyName, Class fieldType) { + if ( classAccessType != null ) { + final AccessType explicitAccessType = AccessStrategyHelper.getAccessType( containerJavaType, propertyName ); + if ( explicitAccessType == AccessType.FIELD ) { + // We need to default to FIELD unless we have an explicit AccessType to avoid unnecessary initializations + final Field field = findField( containerJavaType, propertyName ); + return new EnhancedSetterImpl( containerJavaType, propertyName, field ); + } } - return AccessStrategyHelper.getAccessType( containerJavaType, propertyName ); + // when classAccessType is null know PROPERTY is the explicit access type + final Method setterMethod = findSetterMethod( containerJavaType, propertyName, fieldType ); + return new EnhancedSetterMethodImpl( containerJavaType, propertyName, setterMethod ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/property/access/internal/PropertyAccessStrategyEnhancedImpl.java b/hibernate-core/src/main/java/org/hibernate/property/access/internal/PropertyAccessStrategyEnhancedImpl.java index 33c5181552dc..9d1650521459 100644 --- a/hibernate-core/src/main/java/org/hibernate/property/access/internal/PropertyAccessStrategyEnhancedImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/property/access/internal/PropertyAccessStrategyEnhancedImpl.java @@ -21,23 +21,32 @@ */ public class PropertyAccessStrategyEnhancedImpl implements PropertyAccessStrategy { public static PropertyAccessStrategyEnhancedImpl with(AccessType getterAccessType) { - if ( getterAccessType == AccessType.FIELD ) { - return FIELD; + if ( getterAccessType == null ) { + return STANDARD; + } + + switch ( getterAccessType ) { + case FIELD: + return FIELD; + case PROPERTY: + return PROPERTY; + default: + return STANDARD; } - return STANDARD; } - private final @Nullable AccessType getterAccessType; + private final @Nullable AccessType classAccessType; public static PropertyAccessStrategyEnhancedImpl STANDARD = new PropertyAccessStrategyEnhancedImpl( null ); public static PropertyAccessStrategyEnhancedImpl FIELD = new PropertyAccessStrategyEnhancedImpl( AccessType.FIELD ); + public static PropertyAccessStrategyEnhancedImpl PROPERTY = new PropertyAccessStrategyEnhancedImpl( AccessType.PROPERTY ); - public PropertyAccessStrategyEnhancedImpl(@Nullable AccessType getterAccessType) { - this.getterAccessType = getterAccessType; + public PropertyAccessStrategyEnhancedImpl(@Nullable AccessType classAccessType) { + this.classAccessType = classAccessType; } @Override public PropertyAccess buildPropertyAccess(Class containerJavaType, final String propertyName, boolean setterRequired) { - return new PropertyAccessEnhancedImpl( this, containerJavaType, propertyName, getterAccessType ); + return new PropertyAccessEnhancedImpl( this, containerJavaType, propertyName, classAccessType ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/property/access/internal/PropertyAccessStrategyResolverStandardImpl.java b/hibernate-core/src/main/java/org/hibernate/property/access/internal/PropertyAccessStrategyResolverStandardImpl.java index 2ddb660a0c0b..4f38030ee05a 100644 --- a/hibernate-core/src/main/java/org/hibernate/property/access/internal/PropertyAccessStrategyResolverStandardImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/property/access/internal/PropertyAccessStrategyResolverStandardImpl.java @@ -8,6 +8,7 @@ import org.hibernate.HibernateException; import org.hibernate.boot.registry.selector.spi.StrategySelector; +import org.hibernate.boot.spi.AccessType; import org.hibernate.internal.util.StringHelper; import org.hibernate.metamodel.RepresentationMode; import org.hibernate.property.access.spi.BuiltInPropertyAccessStrategies; @@ -40,9 +41,12 @@ public PropertyAccessStrategy resolvePropertyAccessStrategy( || BuiltInPropertyAccessStrategies.MIXED.getExternalName().equals( explicitAccessStrategyName ) ) { //type-cache-pollution agent: it's crucial to use the ManagedTypeHelper rather than attempting a direct cast if ( isManagedType( containerClass ) ) { - if ( BuiltInPropertyAccessStrategies.FIELD.getExternalName().equals( explicitAccessStrategyName ) ) { + if ( AccessType.FIELD.getType().equals( explicitAccessStrategyName ) ) { return PropertyAccessStrategyEnhancedImpl.FIELD; } + else if ( AccessType.PROPERTY.getType().equals( explicitAccessStrategyName ) ) { + return PropertyAccessStrategyEnhancedImpl.PROPERTY; + } return PropertyAccessStrategyEnhancedImpl.STANDARD; } } diff --git a/hibernate-core/src/main/java/org/hibernate/property/access/spi/EnhancedGetterFieldImpl.java b/hibernate-core/src/main/java/org/hibernate/property/access/spi/EnhancedGetterFieldImpl.java new file mode 100644 index 000000000000..8935806d3c7f --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/property/access/spi/EnhancedGetterFieldImpl.java @@ -0,0 +1,43 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.property.access.spi; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.hibernate.Internal; + +import java.lang.reflect.Field; +import java.lang.reflect.Member; +import java.lang.reflect.Method; + +import static org.hibernate.internal.util.NullnessUtil.castNonNull; + +/** + * A specialized Getter implementation for handling getting values from + * a bytecode-enhanced Class. The reason we need specialized handling + * is to produce the correct {@link java.lang.reflect.Member} while + * using the {@link Field} to access values and ensure correct functionality. + * + * @author Steve Ebersole + * @author Luis Barreiro + */ +@Internal +public class EnhancedGetterFieldImpl extends GetterFieldImpl { + public EnhancedGetterFieldImpl(Class containerClass, String propertyName, Field field, Method getterMethod) { + super( containerClass, propertyName, field, getterMethod ); + assert getterMethod != null; + } + + @Override + public @NonNull Method getMethod() { + return castNonNull( super.getMethod() ); + } + + @Override + public Member getMember() { + return getMethod(); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/property/access/spi/GetterFieldImpl.java b/hibernate-core/src/main/java/org/hibernate/property/access/spi/GetterFieldImpl.java index 74647c50717e..f29095f2ba49 100644 --- a/hibernate-core/src/main/java/org/hibernate/property/access/spi/GetterFieldImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/property/access/spi/GetterFieldImpl.java @@ -35,11 +35,14 @@ public class GetterFieldImpl implements Getter { private final @Nullable Method getterMethod; public GetterFieldImpl(Class containerClass, String propertyName, Field field) { + this ( containerClass, propertyName, field, ReflectHelper.findGetterMethodForFieldAccess( field, propertyName ) ); + } + + GetterFieldImpl(Class containerClass, String propertyName, Field field, Method getterMethod) { this.containerClass = containerClass; this.propertyName = propertyName; this.field = field; - - this.getterMethod = ReflectHelper.findGetterMethodForFieldAccess( field, propertyName ); + this.getterMethod = getterMethod; } @Override @@ -78,9 +81,13 @@ public Type getReturnType() { return field.getGenericType(); } + public Field getField() { + return field; + } + @Override public Member getMember() { - return field; + return getField(); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/proxy/pojo/bytebuddy/ByteBuddyProxyHelper.java b/hibernate-core/src/main/java/org/hibernate/proxy/pojo/bytebuddy/ByteBuddyProxyHelper.java index 06a3e48358ad..9916891114b1 100644 --- a/hibernate-core/src/main/java/org/hibernate/proxy/pojo/bytebuddy/ByteBuddyProxyHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/proxy/pojo/bytebuddy/ByteBuddyProxyHelper.java @@ -15,6 +15,7 @@ import java.util.HashSet; import java.util.Locale; import java.util.Set; +import java.util.function.BiFunction; import java.util.function.Function; import org.hibernate.HibernateException; @@ -27,7 +28,6 @@ import net.bytebuddy.ByteBuddy; import net.bytebuddy.NamingStrategy; -import net.bytebuddy.TypeCache; import net.bytebuddy.description.modifier.Visibility; import net.bytebuddy.description.type.TypeDefinition; import net.bytebuddy.description.type.TypeDescription; @@ -59,7 +59,8 @@ public Class buildProxy( } Collections.addAll( key, interfaces ); - return byteBuddyState.loadProxy( persistentClass, new TypeCache.SimpleKey( key ), + final String proxyClassName = persistentClass.getTypeName() + "$" + PROXY_NAMING_SUFFIX; + return byteBuddyState.loadProxy( persistentClass, proxyClassName, proxyBuilder( TypeDescription.ForLoadedType.of( persistentClass ), new TypeList.Generic.ForLoadedTypes( interfaces ) ) ); } @@ -68,7 +69,7 @@ public Class buildProxy( */ @Deprecated public DynamicType.Unloaded buildUnloadedProxy(final Class persistentClass, final Class[] interfaces) { - return byteBuddyState.make( proxyBuilder( TypeDescription.ForLoadedType.of( persistentClass ), + return byteBuddyState.make( proxyBuilderLegacy( TypeDescription.ForLoadedType.of( persistentClass ), new TypeList.Generic.ForLoadedTypes( interfaces ) ) ); } @@ -77,15 +78,24 @@ public DynamicType.Unloaded buildUnloadedProxy(final Class persistentClass */ public DynamicType.Unloaded buildUnloadedProxy(TypePool typePool, TypeDefinition persistentClass, Collection interfaces) { - return byteBuddyState.make( typePool, proxyBuilder( persistentClass, interfaces ) ); + return byteBuddyState.make( typePool, proxyBuilderLegacy( persistentClass, interfaces ) ); } - private Function> proxyBuilder(TypeDefinition persistentClass, + private Function> proxyBuilderLegacy(TypeDefinition persistentClass, + Collection interfaces) { + final BiFunction> proxyBuilder = + proxyBuilder( persistentClass, interfaces ); + final NamingStrategy.Suffixing namingStrategy = + new NamingStrategy.Suffixing( PROXY_NAMING_SUFFIX, new NamingStrategy.Suffixing.BaseNameResolver.ForFixedValue( persistentClass.getTypeName() ) ); + return byteBuddy -> proxyBuilder.apply( byteBuddy, namingStrategy ); + } + + private BiFunction> proxyBuilder(TypeDefinition persistentClass, Collection interfaces) { ByteBuddyState.ProxyDefinitionHelpers helpers = byteBuddyState.getProxyDefinitionHelpers(); - return byteBuddy -> helpers.appendIgnoreAlsoAtEnd( byteBuddy + return (byteBuddy, namingStrategy) -> helpers.appendIgnoreAlsoAtEnd( byteBuddy .ignore( helpers.getGroovyGetMetaClassFilter() ) - .with( new NamingStrategy.SuffixingRandom( PROXY_NAMING_SUFFIX, new NamingStrategy.Suffixing.BaseNameResolver.ForFixedValue( persistentClass.getTypeName() ) ) ) + .with( namingStrategy ) .subclass( interfaces.size() == 1 ? persistentClass : OBJECT, ConstructorStrategy.Default.IMITATE_SUPER_CLASS_OPENING ) .implement( interfaces ) .method( helpers.getVirtualNotFinalizerFilter() ) diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/internal/NamedCriteriaQueryMementoImpl.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/internal/NamedCriteriaQueryMementoImpl.java index 1c0ef4f2f623..049c4dab3a1d 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/internal/NamedCriteriaQueryMementoImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/internal/NamedCriteriaQueryMementoImpl.java @@ -22,6 +22,8 @@ import org.hibernate.query.sqm.spi.NamedSqmQueryMemento; import org.hibernate.query.sqm.tree.SqmStatement; +import org.checkerframework.checker.nullness.qual.Nullable; + public class NamedCriteriaQueryMementoImpl extends AbstractNamedQueryMemento implements NamedSqmQueryMemento, Serializable { private final SqmStatement sqmStatement; @@ -33,6 +35,7 @@ public class NamedCriteriaQueryMementoImpl extends AbstractNamedQueryMemento imp public NamedCriteriaQueryMementoImpl( String name, + @Nullable Class resultType, SqmStatement sqmStatement, Integer firstResult, Integer maxResults, @@ -47,7 +50,7 @@ public NamedCriteriaQueryMementoImpl( String comment, Map parameterTypes, Map hints) { - super( name, cacheable, cacheRegion, cacheMode, flushMode, readOnly, timeout, fetchSize, comment, hints ); + super( name, resultType, cacheable, cacheRegion, cacheMode, flushMode, readOnly, timeout, fetchSize, comment, hints ); this.sqmStatement = sqmStatement; this.firstResult = firstResult; this.maxResults = maxResults; @@ -114,6 +117,7 @@ public Map getParameterTypes() { public NamedSqmQueryMemento makeCopy(String name) { return new NamedCriteriaQueryMementoImpl( name, + getResultType(), sqmStatement, firstResult, maxResults, diff --git a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleBasicEntityIdentifierMapping.java b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleBasicEntityIdentifierMapping.java index d1f65dc0b858..f2ee788ff6d7 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleBasicEntityIdentifierMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleBasicEntityIdentifierMapping.java @@ -9,6 +9,7 @@ import org.hibernate.Incubating; import org.hibernate.engine.spi.IdentifierValue; import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.event.spi.MergeContext; import org.hibernate.metamodel.mapping.BasicEntityIdentifierMapping; import org.hibernate.metamodel.mapping.JdbcMapping; import org.hibernate.metamodel.mapping.ManagedMappingType; @@ -51,6 +52,11 @@ public Object getIdentifier(Object entity) { return delegate.getIdentifier( entity ); } + @Override + public Object getIdentifier(Object entity, MergeContext mergeContext) { + return delegate.getIdentifier( entity, mergeContext ); + } + @Override public void setIdentifier(Object entity, Object id, SharedSessionContractImplementor session) { delegate.setIdentifier( entity, id, session ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleEmbeddableValuedModelPart.java b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleEmbeddableValuedModelPart.java index 932baaa7175f..454ee5749203 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleEmbeddableValuedModelPart.java +++ b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleEmbeddableValuedModelPart.java @@ -63,6 +63,9 @@ import org.hibernate.type.descriptor.java.JavaType; import jakarta.persistence.metamodel.Attribute; +import org.checkerframework.checker.nullness.qual.Nullable; + +import static java.util.Objects.requireNonNullElse; /** * @author Christian Beikov @@ -346,13 +349,13 @@ public boolean isSimpleJoinPredicate(Predicate predicate) { public TableGroupJoin createTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType requestedJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType requestedJoinType, boolean fetched, boolean addsPredicate, SqlAstCreationState creationState) { - final SqlAstJoinType joinType = requestedJoinType == null ? SqlAstJoinType.INNER : requestedJoinType; + final SqlAstJoinType joinType = requireNonNullElse( requestedJoinType, SqlAstJoinType.INNER ); final TableGroup tableGroup = createRootTableGroupJoin( navigablePath, lhs, @@ -371,18 +374,13 @@ public TableGroupJoin createTableGroupJoin( public TableGroup createRootTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType sqlAstJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType sqlAstJoinType, boolean fetched, - Consumer predicateConsumer, + @Nullable Consumer predicateConsumer, SqlAstCreationState creationState) { - return new StandardVirtualTableGroup( - navigablePath, - this, - lhs, - fetched - ); + return new StandardVirtualTableGroup( navigablePath, this, lhs, fetched ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleEmbeddedEntityIdentifierMapping.java b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleEmbeddedEntityIdentifierMapping.java index 64be6083d4d5..abd8f49da889 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleEmbeddedEntityIdentifierMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleEmbeddedEntityIdentifierMapping.java @@ -12,6 +12,7 @@ import org.hibernate.Incubating; import org.hibernate.engine.spi.IdentifierValue; import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.event.spi.MergeContext; import org.hibernate.metamodel.mapping.CompositeIdentifierMapping; import org.hibernate.metamodel.mapping.EmbeddableMappingType; import org.hibernate.metamodel.mapping.internal.SingleAttributeIdentifierMapping; @@ -70,6 +71,11 @@ public Object getIdentifier(Object entity) { return delegate.getIdentifier( entity ); } + @Override + public Object getIdentifier(Object entity, MergeContext mergeContext) { + return delegate.getIdentifier( entity, mergeContext ); + } + @Override public void setIdentifier(Object entity, Object id, SharedSessionContractImplementor session) { delegate.setIdentifier( entity, id, session ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleEntityValuedModelPart.java b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleEntityValuedModelPart.java index dc73c84a403a..c973837b40a1 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleEntityValuedModelPart.java +++ b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleEntityValuedModelPart.java @@ -70,6 +70,8 @@ import org.hibernate.sql.results.graph.DomainResultCreationState; import org.hibernate.type.descriptor.java.JavaType; +import org.checkerframework.checker.nullness.qual.Nullable; + import static java.util.Objects.requireNonNullElse; import static org.hibernate.internal.util.collections.CollectionHelper.arrayList; @@ -245,14 +247,13 @@ public JavaType getMappedJavaType() { public TableGroupJoin createTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType requestedJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType requestedJoinType, boolean fetched, boolean addsPredicate, SqlAstCreationState creationState) { final SqlAstJoinType joinType = requireNonNullElse( requestedJoinType, SqlAstJoinType.INNER ); - final LazyTableGroup lazyTableGroup = createRootTableGroupJoin( navigablePath, lhs, @@ -441,11 +442,11 @@ public TableGroup createTableGroupInternal( public LazyTableGroup createRootTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType sqlAstJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType sqlAstJoinType, boolean fetched, - Consumer predicateConsumer, + @Nullable Consumer predicateConsumer, SqlAstCreationState creationState) { final SqlAliasBase sqlAliasBase = SqlAliasBase.from( explicitSqlAliasBase, diff --git a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleNonAggregatedEntityIdentifierMapping.java b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleNonAggregatedEntityIdentifierMapping.java index beee4fecd2a7..c2d7db73fb9d 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleNonAggregatedEntityIdentifierMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleNonAggregatedEntityIdentifierMapping.java @@ -14,6 +14,7 @@ import org.hibernate.engine.FetchTiming; import org.hibernate.engine.spi.IdentifierValue; import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.event.spi.MergeContext; import org.hibernate.metamodel.mapping.EmbeddableMappingType; import org.hibernate.metamodel.mapping.NonAggregatedIdentifierMapping; import org.hibernate.metamodel.mapping.internal.IdClassEmbeddable; @@ -79,6 +80,12 @@ public Object getIdentifier(Object entity) { return delegate.getIdentifier( entity ); } + + @Override + public Object getIdentifier(Object entity, MergeContext mergeContext) { + return delegate.getIdentifier( entity, mergeContext ); + } + @Override public void setIdentifier(Object entity, Object id, SharedSessionContractImplementor session) { delegate.setIdentifier( entity, id, session ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleTableGroupProducer.java b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleTableGroupProducer.java index f000e25a8a3c..ed67edec52f5 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleTableGroupProducer.java +++ b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleTableGroupProducer.java @@ -6,6 +6,7 @@ */ package org.hibernate.query.derived; +import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -67,6 +68,8 @@ public class AnonymousTupleTableGroupProducer implements TableGroupProducer, Map private final JavaType javaTypeDescriptor; private final Map modelParts; private final Set compatibleTableExpressions; + private final int jdbcTypeCount; + private final List jdbcMappings = new ArrayList<>(); public AnonymousTupleTableGroupProducer( AnonymousTupleType tupleType, @@ -114,10 +117,14 @@ public AnonymousTupleTableGroupProducer( ); } modelParts.put( partName, modelPart ); + for ( int j = 0; j < modelPart.getJdbcTypeCount(); j++ ) { + jdbcMappings.add( modelPart.getJdbcMapping( j ) ); + } selectionIndex += modelPart.getJdbcTypeCount(); } this.modelParts = modelParts; this.compatibleTableExpressions = compatibleTableExpressions; + jdbcTypeCount = selectionIndex; } private ModelPart getModelPart(TableGroup tableGroup) { @@ -368,11 +375,16 @@ public int forEachDisassembledJdbcValue( @Override public JdbcMapping getJdbcMapping(int index) { - throw new UnsupportedOperationException( "Not yet implemented" ); + return jdbcMappings.get( index ); } @Override public int forEachJdbcType(int offset, IndexedConsumer action) { throw new UnsupportedOperationException( "Not yet implemented" ); } + + @Override + public int getJdbcTypeCount() { + return jdbcTypeCount; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleType.java b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleType.java index be448dc0b16e..6d1a05e5e4b4 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleType.java +++ b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleType.java @@ -11,6 +11,7 @@ import java.util.List; import java.util.Map; +import jakarta.persistence.metamodel.Bindable; import org.hibernate.Incubating; import org.hibernate.internal.util.collections.CollectionHelper; import org.hibernate.metamodel.UnsupportedMappingException; @@ -31,6 +32,7 @@ import org.hibernate.query.sqm.tree.select.SqmSubQuery; import org.hibernate.sql.ast.spi.FromClauseAccess; import org.hibernate.sql.ast.spi.SqlSelection; +import org.hibernate.type.BasicType; import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.descriptor.java.ObjectArrayJavaType; @@ -177,20 +179,21 @@ public SqmPathSource findSubPathSource(String name) { final SqmSelectableNode component = components[index]; if ( component instanceof SqmPath ) { final SqmPath sqmPath = (SqmPath) component; - if ( sqmPath.getNodeType() instanceof SingularPersistentAttribute ) { + final Bindable model = sqmPath.getModel(); + if ( model instanceof SingularPersistentAttribute ) { //noinspection unchecked,rawtypes return new AnonymousTupleSqmAssociationPathSource( name, sqmPath, - ( (SingularPersistentAttribute) sqmPath.getNodeType() ).getType() + ( (SingularPersistentAttribute) model ).getType() ); } - else if ( sqmPath.getNodeType() instanceof PluralPersistentAttribute ) { + else if ( model instanceof PluralPersistentAttribute ) { //noinspection unchecked,rawtypes return new AnonymousTupleSqmAssociationPathSource( name, sqmPath, - ( (PluralPersistentAttribute) sqmPath.getNodeType() ).getElementType() + ( (PluralPersistentAttribute) model ).getElementType() ); } else if ( sqmPath.getNodeType() instanceof EntityDomainType ) { diff --git a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/NamedHqlQueryMementoImpl.java b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/NamedHqlQueryMementoImpl.java index 2cc32c0eb3ff..272c4a7cd6bb 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/NamedHqlQueryMementoImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/NamedHqlQueryMementoImpl.java @@ -22,6 +22,8 @@ import org.hibernate.query.sqm.spi.NamedSqmQueryMemento; import org.hibernate.query.sqm.tree.SqmStatement; +import org.checkerframework.checker.nullness.qual.Nullable; + /** * Definition of a named query, defined in the mapping metadata. * Additionally, as of JPA 2.1, named query definition can also come @@ -41,6 +43,7 @@ public class NamedHqlQueryMementoImpl extends AbstractNamedQueryMemento implemen public NamedHqlQueryMementoImpl( String name, + @Nullable Class resultType, String hqlString, Integer firstResult, Integer maxResults, @@ -57,6 +60,7 @@ public NamedHqlQueryMementoImpl( Map hints) { super( name, + resultType, cacheable, cacheRegion, cacheMode, @@ -103,6 +107,7 @@ public Map getParameterTypes() { public NamedSqmQueryMemento makeCopy(String name) { return new NamedHqlQueryMementoImpl( name, + getResultType(), hqlString, firstResult, maxResults, diff --git a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/QualifiedJoinPathConsumer.java b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/QualifiedJoinPathConsumer.java index 40cf38c349f1..e6c872e13501 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/QualifiedJoinPathConsumer.java +++ b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/QualifiedJoinPathConsumer.java @@ -197,7 +197,7 @@ private AttributeJoinDelegate resolveAlias(String identifier, boolean isTerminal if ( allowReuse ) { if ( !isTerminal ) { for ( SqmJoin sqmJoin : lhs.getSqmJoins() ) { - if ( sqmJoin.getAlias() == null && sqmJoin.getReferencedPathSource() == subPathSource ) { + if ( sqmJoin.getAlias() == null && sqmJoin.getModel() == subPathSource ) { return sqmJoin; } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java index f6ba427a3f69..17b152e5642f 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java @@ -2886,11 +2886,14 @@ else if ( inListContext instanceof HqlParser.ArrayInListContext ) { final SqmExpression arrayExpr = (SqmExpression) arrayInListContext.expression().accept( this ); final SqmExpressible arrayExpressible = arrayExpr.getExpressible(); - if ( arrayExpressible != null && !( arrayExpressible.getSqmType() instanceof BasicPluralType) ) { - throw new SemanticException( - "Right operand for in-array predicate must be a basic plural type expression, but found: " + arrayExpressible.getSqmType(), - query - ); + if ( arrayExpressible != null ) { + if ( !(arrayExpressible.getSqmType() instanceof BasicPluralType) ) { + throw new SemanticException( + "Right operand for in-array predicate must be a basic plural type expression, but found: " + arrayExpressible.getSqmType(), + query + ); + } + testExpression.applyInferableType( ( (BasicPluralType) arrayExpressible.getSqmType() ).getElementType() ); } final SelfRenderingSqmFunction contains = getFunctionDescriptor( "array_contains" ).generateSqmExpression( asList( arrayExpr, testExpression ), @@ -2971,9 +2974,15 @@ public SqmPath visitEntityIdReference(HqlParser.EntityIdReferenceContext ctx) final SqmPath sqmPath = consumeDomainPath( ctx.path() ); final DomainType sqmPathType = sqmPath.getReferencedPathSource().getSqmPathType(); - if ( sqmPathType instanceof IdentifiableDomainType ) { - final SqmPathSource identifierDescriptor = ( (IdentifiableDomainType) sqmPathType ).getIdentifierDescriptor(); + final IdentifiableDomainType identifiableType = (IdentifiableDomainType) sqmPathType; + final SqmPathSource identifierDescriptor = identifiableType.getIdentifierDescriptor(); + if ( identifierDescriptor == null ) { + // mainly for benefit of Hibernate Processor + throw new FunctionArgumentException( "Argument '" + sqmPath.getNavigablePath() + + "' of 'id()' is a '" + identifiableType.getTypeName() + + "' and does not have a well-defined '@Id' attribute" ); + } return sqmPath.get( identifierDescriptor.getPathName() ); } else if ( sqmPath instanceof SqmAnyValuedSimplePath ) { @@ -2981,7 +2990,7 @@ else if ( sqmPath instanceof SqmAnyValuedSimplePath ) { } else { throw new FunctionArgumentException( "Argument '" + sqmPath.getNavigablePath() - + "' of 'id()' function does not resolve to an entity type" ); + + "' of 'id()' does not resolve to an entity type" ); } } @@ -2994,26 +3003,22 @@ public SqmExpression visitEntityVersionExpression(HqlParser.EntityVersionExpr public SqmPath visitEntityVersionReference(HqlParser.EntityVersionReferenceContext ctx) { final SqmPath sqmPath = consumeDomainPath( ctx.path() ); final DomainType sqmPathType = sqmPath.getReferencedPathSource().getSqmPathType(); - if ( sqmPathType instanceof IdentifiableDomainType ) { - @SuppressWarnings("unchecked") - final IdentifiableDomainType identifiableType = (IdentifiableDomainType) sqmPathType; - final SingularPersistentAttribute versionAttribute = identifiableType.findVersionAttribute(); - if ( versionAttribute == null ) { - throw new FunctionArgumentException( - String.format( - "Argument '%s' of 'version()' function resolved to entity type '%s' which does not have a '@Version' attribute", - sqmPath.getNavigablePath(), - identifiableType.getTypeName() - ) - ); + final IdentifiableDomainType identifiableType = (IdentifiableDomainType) sqmPathType; + if ( !identifiableType.hasVersionAttribute() ) { + throw new FunctionArgumentException( "Argument '" + sqmPath.getNavigablePath() + + "' of 'version()' is a '" + identifiableType.getTypeName() + + "' and does not have a '@Version' attribute" ); } - + @SuppressWarnings("unchecked") + final SingularPersistentAttribute versionAttribute = + (SingularPersistentAttribute) identifiableType.findVersionAttribute(); return sqmPath.get( versionAttribute ); } - - throw new FunctionArgumentException( "Argument '" + sqmPath.getNavigablePath() - + "' of 'version()' function does not resolve to an entity type" ); + else { + throw new FunctionArgumentException( "Argument '" + sqmPath.getNavigablePath() + + "' of 'version()' does not resolve to an entity type" ); + } } @Override @@ -3032,35 +3037,29 @@ public SqmPath visitEntityNaturalIdReference(HqlParser.EntityNaturalIdReferen if ( sqmPathType instanceof IdentifiableDomainType ) { @SuppressWarnings("unchecked") - final IdentifiableDomainType identifiableType = (IdentifiableDomainType) sqmPathType; + final IdentifiableDomainType identifiableType = (IdentifiableDomainType) sqmPathType; final List> attributes = identifiableType.findNaturalIdAttributes(); if ( attributes == null ) { - throw new FunctionArgumentException( - String.format( - "Argument '%s' of 'naturalid()' function resolved to entity type '%s' which does not have a natural id", - sqmPath.getNavigablePath(), - identifiableType.getTypeName() - ) + throw new FunctionArgumentException( "Argument '" + sqmPath.getNavigablePath() + + "' of 'naturalid()' is a '" + identifiableType.getTypeName() + + "' and does not have a natural id" ); } else if ( attributes.size() >1 ) { - throw new FunctionArgumentException( - String.format( - "Argument '%s' of 'naturalid()' function resolved to entity type '%s' which has a composite natural id", - sqmPath.getNavigablePath(), - identifiableType.getTypeName() - ) + throw new FunctionArgumentException( "Argument '" + sqmPath.getNavigablePath() + + "' of 'naturalid()' is a '" + identifiableType.getTypeName() + + "' and has a composite natural id" ); } @SuppressWarnings("unchecked") - SingularAttribute naturalIdAttribute + final SingularAttribute naturalIdAttribute = (SingularAttribute) attributes.get(0); return sqmPath.get( naturalIdAttribute ); } throw new FunctionArgumentException( "Argument '" + sqmPath.getNavigablePath() - + "' of 'naturalid()' function does not resolve to an entity type" ); + + "' of 'naturalid()' does not resolve to an entity type" ); } // // @Override @@ -5263,9 +5262,11 @@ public SqmSubQuery visitSubquery(HqlParser.SubqueryContext ctx) { final List> selections = subQuery.getQuerySpec().getSelectClause().getSelections(); if ( selections.size() == 1 ) { - subQuery.applyInferableType( selections.get( 0 ).getExpressible().getSqmType() ); + final SqmExpressible expressible = selections.get( 0 ).getExpressible(); + if ( expressible != null ) { + subQuery.applyInferableType( expressible.getSqmType() ); + } } - return subQuery; } finally { diff --git a/hibernate-core/src/main/java/org/hibernate/query/internal/BindingTypeHelper.java b/hibernate-core/src/main/java/org/hibernate/query/internal/BindingTypeHelper.java index 2fe6aa697876..12d34e14e184 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/internal/BindingTypeHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/query/internal/BindingTypeHelper.java @@ -41,21 +41,27 @@ public BindableType resolveTemporalPrecision( BindableType declaredParameterType, SessionFactoryImplementor sessionFactory) { if ( precision != null ) { - final SqmExpressible sqmExpressible = declaredParameterType.resolveExpressible( sessionFactory ); - if ( !( JavaTypeHelper.isTemporal( sqmExpressible.getExpressibleJavaType() ) ) ) { - throw new UnsupportedOperationException( - "Cannot treat non-temporal parameter type with temporal precision" - ); + final TemporalJavaType temporalJtd; + if ( declaredParameterType != null ) { + final SqmExpressible sqmExpressible = declaredParameterType.resolveExpressible( sessionFactory ); + if ( !( JavaTypeHelper.isTemporal( sqmExpressible.getExpressibleJavaType() ) ) ) { + throw new UnsupportedOperationException( + "Cannot treat non-temporal parameter type with temporal precision" + ); + } + temporalJtd = (TemporalJavaType) sqmExpressible.getExpressibleJavaType(); + } + else { + temporalJtd = null; } - final TemporalJavaType temporalJtd = (TemporalJavaType) sqmExpressible.getExpressibleJavaType(); - if ( temporalJtd.getPrecision() != precision ) { + if ( temporalJtd == null || temporalJtd.getPrecision() != precision ) { final TypeConfiguration typeConfiguration = sessionFactory.getTypeConfiguration(); final TemporalJavaType temporalTypeForPrecision; // Special case java.util.Date, because TemporalJavaType#resolveTypeForPrecision doesn't support widening, // since the main purpose of that method is to determine the final java type based on the reflective type // + the explicit @Temporal(TemporalType...) configuration - if ( java.util.Date.class.isAssignableFrom( temporalJtd.getJavaTypeClass() ) ) { + if ( temporalJtd == null || java.util.Date.class.isAssignableFrom( temporalJtd.getJavaTypeClass() ) ) { //noinspection unchecked temporalTypeForPrecision = (TemporalJavaType) typeConfiguration.getJavaTypeRegistry().getDescriptor( TemporalJavaType.resolveJavaTypeClass( precision ) diff --git a/hibernate-core/src/main/java/org/hibernate/query/internal/QueryParameterBindingImpl.java b/hibernate-core/src/main/java/org/hibernate/query/internal/QueryParameterBindingImpl.java index e5ea76cb30c8..5f102201ae5b 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/internal/QueryParameterBindingImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/internal/QueryParameterBindingImpl.java @@ -20,6 +20,7 @@ import org.hibernate.query.spi.QueryParameterBinding; import org.hibernate.query.spi.QueryParameterBindingValidator; import org.hibernate.query.sqm.SqmExpressible; +import org.hibernate.query.sqm.tree.expression.NullSqmExpressible; import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.descriptor.java.JavaTypeHelper; import org.hibernate.type.spi.TypeConfiguration; @@ -113,10 +114,10 @@ public void setBindValue(T value, boolean resolveJdbcTypeIfNecessary) { final Object coerced; if ( ! sessionFactory.getSessionFactoryOptions().getJpaCompliance().isLoadByIdComplianceEnabled() ) { try { - if ( bindType != null ) { + if ( canValueBeCoerced( bindType) ) { coerced = coerce( value, bindType ); } - else if ( queryParameter.getHibernateType() != null ) { + else if ( canValueBeCoerced( queryParameter.getHibernateType() ) ) { coerced = coerce( value, queryParameter.getHibernateType() ); } else { @@ -190,7 +191,7 @@ private boolean isRegisteredAsBasicType(Class valueClass) { private void bindValue(Object value) { isBound = true; bindValue = value; - if ( bindType == null && value != null ) { + if ( canBindValueBeSet( value, bindType ) ) { bindType = sessionFactory.getMappingMetamodel().resolveParameterBindType( value ); } } @@ -206,10 +207,10 @@ public void setBindValue(T value, BindableType clarifiedType) { } final Object coerced; - if ( bindType != null ) { + if ( canValueBeCoerced( bindType ) ) { coerced = coerce( value, bindType ); } - else if ( queryParameter.getHibernateType() != null ) { + else if ( canValueBeCoerced( queryParameter.getHibernateType() ) ) { coerced = coerce( value, queryParameter.getHibernateType() ); } else { @@ -233,7 +234,7 @@ public void setBindValue(T value, TemporalType temporalTypePrecision) { final Object coerced; if ( ! sessionFactory.getSessionFactoryOptions().getJpaCompliance().isLoadByIdComplianceEnabled() ) { - if ( bindType != null ) { + if (canValueBeCoerced( bindType ) ) { try { coerced = coerce( value, bindType ); } @@ -249,7 +250,7 @@ public void setBindValue(T value, TemporalType temporalTypePrecision) { ); } } - else if ( queryParameter.getHibernateType() != null ) { + else if ( canValueBeCoerced( queryParameter.getHibernateType() ) ) { coerced = coerce( value, queryParameter.getHibernateType() ); } else { @@ -299,10 +300,9 @@ public void setBindValues(Collection values) { value = iterator.next(); } - if ( bindType == null && value != null ) { - this.bindType = sessionFactory.getMappingMetamodel().resolveParameterBindType( value ); + if ( canBindValueBeSet( value, bindType ) ) { + bindType = sessionFactory.getMappingMetamodel().resolveParameterBindType( value ); } - } @Override @@ -378,4 +378,12 @@ private void validate(Object value, TemporalType clarifiedTemporalType) { public TypeConfiguration getTypeConfiguration() { return sessionFactory.getTypeConfiguration(); } + + private static boolean canValueBeCoerced(BindableType bindType) { + return bindType != null && !( bindType instanceof NullSqmExpressible ); + } + + private static boolean canBindValueBeSet(Object value, BindableType bindType) { + return value != null && ( bindType == null || bindType instanceof NullSqmExpressible ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/named/AbstractNamedQueryMemento.java b/hibernate-core/src/main/java/org/hibernate/query/named/AbstractNamedQueryMemento.java index 960cdeb2d68f..14e1bc408fa2 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/named/AbstractNamedQueryMemento.java +++ b/hibernate-core/src/main/java/org/hibernate/query/named/AbstractNamedQueryMemento.java @@ -14,12 +14,15 @@ import org.hibernate.CacheMode; import org.hibernate.FlushMode; +import org.checkerframework.checker.nullness.qual.Nullable; + /** * @author Steve Ebersole * @author Gavin King */ public abstract class AbstractNamedQueryMemento implements NamedQueryMemento { private final String name; + private final @Nullable Class resultType; private final Boolean cacheable; private final String cacheRegion; @@ -37,6 +40,7 @@ public abstract class AbstractNamedQueryMemento implements NamedQueryMemento { protected AbstractNamedQueryMemento( String name, + @Nullable Class resultType, Boolean cacheable, String cacheRegion, CacheMode cacheMode, @@ -47,6 +51,7 @@ protected AbstractNamedQueryMemento( String comment, Map hints) { this.name = name; + this.resultType = resultType; this.cacheable = cacheable; this.cacheRegion = cacheRegion; this.cacheMode = cacheMode; @@ -63,6 +68,10 @@ public String getRegistrationName() { return name; } + public @Nullable Class getResultType() { + return resultType; + } + @Override public Boolean getCacheable() { return cacheable; diff --git a/hibernate-core/src/main/java/org/hibernate/query/results/dynamic/DynamicResultBuilderEntityStandard.java b/hibernate-core/src/main/java/org/hibernate/query/results/dynamic/DynamicResultBuilderEntityStandard.java index 17aab3d40547..4bd6141fb5b1 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/results/dynamic/DynamicResultBuilderEntityStandard.java +++ b/hibernate-core/src/main/java/org/hibernate/query/results/dynamic/DynamicResultBuilderEntityStandard.java @@ -28,7 +28,6 @@ import org.hibernate.query.results.DomainResultCreationStateImpl; import org.hibernate.query.results.FetchBuilder; import org.hibernate.query.results.ResultsHelper; -import org.hibernate.query.results.TableGroupImpl; import org.hibernate.query.results.complete.CompleteFetchBuilder; import org.hibernate.spi.NavigablePath; import org.hibernate.sql.ast.spi.FromClauseAccess; @@ -196,15 +195,17 @@ private T buildResultOrFetch( final TableGroup tableGroup = fromClauseAccess.resolveTableGroup( elementNavigablePath, np -> { - final TableReference tableReference = entityMapping.createPrimaryTableReference( - new SqlAliasBaseConstant( tableAlias ), - creationState - ); - if ( lockMode != null ) { domainResultCreationState.getSqlAstCreationState().registerLockMode( tableAlias, lockMode ); } - return new TableGroupImpl( elementNavigablePath, tableAlias, tableReference, entityMapping ); + return entityMapping.createRootTableGroup( + true, + navigablePath, + tableAlias, + new SqlAliasBaseConstant( tableAlias ), + null, + creationState + ); } ); final TableReference tableReference = tableGroup.getPrimaryTableReference(); diff --git a/hibernate-core/src/main/java/org/hibernate/query/spi/AbstractCommonQueryContract.java b/hibernate-core/src/main/java/org/hibernate/query/spi/AbstractCommonQueryContract.java index 32f8739f9515..499641be6512 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/spi/AbstractCommonQueryContract.java +++ b/hibernate-core/src/main/java/org/hibernate/query/spi/AbstractCommonQueryContract.java @@ -45,6 +45,7 @@ import org.hibernate.query.criteria.JpaExpression; import org.hibernate.query.internal.QueryOptionsImpl; import org.hibernate.query.sqm.SqmExpressible; +import org.hibernate.query.sqm.tree.expression.NullSqmExpressible; import org.hibernate.query.sqm.tree.expression.SqmLiteral; import org.hibernate.query.sqm.tree.expression.SqmParameter; import org.hibernate.query.sqm.tree.select.SqmSelectStatement; @@ -98,10 +99,16 @@ */ public abstract class AbstractCommonQueryContract implements CommonQueryContract { private final SharedSessionContractImplementor session; - private final QueryOptionsImpl queryOptions = new QueryOptionsImpl(); + private final QueryOptionsImpl queryOptions; public AbstractCommonQueryContract(SharedSessionContractImplementor session) { this.session = session; + this.queryOptions = new QueryOptionsImpl(); + } + + protected AbstractCommonQueryContract(AbstractCommonQueryContract original) { + this.session = original.session; + this.queryOptions = original.queryOptions; } public SharedSessionContractImplementor getSession() { @@ -906,7 +913,7 @@ public CommonQueryContract setParameter(int position, Object value) { final QueryParameter param = binding.getQueryParameter(); if ( param.allowsMultiValuedBinding() ) { final BindableType hibernateType = param.getHibernateType(); - if ( hibernateType == null || isInstance( hibernateType, value ) ) { + if ( hibernateType == null || hibernateType instanceof NullSqmExpressible || isInstance( hibernateType, value ) ) { if ( value instanceof Collection && !isRegisteredAsBasicType( value.getClass() ) ) { //noinspection rawtypes return setParameterList( position, (Collection) value ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/spi/AbstractSelectionQuery.java b/hibernate-core/src/main/java/org/hibernate/query/spi/AbstractSelectionQuery.java index df42432cff0e..687302e2902b 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/spi/AbstractSelectionQuery.java +++ b/hibernate-core/src/main/java/org/hibernate/query/spi/AbstractSelectionQuery.java @@ -36,6 +36,7 @@ import org.hibernate.query.IllegalQueryOperationException; import org.hibernate.query.QueryParameter; import org.hibernate.query.SelectionQuery; +import org.hibernate.query.internal.QueryOptionsImpl; import org.hibernate.query.internal.ScrollableResultsIterator; import org.hibernate.query.named.NamedQueryMemento; import org.hibernate.sql.exec.internal.CallbackImpl; @@ -81,6 +82,12 @@ public AbstractSelectionQuery(SharedSessionContractImplementor session) { super( session ); } + protected AbstractSelectionQuery(AbstractSelectionQuery original) { + super( original ); + this.sessionFlushMode = original.sessionFlushMode; + this.sessionCacheMode = original.sessionCacheMode; + } + protected void applyOptions(NamedQueryMemento memento) { if ( memento.getHints() != null ) { memento.getHints().forEach( this::applyHint ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NamedNativeQueryMementoImpl.java b/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NamedNativeQueryMementoImpl.java index 8ba29214b482..5d9f383aae08 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NamedNativeQueryMementoImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NamedNativeQueryMementoImpl.java @@ -38,6 +38,7 @@ public class NamedNativeQueryMementoImpl extends AbstractNamedQueryMemento imple public NamedNativeQueryMementoImpl( String name, + Class resultClass, String sqlString, String originalSqlString, String resultSetMappingName, @@ -56,6 +57,7 @@ public NamedNativeQueryMementoImpl( Map hints) { super( name, + resultClass, cacheable, cacheRegion, cacheMode, @@ -123,6 +125,7 @@ public Integer getMaxResults() { public NamedNativeQueryMemento makeCopy(String name) { return new NamedNativeQueryMementoImpl( name, + getResultType(), sqlString, originalSqlString, resultSetMappingName, @@ -149,7 +152,8 @@ public void validate(QueryEngine queryEngine) { @Override public NativeQueryImplementor toQuery(SharedSessionContractImplementor session) { - return new NativeQueryImpl<>( this, session ); + //noinspection unchecked + return new NativeQueryImpl<>( this, (Class) getResultType(), session ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NativeQueryImpl.java b/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NativeQueryImpl.java index 6179773fc609..36e5e1d3f55e 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NativeQueryImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NativeQueryImpl.java @@ -20,8 +20,13 @@ import java.util.function.Consumer; import java.util.function.Supplier; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.CacheMode; import org.hibernate.FlushMode; +import org.hibernate.jpa.spi.NativeQueryArrayTransformer; +import org.hibernate.jpa.spi.NativeQueryConstructorTransformer; +import org.hibernate.jpa.spi.NativeQueryListTransformer; +import org.hibernate.jpa.spi.NativeQueryMapTransformer; import org.hibernate.HibernateException; import org.hibernate.LockMode; import org.hibernate.LockOptions; @@ -55,7 +60,6 @@ import org.hibernate.query.internal.DelegatingDomainQueryExecutionContext; import org.hibernate.query.internal.ParameterMetadataImpl; import org.hibernate.query.internal.QueryOptionsImpl; -import org.hibernate.query.internal.QueryParameterBindingsImpl; import org.hibernate.query.internal.ResultSetMappingResolutionContext; import org.hibernate.query.named.NamedResultSetMappingMemento; import org.hibernate.query.results.Builders; @@ -92,7 +96,6 @@ import org.hibernate.query.sql.spi.SelectInterpretationsKey; import org.hibernate.sql.exec.internal.CallbackImpl; import org.hibernate.sql.exec.spi.Callback; -import org.hibernate.sql.results.jdbc.spi.JdbcValuesMappingProducer; import org.hibernate.sql.results.spi.SingleResultConsumer; import org.hibernate.transform.ResultTransformer; import org.hibernate.type.BasicType; @@ -110,8 +113,13 @@ import jakarta.persistence.TemporalType; import jakarta.persistence.Tuple; import jakarta.persistence.metamodel.SingularAttribute; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.java.spi.UnknownBasicJavaType; +import static org.hibernate.internal.util.ReflectHelper.isClass; +import static org.hibernate.internal.util.StringHelper.unqualify; import static org.hibernate.jpa.HibernateHints.HINT_NATIVE_LOCK_MODE; +import static org.hibernate.query.sqm.internal.SqmUtil.isResultTypeAlwaysAllowed; import static org.hibernate.query.results.Builders.resultClassBuilder; /** @@ -126,6 +134,7 @@ public class NativeQueryImpl private final List parameterOccurrences; private final QueryParameterBindings parameterBindings; + private final Class resultType; private final ResultSetMapping resultSetMapping; private final boolean resultMappingSuppliedToCtor; @@ -177,6 +186,7 @@ else if ( memento.getResultMappingClass() != null ) { return false; }, + null, session ); } @@ -206,39 +216,16 @@ public NativeQueryImpl( } } - if ( memento.getResultMappingClass() != null ) { - resultSetMapping.addResultBuilder( resultClassBuilder( - memento.getResultMappingClass(), - context - ) ); + if ( memento.getResultType() != null ) { + resultSetMapping.addResultBuilder( resultClassBuilder( memento.getResultType(), context ) ); return true; } return false; }, + resultJavaType, session ); - - if ( resultJavaType == Tuple.class ) { - setTupleTransformer( new NativeQueryTupleTransformer() ); - } - else if ( resultJavaType != null && !resultJavaType.isArray() ) { - switch ( resultSetMapping.getNumberOfResultBuilders() ) { - case 0: { - throw new IllegalArgumentException( "Named query exists, but did not specify a resultClass" ); - } - case 1: { - final Class actualResultJavaType = resultSetMapping.getResultBuilders().get( 0 ).getJavaType(); - if ( actualResultJavaType != null && !resultJavaType.isAssignableFrom( actualResultJavaType ) ) { - throw buildIncompatibleException( resultJavaType, actualResultJavaType ); - } - break; - } - default: { - throw new IllegalArgumentException( "Cannot create TypedQuery for query with more than one return" ); - } - } - } } /** @@ -260,6 +247,7 @@ public NativeQueryImpl( mappingMemento.resolve( resultSetMapping, querySpaceConsumer, context ); return true; }, + null, session ); @@ -270,6 +258,15 @@ public NativeQueryImpl( Supplier resultSetMappingCreator, ResultSetMappingHandler resultSetMappingHandler, SharedSessionContractImplementor session) { + this( memento, resultSetMappingCreator, resultSetMappingHandler, null, session ); + } + + public NativeQueryImpl( + NamedNativeQueryMemento memento, + Supplier resultSetMappingCreator, + ResultSetMappingHandler resultSetMappingHandler, + @Nullable Class resultType, + SharedSessionContractImplementor session) { super( session ); this.originalSqlString = memento.getOriginalSqlString(); @@ -283,6 +280,7 @@ public NativeQueryImpl( this.parameterMetadata = parameterInterpretation.toParameterMetadata( session ); this.parameterOccurrences = parameterInterpretation.getOrderedParameterOccurrences(); this.parameterBindings = parameterMetadata.createBindings( session.getFactory() ); + this.resultType = resultType; this.querySpaces = new HashSet<>(); this.resultSetMapping = resultSetMappingCreator.get(); @@ -296,6 +294,27 @@ public NativeQueryImpl( this.resultMappingSuppliedToCtor = appliedAnyResults; + if ( resultType != null ) { + if ( !isResultTypeAlwaysAllowed( resultType ) ) { + switch ( resultSetMapping.getNumberOfResultBuilders() ) { + case 0: + throw new IllegalArgumentException( "Named query exists, but did not specify a resultClass" ); + case 1: + final Class actualResultJavaType = resultSetMapping.getResultBuilders().get( 0 ) + .getJavaType(); + if ( actualResultJavaType != null && !resultType.isAssignableFrom( actualResultJavaType ) ) { + throw buildIncompatibleException( resultType, actualResultJavaType ); + } + break; + default: + throw new IllegalArgumentException( + "Cannot create TypedQuery for query with more than one return" ); + } + } + else { + setTupleTransformerForResultType( resultType ); + } + } applyOptions( memento ); } @@ -312,6 +331,7 @@ public NativeQueryImpl( this.parameterMetadata = parameterInterpretation.toParameterMetadata( session ); this.parameterOccurrences = parameterInterpretation.getOrderedParameterOccurrences(); this.parameterBindings = parameterMetadata.createBindings( session.getFactory() ); + this.resultType = null; this.querySpaces = new HashSet<>(); this.resultSetMapping = buildResultSetMapping( resultSetMappingMemento.getName(), false, session ); @@ -325,6 +345,10 @@ public NativeQueryImpl( } public NativeQueryImpl(String sqlString, SharedSessionContractImplementor session) { + this( sqlString, null, session ); + } + + public NativeQueryImpl(String sqlString, @Nullable Class resultType, SharedSessionContractImplementor session) { super( session ); this.querySpaces = new HashSet<>(); @@ -335,11 +359,50 @@ public NativeQueryImpl(String sqlString, SharedSessionContractImplementor sessio this.parameterMetadata = parameterInterpretation.toParameterMetadata( session ); this.parameterOccurrences = parameterInterpretation.getOrderedParameterOccurrences(); this.parameterBindings = parameterMetadata.createBindings( session.getFactory() ); + this.resultType = resultType; + if ( resultType != null ) { + setTupleTransformerForResultType( resultType ); + } this.resultSetMapping = ResultSetMapping.resolveResultSetMapping( sqlString, true, session.getFactory() ); this.resultMappingSuppliedToCtor = false; } + protected void setTupleTransformerForResultType(Class resultClass) { + final TupleTransformer tupleTransformer = determineTupleTransformerForResultType( resultClass ); + if ( tupleTransformer != null ) { + setTupleTransformer( tupleTransformer ); + } + } + + protected @Nullable TupleTransformer determineTupleTransformerForResultType(Class resultClass) { + if ( Tuple.class.equals( resultClass ) ) { + return NativeQueryTupleTransformer.INSTANCE; + } + else if ( Map.class.equals( resultClass ) ) { + return NativeQueryMapTransformer.INSTANCE; + } + else if ( List.class.equals( resultClass ) ) { + return NativeQueryListTransformer.INSTANCE; + } + else if ( Object[].class.equals( resultClass )) { + return NativeQueryArrayTransformer.INSTANCE; + } + else if ( resultClass != Object.class ) { + if ( isClass( resultClass ) && !hasJavaTypeDescriptor( resultClass ) ) { + // not a basic type + return new NativeQueryConstructorTransformer<>( resultClass ); + } + } + return null; + } + + private boolean hasJavaTypeDescriptor(Class resultClass) { + final JavaType descriptor = getSessionFactory().getTypeConfiguration().getJavaTypeRegistry() + .findDescriptor( resultClass ); + return descriptor != null && descriptor.getClass() != UnknownBasicJavaType.class; + } + @FunctionalInterface private interface ResultSetMappingHandler { boolean resolveResultSetMapping( @@ -456,10 +519,16 @@ public QueryParameterBindings getParameterBindings() { return getQueryParameterBindings(); } + @Override + public Class getResultType() { + return resultType; + } + @Override public NamedNativeQueryMemento toMemento(String name) { return new NamedNativeQueryMementoImpl( name, + resultType != null ? resultType : extractResultClass( resultSetMapping ), sqlString, originalSqlString, resultSetMapping.getMappingIdentifier(), @@ -479,14 +548,14 @@ public NamedNativeQueryMemento toMemento(String name) { ); } - private Class extractResultClass(ResultSetMapping resultSetMapping) { + private Class extractResultClass(ResultSetMapping resultSetMapping) { final List resultBuilders = resultSetMapping.getResultBuilders(); if ( resultBuilders.size() == 1 ) { final ResultBuilder resultBuilder = resultBuilders.get( 0 ); if ( resultBuilder instanceof ImplicitResultClassBuilder || resultBuilder instanceof ImplicitModelPartResultBuilderEntity || resultBuilder instanceof DynamicResultBuilderEntityCalculated ) { - return resultBuilder.getJavaType(); + return (Class) resultBuilder.getJavaType(); } } return null; @@ -645,19 +714,32 @@ public KeyedResultList getKeyedResultList(KeyedPage page) { } protected SelectQueryPlan resolveSelectQueryPlan() { - if ( isCacheableQuery() ) { - final QueryInterpretationCache.Key cacheKey = generateSelectInterpretationsKey( resultSetMapping ); - return getSession().getFactory().getQueryEngine().getInterpretationCache() - .resolveSelectQueryPlan( cacheKey, () -> createQueryPlan( resultSetMapping ) ); + final ResultSetMapping mapping; + if ( resultType != null && resultSetMapping.isDynamic() && resultSetMapping.getNumberOfResultBuilders() == 0 ) { + mapping = ResultSetMapping.resolveResultSetMapping( originalSqlString, true, getSessionFactory() ); + + if ( getSessionFactory().getMappingMetamodel().isEntityClass( resultType ) ) { + mapping.addResultBuilder( + Builders.entityCalculated( unqualify( resultType.getName() ), resultType.getName(), + LockMode.READ, getSessionFactory() ) ); + } + else if ( !isResultTypeAlwaysAllowed( resultType ) + && (!isClass( resultType ) || hasJavaTypeDescriptor( resultType )) ) { + mapping.addResultBuilder( Builders.resultClassBuilder( resultType, getSessionFactory() ) ); + } } else { - return createQueryPlan( resultSetMapping ); + mapping = resultSetMapping; } + return isCacheableQuery() + ? getSession().getFactory().getQueryEngine().getInterpretationCache().resolveSelectQueryPlan( selectInterpretationsKey( mapping ), () -> createQueryPlan( mapping ) ) + : createQueryPlan( mapping ); } private NativeSelectQueryPlan createQueryPlan(ResultSetMapping resultSetMapping) { - final String sqlString = expandParameterLists(); final NativeSelectQueryDefinition queryDefinition = new NativeSelectQueryDefinition<>() { + final String sqlString = expandParameterLists(); + @Override public String getSqlString() { return sqlString; @@ -864,7 +946,7 @@ public static int determineBindValueMaxCount(boolean paddingEnabled, int inExprL return bindValueMaxCount; } - private SelectInterpretationsKey generateSelectInterpretationsKey(JdbcValuesMappingProducer resultSetMapping) { + private SelectInterpretationsKey selectInterpretationsKey(ResultSetMapping resultSetMapping) { return new SelectInterpretationsKey( getQueryString(), resultSetMapping, diff --git a/hibernate-core/src/main/java/org/hibernate/query/sql/internal/ResultSetMappingProcessor.java b/hibernate-core/src/main/java/org/hibernate/query/sql/internal/ResultSetMappingProcessor.java index c051eb564647..486e00b4335c 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sql/internal/ResultSetMappingProcessor.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sql/internal/ResultSetMappingProcessor.java @@ -25,6 +25,7 @@ import org.hibernate.loader.internal.AliasConstantsHelper; import org.hibernate.metamodel.mapping.CollectionPart; import org.hibernate.metamodel.mapping.EntityMappingType; +import org.hibernate.metamodel.mapping.internal.ToOneAttributeMapping; import org.hibernate.persister.collection.CollectionPersister; import org.hibernate.persister.collection.QueryableCollection; import org.hibernate.persister.collection.SQLLoadableCollection; @@ -399,6 +400,17 @@ else if ( propertyType instanceof ComponentType ) { resultBuilderEntity.addFetchBuilder( propertyName, fetchBuilder ); } else if ( columnAliases.length != 0 ) { + if ( propertyType instanceof EntityType ) { + final ToOneAttributeMapping toOne = (ToOneAttributeMapping) loadable.findAttributeMapping( propertyName ); + if ( !toOne.getIdentifyingColumnsTableExpression().equals( loadable.getMappedTableDetails().getTableName() ) ) { + // The to-one has a join-table, use the plain join column name instead of the alias + assert columnAliases.length == 1; + final String[] targetAliases = new String[1]; + targetAliases[0] = toOne.getTargetKeyPropertyName(); + resultBuilderEntity.addProperty( propertyName, targetAliases ); + return; + } + } resultBuilderEntity.addProperty( propertyName, columnAliases ); } } @@ -409,7 +421,7 @@ private CompleteResultBuilderCollectionStandard createSuffixedResultBuilder( String entitySuffix) { final CollectionPersister collectionPersister = collectionReturn.getPluralAttribute().getCollectionDescriptor(); final String[] elementColumnAliases; - if ( collectionPersister.getElementType().isEntityType() ) { + if ( collectionPersister.getElementType() instanceof EntityType ) { final Loadable elementPersister = (Loadable) ( ( QueryableCollection ) collectionPersister).getElementPersister(); final String[] propertyNames = elementPersister.getPropertyNames(); final String[] identifierAliases = elementPersister.getIdentifierAliases( entitySuffix ); @@ -568,13 +580,13 @@ private void processFetchReturn(NativeQuery.FetchReturn fetchReturn) { SQLLoadable ownerPersister = ( SQLLoadable ) alias2Persister.get( ownerAlias ); Type returnType = ownerPersister.getPropertyType( fetchReturn.getFetchableName() ); - if ( returnType.isCollectionType() ) { + if ( returnType instanceof CollectionType ) { String role = ownerPersister.getEntityName() + '.' + fetchReturn.getFetchableName(); Map propertyResultsMap = Collections.emptyMap();//fetchReturn.getPropertyResultsMap() addCollection( role, alias, propertyResultsMap ); // collectionOwnerAliases.add( ownerAlias ); } - else if ( returnType.isEntityType() ) { + else if ( returnType instanceof EntityType ) { EntityType eType = ( EntityType ) returnType; String returnEntityName = eType.getAssociatedEntityName(); SQLLoadable persister = getSQLLoadable( returnEntityName ); @@ -634,7 +646,7 @@ public Map getPropertyResultsMap(String alias) { // } // for ( CollectionPersister persister : alias2CollectionPersister.values() ) { // final Type elementType = persister.getElementType(); -// if ( elementType.isEntityType() && ! elementType.isAnyType() ) { +// if ( elementType instanceof EntityType && ! elementType instanceof AnyType ) { // final Joinable joinable = ( (EntityType) elementType ).getAssociatedJoinable( factory ); // Collections.addAll( spaces, (String[]) ( (EntityPersister) joinable ).getQuerySpaces() ); // } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sql/spi/NamedNativeQueryMemento.java b/hibernate-core/src/main/java/org/hibernate/query/sql/spi/NamedNativeQueryMemento.java index 54aad16b4cb4..8d4659b15f2f 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sql/spi/NamedNativeQueryMemento.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sql/spi/NamedNativeQueryMemento.java @@ -9,6 +9,7 @@ import java.util.Set; import jakarta.persistence.SqlResultSetMapping; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.boot.query.NamedNativeQueryDefinition; import org.hibernate.boot.registry.classloading.spi.ClassLoaderService; @@ -50,6 +51,8 @@ default String getOriginalSqlString(){ */ Class getResultMappingClass(); + @Nullable Class getResultType(); + Integer getFirstResult(); Integer getMaxResults(); @@ -135,6 +138,7 @@ public void setResultSetMappingClassName(String resultSetMappingClassName) { public NamedNativeQueryMemento build(SessionFactoryImplementor sessionFactory) { return new NamedNativeQueryMementoImpl( name, + null, queryString, queryString, resultSetMappingName, diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java index a550f9d4d7a0..9fa7899fdedb 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java @@ -32,7 +32,6 @@ import org.hibernate.query.criteria.JpaSelection; import org.hibernate.query.criteria.JpaSimpleCase; import org.hibernate.query.criteria.JpaWindow; -import org.hibernate.query.criteria.ValueHandlingMode; import org.hibernate.query.spi.QueryEngine; import org.hibernate.query.sqm.tree.delete.SqmDeleteStatement; import org.hibernate.query.sqm.tree.domain.SqmBagJoin; @@ -86,8 +85,6 @@ public interface NodeBuilder extends HibernateCriteriaBuilder { QueryEngine getQueryEngine(); - void setCriteriaValueHandlingMode(ValueHandlingMode criteriaValueHandlingMode); - SqmTuple tuple( Class tupleType, SqmExpression... expressions); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/AbstractSqmSelectionQuery.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/AbstractSqmSelectionQuery.java index 7d7783cc140c..420b0e9d7413 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/AbstractSqmSelectionQuery.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/AbstractSqmSelectionQuery.java @@ -29,6 +29,7 @@ import org.hibernate.query.spi.QueryOptions; import org.hibernate.query.spi.SelectQueryPlan; import org.hibernate.query.sqm.NodeBuilder; +import org.hibernate.query.sqm.SqmQuerySource; import org.hibernate.query.sqm.spi.NamedSqmQueryMemento; import org.hibernate.query.sqm.tree.SqmStatement; import org.hibernate.query.sqm.tree.from.SqmRoot; @@ -69,6 +70,10 @@ abstract class AbstractSqmSelectionQuery extends AbstractSelectionQuery { super(session); } + AbstractSqmSelectionQuery(AbstractSqmSelectionQuery original) { + super( original ); + } + protected int max(boolean hasLimit, SqmSelectStatement sqmStatement, List list) { return !hasLimit || getQueryOptions().getLimit().getMaxRows() == null ? getMaxRows( sqmStatement, list.size() ) @@ -158,41 +163,14 @@ public SelectionQuery setPage(Page page) { return this; } - private SqmSelectStatement> paginateQuery( - List> keyDefinition, List> keyValues) { - @SuppressWarnings("unchecked") - final SqmSelectStatement> sqm = - (SqmSelectStatement>) - getSqmSelectStatement().copy( noParamCopyContext() ); - final NodeBuilder builder = sqm.nodeBuilder(); - //TODO: find a better way handle parameters - builder.setCriteriaValueHandlingMode(ValueHandlingMode.INLINE); - return paginate( keyDefinition, keyValues, sqm, builder ); - } - - @Override public KeyedResultList getKeyedResultList(KeyedPage keyedPage) { if ( keyedPage == null ) { throw new IllegalArgumentException( "KeyedPage was null" ); } + final List> results = new SqmSelectionQueryImpl>( this, keyedPage ) + .getResultList(); final Page page = keyedPage.getPage(); - final List> key = keyedPage.getKey(); - final List> keyDefinition = keyedPage.getKeyDefinition(); - final List> appliedKeyDefinition = - keyedPage.getKeyInterpretation() == KEY_OF_FIRST_ON_NEXT_PAGE - ? Order.reverse(keyDefinition) : keyDefinition; - - setMaxResults( page.getMaxResults() + 1 ); - if ( key == null ) { - setFirstResult( page.getFirstResult() ); - } - -// getQueryOptions().setQueryPlanCachingEnabled( false ); - final List> results = - buildConcreteQueryPlan( paginateQuery( appliedKeyDefinition, key ), getQueryOptions() ) - .performList(this); - return new KeyedResultList<>( collectResults( results, page.getSize(), keyedPage.getKeyInterpretation() ), collectKeys( results, page.getSize() ), @@ -276,10 +254,6 @@ protected ConcreteSqmSelectQueryPlan buildConcreteQueryPlan( ); } - private SelectQueryPlan buildConcreteQueryPlan(SqmSelectStatement sqmStatement, QueryOptions options) { - return buildConcreteQueryPlan( sqmStatement, null, null, options ); - } - protected void applyOptions(NamedSqmQueryMemento memento) { applyOptions( (NamedQueryMemento) memento ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/KeyBasedPagination.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/KeyBasedPagination.java index f1eb6eabab1d..098f3ec442f5 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/KeyBasedPagination.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/KeyBasedPagination.java @@ -118,11 +118,6 @@ private static > SqmPredicate keyPredicate( Expression key, C keyValue, SortDirection direction, List> previousKeys, List> keyValues, NodeBuilder builder) { - // TODO: use a parameter here and create a binding for it -// @SuppressWarnings("unchecked") -// final Class valueClass = (Class) keyValue.getClass(); -// final JpaParameterExpression parameter = builder.parameter(valueClass); -// setParameter( parameter, keyValue ); SqmPredicate predicate; switch ( direction ) { case ASCENDING: diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/NoParamSqmCopyContext.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/NoParamSqmCopyContext.java index 8a3d960a2a05..ae8f09f48b39 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/NoParamSqmCopyContext.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/NoParamSqmCopyContext.java @@ -6,6 +6,7 @@ */ package org.hibernate.query.sqm.internal; +import org.hibernate.query.sqm.SqmQuerySource; import org.hibernate.query.sqm.tree.expression.SqmParameter; import org.checkerframework.checker.nullness.qual.Nullable; @@ -14,6 +15,13 @@ * @author Marco Belladelli */ public class NoParamSqmCopyContext extends SimpleSqmCopyContext { + public NoParamSqmCopyContext() { + } + + public NoParamSqmCopyContext(@Nullable SqmQuerySource querySource) { + super( querySource ); + } + @Override public @Nullable T getCopy(T original) { if ( original instanceof SqmParameter ) { diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/QuerySqmImpl.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/QuerySqmImpl.java index 785f0ccaf0ee..91fa946753ee 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/QuerySqmImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/QuerySqmImpl.java @@ -904,6 +904,7 @@ public NamedQueryMemento toMemento(String name) { } return new NamedCriteriaQueryMementoImpl( name, + getResultType(), sqmStatement, getQueryOptions().getLimit().getFirstRow(), getQueryOptions().getLimit().getMaxRows(), @@ -923,6 +924,7 @@ public NamedQueryMemento toMemento(String name) { return new NamedHqlQueryMementoImpl( name, + getResultType(), getQueryString(), getQueryOptions().getLimit().getFirstRow(), getQueryOptions().getLimit().getMaxRows(), diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SimpleSqmCopyContext.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SimpleSqmCopyContext.java index 37e61b8d4145..0a3c20cbb051 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SimpleSqmCopyContext.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SimpleSqmCopyContext.java @@ -8,6 +8,7 @@ import java.util.IdentityHashMap; +import org.hibernate.query.sqm.SqmQuerySource; import org.hibernate.query.sqm.tree.SqmCopyContext; import org.checkerframework.checker.nullness.qual.Nullable; @@ -17,6 +18,15 @@ */ public class SimpleSqmCopyContext implements SqmCopyContext { private final IdentityHashMap map = new IdentityHashMap<>(); + private final @Nullable SqmQuerySource querySource; + + public SimpleSqmCopyContext() { + this( null ); + } + + public SimpleSqmCopyContext(@Nullable SqmQuerySource querySource) { + this.querySource = querySource; + } @Override @SuppressWarnings( "unchecked" ) @@ -32,4 +42,9 @@ public T registerCopy(T original, T copy) { } return copy; } + + @Override + public @Nullable SqmQuerySource getQuerySource() { + return querySource; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java index 65d8569f8fb4..632953b7e9b6 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java @@ -46,7 +46,6 @@ import org.hibernate.metamodel.model.domain.DomainType; import org.hibernate.metamodel.model.domain.JpaMetamodel; import org.hibernate.metamodel.model.domain.SingularPersistentAttribute; -import org.hibernate.metamodel.model.domain.internal.BasicTypeImpl; import org.hibernate.metamodel.model.domain.spi.JpaMetamodelImplementor; import org.hibernate.metamodel.spi.MappingMetamodelImplementor; import org.hibernate.query.BindableType; @@ -199,7 +198,7 @@ public class SqmCriteriaNodeBuilder implements NodeBuilder, SqmCreationContext, private final transient boolean jpaComplianceEnabled; private final transient QueryEngine queryEngine; private final transient Supplier sessionFactory; - private transient ValueHandlingMode criteriaValueHandlingMode; + private final transient ValueHandlingMode criteriaValueHandlingMode; private transient BasicType booleanType; private transient BasicType integerType; private transient BasicType longType; @@ -228,10 +227,6 @@ public SqmCriteriaNodeBuilder( } } - public void setCriteriaValueHandlingMode(ValueHandlingMode criteriaValueHandlingMode) { - this.criteriaValueHandlingMode = criteriaValueHandlingMode; - } - @Override public JpaMetamodel getDomainModel() { return getSessionFactory().getJpaMetamodel(); @@ -1331,7 +1326,7 @@ else if ( value == null ) { final EnumJavaType javaType = new EnumJavaType<>( type ); final JdbcType jdbcType = javaType.getRecommendedJdbcType( typeConfiguration.getCurrentBaseSqlTypeIndicators() ); - return new BasicTypeImpl<>( javaType, jdbcType ); + return typeConfiguration.getBasicTypeRegistry().resolve( javaType, jdbcType ); } else { return result; diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmSelectionQueryImpl.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmSelectionQueryImpl.java index 47b1ac97cb59..5996996a3e9d 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmSelectionQueryImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmSelectionQueryImpl.java @@ -30,7 +30,11 @@ import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.graph.spi.AppliedGraph; import org.hibernate.internal.util.collections.IdentitySet; +import org.hibernate.metamodel.mapping.MappingModelExpressible; import org.hibernate.query.BindableType; +import org.hibernate.query.KeyedPage; +import org.hibernate.query.Order; +import org.hibernate.query.Page; import org.hibernate.query.QueryParameter; import org.hibernate.query.criteria.internal.NamedCriteriaQueryMementoImpl; import org.hibernate.query.hql.internal.NamedHqlQueryMementoImpl; @@ -42,9 +46,11 @@ import org.hibernate.query.spi.ParameterMetadataImplementor; import org.hibernate.query.spi.QueryInterpretationCache; import org.hibernate.query.spi.QueryOptions; +import org.hibernate.query.spi.QueryParameterBinding; import org.hibernate.query.spi.QueryParameterBindings; import org.hibernate.query.spi.ScrollableResultsImplementor; import org.hibernate.query.spi.SelectQueryPlan; +import org.hibernate.query.sqm.SqmQuerySource; import org.hibernate.query.sqm.SqmSelectionQuery; import org.hibernate.query.sqm.internal.SqmInterpretationsKey.InterpretationsKeySource; import org.hibernate.query.sqm.spi.SqmSelectionQueryImplementor; @@ -58,6 +64,7 @@ import org.hibernate.sql.results.internal.TupleMetadata; import org.hibernate.sql.results.spi.ResultsConsumer; import org.hibernate.sql.results.spi.SingleResultConsumer; +import org.hibernate.type.descriptor.java.JavaType; import static org.hibernate.jpa.HibernateHints.HINT_CACHEABLE; import static org.hibernate.jpa.HibernateHints.HINT_CACHE_MODE; @@ -69,9 +76,12 @@ import static org.hibernate.jpa.LegacySpecHints.HINT_JAVAEE_CACHE_STORE_MODE; import static org.hibernate.jpa.SpecHints.HINT_SPEC_CACHE_RETRIEVE_MODE; import static org.hibernate.jpa.SpecHints.HINT_SPEC_CACHE_STORE_MODE; +import static org.hibernate.query.KeyedPage.KeyInterpretation.KEY_OF_FIRST_ON_NEXT_PAGE; import static org.hibernate.query.spi.SqlOmittingQueryOptions.omitSqlQueryOptions; +import static org.hibernate.query.sqm.internal.KeyBasedPagination.paginate; import static org.hibernate.query.sqm.internal.SqmInterpretationsKey.createInterpretationsKey; import static org.hibernate.query.sqm.internal.SqmUtil.isSelectionAssignableToResultType; +import static org.hibernate.query.sqm.tree.SqmCopyContext.noParamCopyContext; /** * @author Steve Ebersole @@ -179,6 +189,87 @@ public SqmSelectionQueryImpl( this.tupleMetadata = buildTupleMetadata( sqm, expectedResultType ); } + SqmSelectionQueryImpl(AbstractSqmSelectionQuery original, KeyedPage keyedPage) { + super( original ); + + final Page page = keyedPage.getPage(); + final List> key = keyedPage.getKey(); + final List> keyDefinition = keyedPage.getKeyDefinition(); + final List> appliedKeyDefinition = + keyedPage.getKeyInterpretation() == KEY_OF_FIRST_ON_NEXT_PAGE + ? Order.reverse( keyDefinition ) : keyDefinition; + + //noinspection unchecked + this.sqm = (SqmSelectStatement) paginate( + appliedKeyDefinition, + key, + // Change the query source to CRITERIA, because we will change the query and introduce parameters + (SqmSelectStatement>) original.getSqmStatement() + .copy( noParamCopyContext( SqmQuerySource.CRITERIA ) ), + original.getSqmStatement().nodeBuilder() + ); + this.hql = CRITERIA_HQL_STRING; + + this.domainParameterXref = DomainParameterXref.from( sqm ); + this.parameterMetadata = domainParameterXref.hasParameters() + ? new ParameterMetadataImpl( domainParameterXref.getQueryParameters() ) + : ParameterMetadataImpl.EMPTY; + + // Just use the original parameter bindings since this object is never going to be mutated + this.parameterBindings = parameterMetadata.createBindings( original.getSession().getSessionFactory() ); + // Don't remove this cast. This is here to work around this bug: https://bugs.openjdk.org/browse/JDK-8340443 + (( DomainQueryExecutionContext) original ).getQueryParameterBindings().visitBindings( + (parameter, binding) -> { + //noinspection unchecked + final QueryParameterBinding parameterBinding = + (QueryParameterBinding) this.parameterBindings.getBinding( parameter ); + //noinspection unchecked + final BindableType bindType = (BindableType) binding.getBindType(); + final TemporalType explicitTemporalPrecision = binding.getExplicitTemporalPrecision(); + if ( explicitTemporalPrecision != null ) { + if ( binding.isMultiValued() ) { + parameterBinding.setBindValues( + binding.getBindValues(), + explicitTemporalPrecision, + getSessionFactory().getTypeConfiguration() + ); + } + else { + parameterBinding.setBindValue( binding.getBindValue(), explicitTemporalPrecision ); + } + } + else { + if ( binding.isMultiValued() ) { + parameterBinding.setBindValues( binding.getBindValues(), bindType ); + } + else { + parameterBinding.setBindValue( binding.getBindValue(), bindType ); + } + } + //noinspection unchecked + parameterBinding.setType( (MappingModelExpressible) binding.getType() ); + } + ); + + // Parameters might be created through HibernateCriteriaBuilder.value which we need to bind here + for ( SqmParameter sqmParameter : domainParameterXref.getParameterResolutions().getSqmParameters() ) { + if ( sqmParameter instanceof SqmJpaCriteriaParameterWrapper ) { + bindCriteriaParameter( (SqmJpaCriteriaParameterWrapper) sqmParameter ); + } + } + + //noinspection unchecked + this.expectedResultType = (Class) KeyedResult.class; + this.resultType = determineResultType( sqm, expectedResultType ); + this.tupleMetadata = null; + + setMaxResults( page.getMaxResults() + 1 ); + if ( key == null ) { + setFirstResult( page.getFirstResult() ); + } + } + + private static Class determineResultType(SqmSelectStatement sqm, Class expectedResultType) { final List> selections = sqm.getQuerySpec().getSelectClause().getSelections(); if ( selections.size() == 1 ) { @@ -187,15 +278,15 @@ private static Class determineResultType(SqmSelectStatement sqm, Class return Object[].class; } else { - final SqmSelection selection = selections.get(0); + final SqmSelection selection = selections.get( 0 ); if ( isSelectionAssignableToResultType( selection, expectedResultType ) ) { - return selection.getNodeJavaType().getJavaTypeClass(); - } - else { - // let's assume there's some - // way to instantiate it - return expectedResultType; + final JavaType nodeJavaType = selection.getNodeJavaType(); + if ( nodeJavaType != null ) { + return nodeJavaType.getJavaTypeClass(); + } } + // let's assume there's some way to instantiate it + return expectedResultType; } } else if ( expectedResultType != null ) { diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmUtil.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmUtil.java index d6a63c5cb3a9..db6ae8aa0dbc 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmUtil.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmUtil.java @@ -24,6 +24,7 @@ import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.internal.util.collections.CollectionHelper; +import org.hibernate.internal.util.collections.Stack; import org.hibernate.metamodel.MappingMetamodel; import org.hibernate.metamodel.mapping.BasicValuedMapping; import org.hibernate.metamodel.mapping.Bindable; @@ -37,12 +38,17 @@ import org.hibernate.metamodel.mapping.ModelPart; import org.hibernate.metamodel.mapping.ModelPartContainer; import org.hibernate.metamodel.mapping.PluralAttributeMapping; +import org.hibernate.metamodel.mapping.internal.ToOneAttributeMapping; import org.hibernate.metamodel.model.domain.BasicDomainType; import org.hibernate.metamodel.model.domain.DomainType; import org.hibernate.metamodel.model.domain.EntityDomainType; import org.hibernate.metamodel.model.domain.IdentifiableDomainType; +import org.hibernate.metamodel.model.domain.ManagedDomainType; import org.hibernate.metamodel.model.domain.SimpleDomainType; +import org.hibernate.metamodel.model.domain.SingularPersistentAttribute; import org.hibernate.metamodel.model.domain.internal.EntitySqmPathSource; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.query.BindableType; import org.hibernate.query.IllegalQueryOperationException; import org.hibernate.query.IllegalSelectQueryException; import org.hibernate.query.Order; @@ -63,6 +69,7 @@ import org.hibernate.query.sqm.tree.SqmJoinType; import org.hibernate.query.sqm.tree.SqmStatement; import org.hibernate.query.sqm.tree.domain.SqmPath; +import org.hibernate.query.sqm.tree.domain.SqmSingularJoin; import org.hibernate.query.sqm.tree.expression.JpaCriteriaParameter; import org.hibernate.query.sqm.tree.expression.SqmAliasedNodeRef; import org.hibernate.query.sqm.tree.expression.SqmExpression; @@ -102,6 +109,7 @@ import org.hibernate.type.spi.TypeConfiguration; import jakarta.persistence.Tuple; +import jakarta.persistence.metamodel.Type; import org.checkerframework.checker.nullness.qual.Nullable; import static java.util.stream.Collectors.toList; @@ -168,38 +176,66 @@ public static ModelPartContainer getTargetMappingIfNeeded( SqmPath sqmPath, ModelPartContainer modelPartContainer, SqmToSqlAstConverter sqlAstCreationState) { - final SqmQueryPart queryPart = sqlAstCreationState.getCurrentSqmQueryPart(); - if ( queryPart != null ) { - // We only need to do this for queries - final Clause clause = sqlAstCreationState.getCurrentClauseStack().getCurrent(); - if ( clause != Clause.FROM && modelPartContainer.getPartMappingType() != modelPartContainer && sqmPath.getLhs() instanceof SqmFrom ) { - final ModelPart modelPart; - if ( modelPartContainer instanceof PluralAttributeMapping ) { - modelPart = getCollectionPart( - (PluralAttributeMapping) modelPartContainer, - castNonNull( sqmPath.getNavigablePath().getParent() ) - ); - } - else { - modelPart = modelPartContainer; - } - if ( modelPart instanceof EntityAssociationMapping ) { - final EntityAssociationMapping association = (EntityAssociationMapping) modelPart; - // If the path is one of the association's target key properties, - // we need to render the target side if in group/order by - if ( association.getTargetKeyPropertyNames().contains( sqmPath.getReferencedPathSource().getPathName() ) - && ( clause == Clause.GROUP || clause == Clause.ORDER - || !isFkOptimizationAllowed( sqmPath.getLhs() ) - || queryPart.getFirstQuerySpec().groupByClauseContains( sqmPath.getNavigablePath(), sqlAstCreationState ) - || queryPart.getFirstQuerySpec().orderByClauseContains( sqmPath.getNavigablePath(), sqlAstCreationState ) ) ) { - return association.getAssociatedEntityMappingType(); - } + // We only need to do this for queries + if ( sqlAstCreationState.getCurrentClauseStack().getCurrent() != Clause.FROM + && modelPartContainer.getPartMappingType() != modelPartContainer && sqmPath.getLhs() instanceof SqmFrom ) { + final ModelPart modelPart; + if ( modelPartContainer instanceof PluralAttributeMapping ) { + modelPart = getCollectionPart( + (PluralAttributeMapping) modelPartContainer, + castNonNull( sqmPath.getNavigablePath().getParent() ) + ); + } + else { + modelPart = modelPartContainer; + } + if ( modelPart instanceof EntityAssociationMapping ) { + final EntityAssociationMapping association = (EntityAssociationMapping) modelPart; + if ( shouldRenderTargetSide( sqmPath, association, sqlAstCreationState ) ) { + return association.getAssociatedEntityMappingType(); } } } return modelPartContainer; } + private static boolean shouldRenderTargetSide( + SqmPath sqmPath, + EntityAssociationMapping association, + SqmToSqlAstConverter sqlAstCreationState) { + if ( !association.getTargetKeyPropertyNames().contains( sqmPath.getReferencedPathSource().getPathName() ) ) { + return false; + } + // If the path is one of the association's target key properties, + // we need to render the target side if in group/order by + final Clause clause = sqlAstCreationState.getCurrentClauseStack().getCurrent(); + return clause == Clause.GROUP || clause == Clause.ORDER + || !isFkOptimizationAllowed( sqmPath.getLhs(), association ) + || clauseContainsPath( Clause.GROUP, sqmPath, sqlAstCreationState ) + || clauseContainsPath( Clause.ORDER, sqmPath, sqlAstCreationState ); + } + + private static boolean clauseContainsPath( + Clause clauseToCheck, + SqmPath sqmPath, + SqmToSqlAstConverter sqlAstCreationState) { + final Stack queryPartStack = sqlAstCreationState.getSqmQueryPartStack(); + final NavigablePath navigablePath = sqmPath.getNavigablePath(); + final Boolean found = queryPartStack.findCurrentFirst( queryPart -> { + final SqmQuerySpec spec = queryPart.getFirstQuerySpec(); + if ( clauseToCheck == Clause.GROUP && spec.groupByClauseContains( navigablePath, sqlAstCreationState ) ) { + return true; + } + else if ( clauseToCheck == Clause.ORDER && spec.orderByClauseContains( navigablePath, sqlAstCreationState ) ) { + return true; + } + else { + return null; + } + } ); + return Boolean.TRUE.equals( found ); + } + private static CollectionPart getCollectionPart(PluralAttributeMapping attribute, NavigablePath path) { final CollectionPart.Nature nature = CollectionPart.Nature.fromNameExact( path.getLocalName() ); if ( nature != null ) { @@ -208,6 +244,8 @@ private static CollectionPart getCollectionPart(PluralAttributeMapping attribute return attribute.getElementDescriptor(); case INDEX: return attribute.getIndexDescriptor(); + case ID: + return attribute.getIdentifierDescriptor(); } } return null; @@ -218,13 +256,52 @@ private static CollectionPart getCollectionPart(PluralAttributeMapping attribute * a join that cannot be dereferenced through the foreign key on the associated table, * i.e. a join that's neither {@linkplain SqmJoinType#INNER} nor {@linkplain SqmJoinType#LEFT} * or one that has an explicit on clause predicate. + * + * @deprecated Use {@link #isFkOptimizationAllowed(SqmPath, EntityAssociationMapping)} instead */ + @Deprecated(forRemoval = true, since = "6.6.1") public static boolean isFkOptimizationAllowed(SqmPath sqmPath) { if ( sqmPath instanceof SqmJoin ) { final SqmJoin sqmJoin = (SqmJoin) sqmPath; switch ( sqmJoin.getSqmJoinType() ) { + case LEFT: + final EntityAssociationMapping associationMapping = resolveAssociationMapping( sqmJoin ); + if ( associationMapping != null && isFiltered( associationMapping ) ) { + return false; + } + // FallThrough intended case INNER: + return !( sqmJoin instanceof SqmQualifiedJoin) + || ( (SqmQualifiedJoin) sqmJoin ).getJoinPredicate() == null; + default: + return false; + } + } + return false; + } + + /** + * Utility that returns {@code false} when the provided {@link SqmPath sqmPath} is + * a join that cannot be dereferenced through the foreign key on the associated table, + * i.e. a join that's neither {@linkplain SqmJoinType#INNER} nor {@linkplain SqmJoinType#LEFT} + * or one that has an explicit on clause predicate. + */ + public static boolean isFkOptimizationAllowed(SqmPath sqmPath, EntityAssociationMapping associationMapping) { + // By default, never allow the FK optimization if the path is a join, unless the association has a join table + // Hibernate ORM has no way for users to refer to collection/join table rows, + // so referring the columns of these rows by default when requesting FK column attributes is sensible. + // Users that need to refer to the actual target table columns will have to add an explicit entity join. + if ( associationMapping.isFkOptimizationAllowed() + && sqmPath instanceof SqmJoin + && hasJoinTable( associationMapping ) ) { + final SqmJoin sqmJoin = (SqmJoin) sqmPath; + switch ( sqmJoin.getSqmJoinType() ) { case LEFT: + if ( isFiltered( associationMapping ) ) { + return false; + } + // FallThrough intended + case INNER: return !( sqmJoin instanceof SqmQualifiedJoin) || ( (SqmQualifiedJoin) sqmJoin ).getJoinPredicate() == null; default: @@ -234,6 +311,69 @@ public static boolean isFkOptimizationAllowed(SqmPath sqmPath) { return false; } + private static boolean hasJoinTable(EntityAssociationMapping associationMapping) { + if ( associationMapping instanceof CollectionPart ) { + return !( (CollectionPart) associationMapping ).getCollectionAttribute().getCollectionDescriptor().isOneToMany(); + } + else if ( associationMapping instanceof ToOneAttributeMapping ) { + return ( (ToOneAttributeMapping) associationMapping ).hasJoinTable(); + } + return false; + } + + private static boolean isFiltered(EntityAssociationMapping associationMapping) { + final EntityMappingType entityMappingType = associationMapping.getAssociatedEntityMappingType(); + return !associationMapping.isFkOptimizationAllowed() + // When the identifier mappings are different we have a joined subclass entity + // which will filter rows based on a discriminator predicate + || entityMappingType.getIdentifierMappingForJoin() != entityMappingType.getIdentifierMapping(); + } + + private static @Nullable EntityAssociationMapping resolveAssociationMapping(SqmJoin sqmJoin) { + if ( sqmJoin instanceof SqmSingularJoin ) { + final SqmSingularJoin singularJoin = (SqmSingularJoin) sqmJoin; + if ( singularJoin.getAttribute().getSqmPathType() instanceof EntityDomainType ) { + return resolveAssociationMapping( singularJoin ); + } + } + return null; + } + + private static @Nullable EntityAssociationMapping resolveAssociationMapping(SqmSingularJoin sqmJoin) { + SingularPersistentAttribute attribute = sqmJoin.getAttribute(); + ManagedDomainType declaringType = attribute.getDeclaringType(); + if ( declaringType.getPersistenceType() != Type.PersistenceType.ENTITY ) { + final StringBuilder pathBuilder = new StringBuilder(); + do { + if ( pathBuilder.length() > 0 ) { + pathBuilder.insert(0, '.'); + } + pathBuilder.insert( 0, attribute.getName() ); + final SqmFrom lhs = sqmJoin.getLhs(); + if ( !(lhs instanceof SqmSingularJoin ) ) { + return null; + } + sqmJoin = (SqmSingularJoin) lhs; + attribute = sqmJoin.getAttribute(); + declaringType = attribute.getDeclaringType(); + } while (declaringType.getPersistenceType() != Type.PersistenceType.ENTITY ); + pathBuilder.insert(0, '.'); + pathBuilder.insert( 0, attribute.getName() ); + final EntityPersister entityDescriptor = sqmJoin.nodeBuilder() + .getSessionFactory() + .getMappingMetamodel() + .getEntityDescriptor( ( (EntityDomainType) declaringType ).getHibernateEntityName() ); + return (EntityAssociationMapping) entityDescriptor.findByPath( pathBuilder.toString() ); + } + else { + final EntityPersister entityDescriptor = sqmJoin.nodeBuilder() + .getSessionFactory() + .getMappingMetamodel() + .getEntityDescriptor( ( (EntityDomainType) declaringType ).getHibernateEntityName() ); + return (EntityAssociationMapping) entityDescriptor.findAttributeMapping( attribute.getName() ); + } + } + public static List getWhereClauseNavigablePaths(SqmQuerySpec querySpec) { final SqmWhereClause where = querySpec.getWhereClause(); if ( where == null || where.getPredicate() == null ) { @@ -287,7 +427,7 @@ public static SqmAttributeJoin findCompatibleFetchJoin( SqmPathSource pathSource, SqmJoinType requestedJoinType) { for ( final SqmJoin join : sqmFrom.getSqmJoins() ) { - if ( join.getReferencedPathSource() == pathSource ) { + if ( join.getModel() == pathSource ) { final SqmAttributeJoin attributeJoin = (SqmAttributeJoin) join; if ( attributeJoin.isFetched() ) { final SqmJoinType joinType = join.getSqmJoinType(); @@ -762,10 +902,16 @@ static JpaOrder sortSpecification(SqmSelectStatement sqm, Order order) { } public static boolean isSelectionAssignableToResultType(SqmSelection selection, Class expectedResultType) { - if ( expectedResultType == null - || selection != null && selection.getSelectableNode() instanceof SqmParameter ) { + if ( expectedResultType == null ) { return true; } + else if ( selection != null && selection.getSelectableNode() instanceof SqmParameter ) { + final SqmParameter sqmParameter = (SqmParameter) selection.getSelectableNode(); + final Class anticipatedClass = sqmParameter.getAnticipatedType() != null ? + sqmParameter.getAnticipatedType().getBindableJavaType() : + null; + return anticipatedClass != null && expectedResultType.isAssignableFrom( anticipatedClass ); + } else if ( selection == null || !isHqlTuple( selection ) && selection.getSelectableNode().isCompoundSelection() ) { return false; @@ -1030,7 +1176,10 @@ protected static void verifyResultType(Class resultClass, @Nullable SqmExpres } final JavaType selectionExpressibleJavaType = selectionExpressible.getExpressibleJavaType(); - assert selectionExpressibleJavaType != null; + if ( selectionExpressibleJavaType == null ) { + // nothing we can validate + return; + } final Class selectionExpressibleJavaTypeClass = selectionExpressibleJavaType.getJavaTypeClass(); if ( selectionExpressibleJavaTypeClass == Object.class ) { diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/TypecheckUtil.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/TypecheckUtil.java index d44a614cf9a1..1fab71f008b2 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/TypecheckUtil.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/TypecheckUtil.java @@ -443,8 +443,8 @@ public static void assertAssignable( } public static void assertOperable(SqmExpression left, SqmExpression right, BinaryArithmeticOperator op) { - final SqmExpressible leftNodeType = left.getNodeType(); - final SqmExpressible rightNodeType = right.getNodeType(); + final SqmExpressible leftNodeType = left.getExpressible(); + final SqmExpressible rightNodeType = right.getExpressible(); if ( leftNodeType != null && rightNodeType != null ) { final Class leftJavaType = leftNodeType.getRelationalJavaType().getJavaTypeClass(); final Class rightJavaType = rightNodeType.getRelationalJavaType().getJavaTypeClass(); @@ -579,7 +579,7 @@ public static void assertDuration(SqmExpression expression) { } public static void assertNumeric(SqmExpression expression, UnaryArithmeticOperator op) { - final SqmExpressible nodeType = expression.getNodeType(); + final SqmExpressible nodeType = expression.getExpressible(); if ( nodeType != null ) { final DomainType domainType = nodeType.getSqmType(); if ( !( domainType instanceof JdbcMapping ) || !( (JdbcMapping) domainType ).getJdbcType().isNumber() ) { diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteUpdateHandler.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteUpdateHandler.java index 1e4a3f598277..e2428b65283b 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteUpdateHandler.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteUpdateHandler.java @@ -157,8 +157,8 @@ protected void addDmlCtes( tableExpression, true ); - final List assignmentList = assignmentsByTable.get( updatingTableReference ); - if ( assignmentList == null ) { + final List assignmentsForInsert = assignmentsByTable.get( updatingTableReference ); + if ( assignmentsForInsert == null ) { continue; } final String insertCteTableName = getInsertCteTableName( tableExpression ); @@ -233,7 +233,7 @@ protected void addDmlCtes( // Collect the target column references from the key expressions final List targetColumnReferences = new ArrayList<>( existsKeyColumns ); // And transform assignments to target column references and selections - for ( Assignment assignment : assignments ) { + for ( Assignment assignment : assignmentsForInsert ) { targetColumnReferences.addAll( assignment.getAssignable().getColumnReferences() ); querySpec.getSelectClause().addSqlSelection( new SqlSelectionImpl( diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/FunctionReturnTypeResolver.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/FunctionReturnTypeResolver.java index 8fb1fa934e04..cce579ac9205 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/FunctionReturnTypeResolver.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/FunctionReturnTypeResolver.java @@ -43,7 +43,7 @@ public interface FunctionReturnTypeResolver { ReturnableType impliedType, List> arguments, TypeConfiguration typeConfiguration) { - throw new UnsupportedOperationException( "Not implemented for " + getClass().getName() ); + return resolveFunctionReturnType( impliedType, new FakeSqmToSqlAstConverter( null ), arguments, typeConfiguration ); } /** diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/StandardFunctionReturnTypeResolvers.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/StandardFunctionReturnTypeResolvers.java index ec941199417f..eca56d466d02 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/StandardFunctionReturnTypeResolvers.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/StandardFunctionReturnTypeResolvers.java @@ -241,13 +241,8 @@ else if ( !(specifiedArgType instanceof ReturnableType) ) { } private static SqmExpressible getArgumentExpressible(SqmTypedNode specifiedArgument) { - final SqmExpressible expressible = specifiedArgument.getNodeType(); - final SqmExpressible specifiedArgType = expressible instanceof SqmTypedNode - ? ( (SqmTypedNode) expressible ).getNodeType() - : expressible; - return specifiedArgType instanceof SqmPathSource - ? ( (SqmPathSource) specifiedArgType ).getSqmPathType() - : specifiedArgType; + final SqmExpressible expressible = specifiedArgument.getExpressible(); + return expressible != null ? expressible.getSqmType() : null; } public static JdbcMapping extractArgumentJdbcMapping( diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java index d52065febb34..30ff86238ce9 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java @@ -6,6 +6,7 @@ */ package org.hibernate.query.sqm.sql; +import jakarta.annotation.Nullable; import jakarta.persistence.TemporalType; import jakarta.persistence.metamodel.SingularAttribute; import jakarta.persistence.metamodel.Type; @@ -477,7 +478,7 @@ public abstract class BaseSqmToSqlAstConverter extends Base private boolean deduplicateSelectionItems; private ForeignKeyDescriptor.Nature currentlyResolvingForeignKeySide; private SqmStatement currentSqmStatement; - private SqmQueryPart currentSqmQueryPart; + private Stack sqmQueryPartStack = new StandardStack<>( SqmQueryPart.class ); private CteContainer cteContainer; /** * A map from {@link SqmCteTable#getCteName()} to the final SQL name. @@ -783,9 +784,10 @@ public Stack getCurrentClauseStack() { return currentClauseStack; } + @SuppressWarnings("rawtypes") @Override - public SqmQueryPart getCurrentSqmQueryPart() { - return currentSqmQueryPart; + public Stack getSqmQueryPartStack() { + return sqmQueryPartStack; } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -1269,11 +1271,6 @@ public InsertStatement visitInsertValuesStatement(SqmInsertValuesStatement sq this ); - if ( !rootTableGroup.getTableReferenceJoins().isEmpty() - || !rootTableGroup.getTableGroupJoins().isEmpty() ) { - throw new HibernateException( "Not expecting multiple table references for an SQM INSERT-SELECT" ); - } - getFromClauseAccess().registerTableGroup( rootPath, rootTableGroup ); final InsertSelectStatement insertStatement = new InsertSelectStatement( @@ -1289,9 +1286,8 @@ public InsertStatement visitInsertValuesStatement(SqmInsertValuesStatement sq rootTableGroup ); - if ( !rootTableGroup.getTableReferenceJoins().isEmpty() - || !rootTableGroup.getTableGroupJoins().isEmpty() ) { - throw new SemanticException( "Not expecting multiple table references for an SQM INSERT-SELECT" ); + if ( hasJoins( rootTableGroup ) ) { + throw new HibernateException( "Not expecting multiple table references for an SQM INSERT-SELECT" ); } for ( SqmValues sqmValues : sqmStatement.getValuesList() ) { @@ -1734,8 +1730,7 @@ public CteStatement visitCteStatement(SqmCteStatement sqmCteStatement) { ); final DelegatingSqmAliasedNodeCollector collector = (DelegatingSqmAliasedNodeCollector) processingState .getSqlExpressionResolver(); - final SqmQueryPart oldSqmQueryPart = currentSqmQueryPart; - currentSqmQueryPart = queryGroup; + sqmQueryPartStack.push( queryGroup ); pushProcessingState( processingState ); try { @@ -1779,7 +1774,7 @@ public CteStatement visitCteStatement(SqmCteStatement sqmCteStatement) { } finally { popProcessingStateStack(); - currentSqmQueryPart = oldSqmQueryPart; + sqmQueryPartStack.pop(); } } finally { @@ -1992,13 +1987,16 @@ public QueryGroup visitQueryGroup(SqmQueryGroup queryGroup) { ); final DelegatingSqmAliasedNodeCollector collector = (DelegatingSqmAliasedNodeCollector) processingState .getSqlExpressionResolver(); - final SqmQueryPart sqmQueryPart = currentSqmQueryPart; - currentSqmQueryPart = queryGroup; + sqmQueryPartStack.push( queryGroup ); pushProcessingState( processingState ); + FromClauseIndex firstQueryPartIndex = null; + SqlAstProcessingState firstPoppedProcessingState = null; try { newQueryParts.add( visitQueryPart( queryParts.get( 0 ) ) ); + firstQueryPartIndex = lastPoppedFromClauseIndex; + firstPoppedProcessingState = lastPoppedProcessingState; collector.setSqmAliasedNodeCollector( (SqmAliasedNodeCollector) lastPoppedProcessingState.getSqlExpressionResolver() ); @@ -2015,7 +2013,9 @@ public QueryGroup visitQueryGroup(SqmQueryGroup queryGroup) { } finally { popProcessingStateStack(); - currentSqmQueryPart = sqmQueryPart; + sqmQueryPartStack.pop(); + lastPoppedFromClauseIndex = firstQueryPartIndex; + lastPoppedProcessingState = firstPoppedProcessingState; } } @@ -2075,9 +2075,8 @@ else if ( sqmQuerySpec.hasPositionalGroupItem() ) { ); } - final SqmQueryPart sqmQueryPart = currentSqmQueryPart; final boolean originalDeduplicateSelectionItems = deduplicateSelectionItems; - currentSqmQueryPart = sqmQuerySpec; + sqmQueryPartStack.push( sqmQuerySpec ); // In sub-queries, we can never deduplicate the selection items as that might change semantics deduplicateSelectionItems = false; pushProcessingState( processingState ); @@ -2144,7 +2143,7 @@ else if ( sqmQuerySpec.hasPositionalGroupItem() ) { inNestedContext = oldInNestedContext; popProcessingStateStack(); queryTransformers.pop(); - currentSqmQueryPart = sqmQueryPart; + sqmQueryPartStack.pop(); deduplicateSelectionItems = originalDeduplicateSelectionItems; } } @@ -2210,7 +2209,7 @@ public SelectClause visitSelectClause(SqmSelectClause selectClause) { try { final SelectClause sqlSelectClause = currentQuerySpec().getSelectClause(); if ( selectClause == null ) { - final SqmFrom implicitSelection = determineImplicitSelection( (SqmQuerySpec) currentSqmQueryPart ); + final SqmFrom implicitSelection = determineImplicitSelection( (SqmQuerySpec) getCurrentSqmQueryPart() ); visitSelection( 0, new SqmSelection<>( implicitSelection, implicitSelection.nodeBuilder() ) ); } else { @@ -2235,7 +2234,7 @@ public SelectClause visitSelectClause(SqmSelectClause selectClause) { @Override public Void visitSelection(SqmSelection sqmSelection) { return visitSelection( - currentSqmQueryPart.getFirstQuerySpec().getSelectClause().getSelections().indexOf( sqmSelection ), + getCurrentSqmQueryPart().getFirstQuerySpec().getSelectClause().getSelections().indexOf( sqmSelection ), sqmSelection ); } @@ -2354,7 +2353,7 @@ else if ( statement.getQuerySource() == SqmQuerySource.CRITERIA && currentClause // To avoid this issue, we determine the position and let the SqlAstTranslator handle the rest. // Usually it will render `select ?, count(*) from dual group by 1` if supported // or force rendering the parameter as literal instead so that the database can see the grouping is fine - final SqmQuerySpec querySpec = currentSqmQueryPart.getFirstQuerySpec(); + final SqmQuerySpec querySpec = getCurrentSqmQueryPart().getFirstQuerySpec(); sqmPosition = indexOfExpression( querySpec.getSelectClause().getSelections(), groupByClauseExpression ); path = null; } @@ -2382,7 +2381,7 @@ else if ( statement.getQuerySource() == SqmQuerySource.CRITERIA && currentClause continue OUTER; } } - if ( currentSqmQueryPart instanceof SqmQueryGroup ) { + if ( getCurrentSqmQueryPart() instanceof SqmQueryGroup ) { // Reusing the SqlSelection for query groups would be wrong because the aliases do no exist // So we have to use a literal expression in a new SqlSelection instance to refer to the position expressions.add( @@ -3021,10 +3020,20 @@ private void registerPathAttributeEntityNameUsage(SqmPath sqmPath, TableGroup parentType.getRootEntityDescriptor().getEntityName() ); } + final EntityDiscriminatorMapping discriminator = parentType.getDiscriminatorMapping(); + final String entityName; + if ( discriminator != null && discriminator.hasPhysicalColumn() && !parentType.getSubMappingTypes().isEmpty() ) { + // This is needed to preserve optimization for joined + discriminator inheritance + // see JoinedSubclassEntityPersister#getIdentifierMappingForJoin + entityName = parentType.getRootEntityDescriptor().getEntityName(); + } + else { + entityName = parentType.getEntityName(); + } registerEntityNameUsage( tableGroup, EntityNameUse.EXPRESSION, - parentType.getEntityName() + entityName ); } else { @@ -3161,10 +3170,6 @@ else if ( useKind == EntityNameUse.UseKind.PROJECTION ) { subType.getEntityName(), (s, existingUse) -> finalEntityNameUse.stronger( existingUse ) ); - actualTableGroup.resolveTableReference( - null, - subType.getEntityPersister().getMappedTableDetails().getTableName() - ); } } } @@ -3925,7 +3930,7 @@ private TableGroup createTableGroup(TableGroup parentTableGroup, SqmPath join this ); // Implicit joins in the ON clause need to be added as nested table group joins - final boolean nested = currentClauseStack.getCurrent() == Clause.FROM; + final boolean nested = currentlyProcessingJoin != null; if ( nested ) { parentTableGroup.addNestedTableGroupJoin( tableGroupJoin ); } @@ -3999,6 +4004,13 @@ private void registerPluralTableGroupParts(NavigablePath navigablePath, TableGro } } + @Override + public @Nullable SqlAstJoinType getCurrentlyProcessingJoinType() { + return currentlyProcessingJoin == null + ? null + : currentlyProcessingJoin.getSqmJoinType().getCorrespondingSqlJoinType(); + } + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // SqmPath handling // - Note that SqmFrom references defined in the FROM-clause are already @@ -4128,8 +4140,7 @@ private Expression visitTableGroup(TableGroup tableGroup, SqmPath path) { // When the inferred mapping is null, we try to resolve to the FK by default, which is fine because // expansion to all target columns for select and group by clauses is handled in EntityValuedPathInterpretation if ( entityValuedModelPart instanceof EntityAssociationMapping - && ( (EntityAssociationMapping) entityValuedModelPart ).isFkOptimizationAllowed() - && isFkOptimizationAllowed( path ) ) { + && isFkOptimizationAllowed( path, (EntityAssociationMapping) entityValuedModelPart ) ) { // If the table group uses an association mapping that is not a one-to-many, // we make use of the FK model part - unless the path is a non-optimizable join, // for which we should always use the target's identifier to preserve semantics @@ -5612,40 +5623,7 @@ else if ( inferableExpressible instanceof BasicValuedMapping ) { final BasicValuedMapping basicValuedMapping = (BasicValuedMapping) inferableExpressible; final BasicValueConverter valueConverter = basicValuedMapping.getJdbcMapping().getValueConverter(); if ( valueConverter != null ) { - final Object value = literal.getLiteralValue(); - final Object sqlLiteralValue; - // For converted query literals, we support both, the domain and relational java type - if ( value == null || valueConverter.getDomainJavaType().isInstance( value ) ) { - sqlLiteralValue = valueConverter.toRelationalValue( value ); - } - else if ( valueConverter.getRelationalJavaType().isInstance( value ) ) { - sqlLiteralValue = value; - } - else if ( Character.class.isAssignableFrom( valueConverter.getRelationalJavaType().getJavaTypeClass() ) - && value instanceof CharSequence && ( (CharSequence) value ).length() == 1 ) { - sqlLiteralValue = ( (CharSequence) value ).charAt( 0 ); - } - // In HQL, number literals might not match the relational java type exactly, - // so we allow coercion between the number types - else if ( Number.class.isAssignableFrom( valueConverter.getRelationalJavaType().getJavaTypeClass() ) - && value instanceof Number ) { - sqlLiteralValue = valueConverter.getRelationalJavaType().coerce( - value, - creationContext.getSessionFactory()::getTypeConfiguration - ); - } - else { - throw new SemanticException( - String.format( - Locale.ROOT, - "Literal type '%s' did not match domain type '%s' nor converted type '%s'", - value.getClass(), - valueConverter.getDomainJavaType().getJavaTypeClass().getName(), - valueConverter.getRelationalJavaType().getJavaTypeClass().getName() - ) - ); - } - return new QueryLiteral<>( sqlLiteralValue, basicValuedMapping ); + return new QueryLiteral<>( sqlLiteralValue( valueConverter, literal.getLiteralValue() ), basicValuedMapping ); } } @@ -5762,6 +5740,40 @@ else if ( expressible instanceof EntityValuedModelPart ) { } } + private Object sqlLiteralValue(BasicValueConverter valueConverter, D value) { + // For converted query literals, we support both, the domain and relational java type + if ( value == null || valueConverter.getDomainJavaType().isInstance( value ) ) { + return valueConverter.toRelationalValue( value ); + } + else if ( valueConverter.getRelationalJavaType().isInstance( value ) ) { + return value; + } + else if ( Character.class.isAssignableFrom( valueConverter.getRelationalJavaType().getJavaTypeClass() ) + && value instanceof CharSequence && ( (CharSequence) value ).length() == 1 ) { + return ( (CharSequence) value ).charAt( 0 ); + } + // In HQL, number literals might not match the relational java type exactly, + // so we allow coercion between the number types + else if ( Number.class.isAssignableFrom( valueConverter.getRelationalJavaType().getJavaTypeClass() ) + && value instanceof Number ) { + return valueConverter.getRelationalJavaType().coerce( + value, + creationContext.getSessionFactory()::getTypeConfiguration + ); + } + else { + throw new SemanticException( + String.format( + Locale.ROOT, + "Literal type '%s' did not match domain type '%s' nor converted type '%s'", + value.getClass(), + valueConverter.getDomainJavaType().getJavaTypeClass().getName(), + valueConverter.getRelationalJavaType().getJavaTypeClass().getName() + ) + ); + } + } + @Override public Expression visitHqlNumericLiteral(SqmHqlNumericLiteral numericLiteral) { final BasicValuedMapping inferredExpressible = (BasicValuedMapping) getInferredValueMapping(); @@ -6280,7 +6292,14 @@ else if ( bindable instanceof SelectableMapping ) { final QueryParameterBinding binding = domainParameterBindings.getBinding( domainParameterXref.getQueryParameter( expression ) ); - final Object bindValue = binding.getBindValue(); + final Object bindValue; + if ( binding.isMultiValued() ) { + final Collection bindValues = binding.getBindValues(); + bindValue = !bindValues.isEmpty() ? bindValues.iterator().next() : null; + } + else { + bindValue = binding.getBindValue(); + } if ( bindValue != null ) { if ( bindValue instanceof BigInteger ) { int precision = bindValue.toString().length() - ( ( (BigInteger) bindValue ).signum() < 0 ? 1 : 0 ); @@ -7118,6 +7137,8 @@ public CaseSimpleExpression visitSimpleCaseExpression(SqmCaseSimple expres inferrableTypeAccessStack.push( () -> fixtureType ); final Expression checkValue = (Expression) whenFragment.getCheckValue().accept( this ); inferrableTypeAccessStack.pop(); + handleTypeInCaseExpression( fixture, checkValue ); + final MappingModelExpressible alreadyKnown = resolved; inferrableTypeAccessStack.push( () -> alreadyKnown == null && inferenceSupplier != null ? inferenceSupplier.get() : alreadyKnown @@ -7153,6 +7174,42 @@ public CaseSimpleExpression visitSimpleCaseExpression(SqmCaseSimple expres ); } + private void handleTypeInCaseExpression(Expression fixture, Expression checkValue) { + if ( fixture instanceof DiscriminatorPathInterpretation ) { + final DiscriminatorPathInterpretation typeExpression = (DiscriminatorPathInterpretation) fixture; + final TableGroup tableGroup = getFromClauseIndex().getTableGroup( typeExpression.getNavigablePath().getParent() ); + final MappingType partMappingType = tableGroup.getModelPart().getPartMappingType(); + if ( !(partMappingType instanceof EntityMappingType) ) { + return; + } + final EntityMappingType entityMappingType = (EntityMappingType) partMappingType; + if ( entityMappingType.getDiscriminatorMapping().hasPhysicalColumn() ) { + // If the entity type has a physical type column we only need to register an expression + // usage for the root type to prevent pruning the table where the discriminator is found + registerEntityNameUsage( + tableGroup, + EntityNameUse.EXPRESSION, + entityMappingType.getRootEntityDescriptor().getEntityName() + ); + } + else if ( checkValue instanceof EntityTypeLiteral ) { + // Register an expression type usage for the literal subtype to prevent pruning its table group + registerEntityNameUsage( + tableGroup, + EntityNameUse.EXPRESSION, + ( (EntityTypeLiteral) checkValue ).getEntityTypeDescriptor().getEntityName() + ); + } + else { + // We have to assume all types are possible and can't do optimizations + registerEntityNameUsage( tableGroup, EntityNameUse.EXPRESSION, entityMappingType.getEntityName() ); + for ( EntityMappingType subMappingType : entityMappingType.getSubMappingTypes() ) { + registerEntityNameUsage( tableGroup, EntityNameUse.EXPRESSION, subMappingType.getEntityName() ); + } + } + } + } + @Override public CaseSearchedExpression visitSearchedCaseExpression(SqmCaseSearched expression) { final List whenFragments = new ArrayList<>( expression.getWhenFragments().size() ); @@ -7338,12 +7395,13 @@ private static > QueryLiteral queryLiteral( ); } + @SuppressWarnings({"unchecked", "rawtypes"}) @Override public Object visitFieldLiteral(SqmFieldLiteral sqmFieldLiteral) { - return new QueryLiteral<>( - sqmFieldLiteral.getValue(), - (BasicValuedMapping) determineValueMapping( sqmFieldLiteral ) - ); + final BasicValuedMapping valueMapping = (BasicValuedMapping) determineValueMapping( sqmFieldLiteral ); + final Object value = sqmFieldLiteral.getValue(); + final BasicValueConverter converter = valueMapping.getJdbcMapping().getValueConverter(); + return new QueryLiteral<>( converter != null ? sqlLiteralValue( converter, value ) : value, valueMapping ); } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -7407,13 +7465,7 @@ public Junction visitJunctionPredicate(SqmJunctionPredicate predicate) { new ArrayList<>( predicate.getPredicates().size() ), getBooleanType() ); - final Map> previousTableGroupEntityNameUses; - if ( tableGroupEntityNameUses.isEmpty() ) { - previousTableGroupEntityNameUses = null; - } - else { - previousTableGroupEntityNameUses = new IdentityHashMap<>( tableGroupEntityNameUses ); - } + final Map> previousTableGroupEntityNameUses = new IdentityHashMap<>( tableGroupEntityNameUses ); Map>[] disjunctEntityNameUsesArray = null; Map> entityNameUsesToPropagate = null; List treatedTableGroups = null; @@ -7425,9 +7477,7 @@ public Junction visitJunctionPredicate(SqmJunctionPredicate predicate) { if ( !tableGroupEntityNameUses.isEmpty() ) { if ( disjunctEntityNameUsesArray == null ) { disjunctEntityNameUsesArray = new Map[predicate.getPredicates().size()]; - entityNameUsesToPropagate = previousTableGroupEntityNameUses == null - ? new IdentityHashMap<>() - : new IdentityHashMap<>( previousTableGroupEntityNameUses ); + entityNameUsesToPropagate = new IdentityHashMap<>( previousTableGroupEntityNameUses ); } if ( i == 0 ) { // Collect the table groups for which filters are registered @@ -7465,7 +7515,9 @@ public Junction visitJunctionPredicate(SqmJunctionPredicate predicate) { // If every disjunct contains a FILTER, we can merge the filters // If every disjunct contains a TREAT, we can merge the treats // Otherwise, we downgrade the entity name uses to expression uses - for ( Map.Entry> entry : tableGroupEntityNameUses.entrySet() ) { + final Iterator>> iterator = tableGroupEntityNameUses.entrySet().iterator(); + while ( iterator.hasNext() ) { + final Map.Entry> entry = iterator.next(); final TableGroup tableGroup = entry.getKey(); final Map entityNameUses = entityNameUsesToPropagate.computeIfAbsent( tableGroup, @@ -7473,10 +7525,22 @@ public Junction visitJunctionPredicate(SqmJunctionPredicate predicate) { ); final boolean downgradeTreatUses; final boolean downgradeFilterUses; - if ( i == 0 ) { - // Never downgrade the treat uses of the first disjunct + if ( getFromClauseAccess().findTableGroup( tableGroup.getNavigablePath() ) == null ) { + // Always preserver name uses for table groups not found in the current from clause index + previousTableGroupEntityNameUses.put( tableGroup, entry.getValue() ); + // Remove from the current junction context since no more processing is required + if ( treatedTableGroups != null ) { + treatedTableGroups.remove( tableGroup ); + } + if ( filteredTableGroups != null ) { + filteredTableGroups.remove( tableGroup ); + } + iterator.remove(); + continue; + } + else if ( i == 0 ) { + // Never downgrade treat or filter uses of the first disjunct downgradeTreatUses = false; - // Never downgrade the filter uses of the first disjunct downgradeFilterUses = false; } else { @@ -7563,9 +7627,7 @@ else if ( useKind == EntityNameUse.UseKind.FILTER ) { } } if ( disjunctEntityNameUsesArray == null ) { - if ( previousTableGroupEntityNameUses != null ) { - tableGroupEntityNameUses.putAll( previousTableGroupEntityNameUses ); - } + tableGroupEntityNameUses.putAll( previousTableGroupEntityNameUses ); return disjunction; } @@ -7636,9 +7698,7 @@ else if ( useKind == EntityNameUse.UseKind.FILTER ) { // Restore the parent context entity name uses state tableGroupEntityNameUses.clear(); - if ( previousTableGroupEntityNameUses != null ) { - tableGroupEntityNameUses.putAll( previousTableGroupEntityNameUses ); - } + tableGroupEntityNameUses.putAll( previousTableGroupEntityNameUses ); // Propagate the union of the entity name uses upwards for ( Map.Entry> entry : entityNameUsesToPropagate.entrySet() ) { final Map entityNameUses = tableGroupEntityNameUses.putIfAbsent( @@ -8326,6 +8386,15 @@ private Fetch createFetch(FetchParent fetchParent, Fetchable fetchable, Boolean joined = true; alias = fetchedJoin.getExplicitAlias(); explicitFetch = true; + + if ( entityGraphTraversalState != null ) { + // Still do traverse the entity graph even if we encounter a fetch join + traversalResult = entityGraphTraversalState.traverse( + fetchParent, + fetchable, + isKeyFetchable + ); + } } else { fetchablePath = resolvedNavigablePath; @@ -8404,7 +8473,7 @@ else if ( getLoadQueryInfluencers().hasEnabledFetchProfiles() ) { final TableGroupJoinProducer joinProducer = (TableGroupJoinProducer) fetchable; final TableGroup compatibleTableGroup = lhs.findCompatibleJoinedGroup( joinProducer, - joinProducer.determineSqlJoinType( lhs, null, true ) + joinProducer.getDefaultSqlAstJoinType( lhs ) ); final SqmQueryPart queryPart = getCurrentSqmQueryPart(); if ( compatibleTableGroup == null @@ -8425,6 +8494,16 @@ else if ( getLoadQueryInfluencers().hasEnabledFetchProfiles() ) { } else { tableGroup = compatibleTableGroup; + + if ( joinProducer instanceof PluralAttributeMapping ) { + final PluralAttributeMapping attributeMapping = (PluralAttributeMapping) joinProducer; + if ( attributeMapping.getOrderByFragment() != null ) { + applyOrdering( tableGroup, attributeMapping.getOrderByFragment() ); + } + if ( attributeMapping.getManyToManyOrderByFragment() != null ) { + applyOrdering( tableGroup, attributeMapping.getManyToManyOrderByFragment() ); + } + } } // and return the joined group @@ -8535,14 +8614,6 @@ public R withNestedFetchParent(FetchParent fetchParent, Function(); + @Override + public void applyOrdering(TableGroup tableGroup, OrderByFragment orderByFragment) { + if ( currentQuerySpec().isRoot() ) { + if ( orderByFragments == null ) { + orderByFragments = new ArrayList<>(); + } + orderByFragments.add( new AbstractMap.SimpleEntry<>( orderByFragment, tableGroup ) ); } - orderByFragments.add( new AbstractMap.SimpleEntry<>( orderByFragment, tableGroup ) ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/FakeSqmToSqlAstConverter.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/FakeSqmToSqlAstConverter.java index 20065e26dc77..d997c0f7c3f9 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/FakeSqmToSqlAstConverter.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/FakeSqmToSqlAstConverter.java @@ -21,6 +21,7 @@ import org.hibernate.query.sqm.tree.predicate.SqmPredicate; import org.hibernate.query.sqm.tree.select.SqmQueryPart; import org.hibernate.sql.ast.Clause; +import org.hibernate.sql.ast.SqlAstJoinType; import org.hibernate.sql.ast.spi.FromClauseAccess; import org.hibernate.sql.ast.spi.SqlAliasBaseGenerator; import org.hibernate.sql.ast.spi.SqlAstCreationContext; @@ -31,6 +32,8 @@ import org.hibernate.sql.ast.tree.expression.QueryTransformer; import org.hibernate.sql.ast.tree.predicate.Predicate; +import jakarta.annotation.Nullable; + /** * */ @@ -92,6 +95,11 @@ public Stack getCurrentClauseStack() { return null; } + @Override + public Stack getSqmQueryPartStack() { + return null; + } + @Override public SqmQueryPart getCurrentSqmQueryPart() { return null; @@ -101,6 +109,11 @@ public SqmQueryPart getCurrentSqmQueryPart() { public void registerQueryTransformer(QueryTransformer transformer) { } + @Override + public @Nullable SqlAstJoinType getCurrentlyProcessingJoinType() { + return null; + } + @Override public boolean isInTypeInference() { return false; diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/SqmToSqlAstConverter.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/SqmToSqlAstConverter.java index f5fc0caea370..3442b0958398 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/SqmToSqlAstConverter.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/SqmToSqlAstConverter.java @@ -18,6 +18,7 @@ import org.hibernate.query.sqm.tree.expression.SqmParameter; import org.hibernate.query.sqm.tree.predicate.SqmPredicate; import org.hibernate.query.sqm.tree.select.SqmQueryPart; +import org.hibernate.sql.ast.SqlAstJoinType; import org.hibernate.sql.ast.spi.SqlAstCreationState; import org.hibernate.sql.ast.Clause; import org.hibernate.sql.ast.tree.expression.Expression; @@ -34,10 +35,20 @@ public interface SqmToSqlAstConverter extends SemanticQueryWalker, SqlAstCreationState { Stack getCurrentClauseStack(); - SqmQueryPart getCurrentSqmQueryPart(); + Stack getSqmQueryPartStack(); + + default SqmQueryPart getCurrentSqmQueryPart() { + return getSqmQueryPartStack().getCurrent(); + } void registerQueryTransformer(QueryTransformer transformer); + /** + * Returns the {@link SqlAstJoinType} of the currently processing join if there is one, or {@code null}. + * This is used to determine the join type for implicit joins happening in the {@code ON} clause. + */ + @Nullable SqlAstJoinType getCurrentlyProcessingJoinType(); + /** * Returns whether the state of the translation is currently in type inference mode. * This is useful to avoid type inference based on other incomplete inference information. diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/SqmCopyContext.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/SqmCopyContext.java index b57278264f01..fa1af21bbe23 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/SqmCopyContext.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/SqmCopyContext.java @@ -7,6 +7,7 @@ package org.hibernate.query.sqm.tree; import org.hibernate.Incubating; +import org.hibernate.query.sqm.SqmQuerySource; import org.hibernate.query.sqm.internal.NoParamSqmCopyContext; import org.hibernate.query.sqm.internal.SimpleSqmCopyContext; @@ -31,11 +32,30 @@ default boolean copyFetchedFlag() { return true; } + /** + * Returns the query source to use for copied queries. + * {@code null} means, that the original query source should be retained. + * + * @since 7.0 + */ + @Incubating + default @Nullable SqmQuerySource getQuerySource() { + return null; + } + static SqmCopyContext simpleContext() { return new SimpleSqmCopyContext(); } + static SqmCopyContext simpleContext(SqmQuerySource querySource) { + return new SimpleSqmCopyContext( querySource ); + } + static SqmCopyContext noParamCopyContext() { return new NoParamSqmCopyContext(); } + + static SqmCopyContext noParamCopyContext(SqmQuerySource querySource) { + return new NoParamSqmCopyContext( querySource ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/delete/SqmDeleteStatement.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/delete/SqmDeleteStatement.java index 0acd3cee0075..c8cb931a1f48 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/delete/SqmDeleteStatement.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/delete/SqmDeleteStatement.java @@ -93,7 +93,7 @@ public SqmDeleteStatement copy(SqmCopyContext context) { this, new SqmDeleteStatement<>( nodeBuilder(), - getQuerySource(), + context.getQuerySource() == null ? getQuerySource() : context.getQuerySource(), copyParameters( context ), copyCteStatements( context ), getTarget().copy( context ) diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmAttributeJoin.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmAttributeJoin.java index 2f7c6989fcd9..d333b2f965cc 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmAttributeJoin.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmAttributeJoin.java @@ -122,7 +122,7 @@ public X accept(SemanticQueryWalker walker) { @Override public PersistentAttribute getAttribute() { //noinspection unchecked - return (PersistentAttribute) getReferencedPathSource(); + return (PersistentAttribute) getModel(); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmFrom.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmFrom.java index 7a4b03a5cb91..235864080672 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmFrom.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmFrom.java @@ -720,9 +720,11 @@ private SqmAttributeJoin buildJoin( SqmPathSource joinedPathSource, SqmJoinType joinType, boolean fetched) { - final SqmAttributeJoin compatibleFetchJoin = findCompatibleFetchJoin( this, joinedPathSource, joinType ); - if ( compatibleFetchJoin != null ) { - return compatibleFetchJoin; + if ( fetched ) { + final SqmAttributeJoin compatibleFetchJoin = findCompatibleFetchJoin( this, joinedPathSource, joinType ); + if ( compatibleFetchJoin != null ) { + return compatibleFetchJoin; + } } final SqmAttributeJoin sqmJoin; diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmPluralValuedSimplePath.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmPluralValuedSimplePath.java index 3ce9b34384db..f370925e78cb 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmPluralValuedSimplePath.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmPluralValuedSimplePath.java @@ -18,23 +18,25 @@ import org.hibernate.query.hql.spi.SqmPathRegistry; import org.hibernate.query.sqm.NodeBuilder; import org.hibernate.query.sqm.SemanticQueryWalker; +import org.hibernate.query.sqm.SqmPathSource; import org.hibernate.query.sqm.tree.SqmCopyContext; import org.hibernate.query.sqm.tree.SqmJoinType; import org.hibernate.query.sqm.tree.expression.SqmExpression; import org.hibernate.query.sqm.tree.from.SqmFrom; +import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.query.sqm.tree.from.SqmQualifiedJoin; /** * An SqmPath for plural attribute paths * - * @param The collection element type, which is the "bindable" type in the SQM tree + * @param The collection type * * @author Steve Ebersole */ -public class SqmPluralValuedSimplePath extends AbstractSqmSimplePath { +public class SqmPluralValuedSimplePath extends AbstractSqmSimplePath { public SqmPluralValuedSimplePath( NavigablePath navigablePath, - PluralPersistentAttribute referencedNavigable, + PluralPersistentAttribute referencedNavigable, SqmPath lhs, NodeBuilder nodeBuilder) { this( navigablePath, referencedNavigable, lhs, null, nodeBuilder ); @@ -42,26 +44,29 @@ public SqmPluralValuedSimplePath( public SqmPluralValuedSimplePath( NavigablePath navigablePath, - PluralPersistentAttribute referencedNavigable, + PluralPersistentAttribute referencedNavigable, SqmPath lhs, String explicitAlias, NodeBuilder nodeBuilder) { - super( navigablePath, referencedNavigable, lhs, explicitAlias, nodeBuilder ); + // We need to do an unchecked cast here: PluralPersistentAttribute implements path source with + // the element type, but paths generated from it must be collection-typed. + //noinspection unchecked + super( navigablePath, (SqmPathSource) referencedNavigable, lhs, explicitAlias, nodeBuilder ); } @Override - public SqmPluralValuedSimplePath copy(SqmCopyContext context) { - final SqmPluralValuedSimplePath existing = context.getCopy( this ); + public SqmPluralValuedSimplePath copy(SqmCopyContext context) { + final SqmPluralValuedSimplePath existing = context.getCopy( this ); if ( existing != null ) { return existing; } final SqmPath lhsCopy = getLhs().copy( context ); - final SqmPluralValuedSimplePath path = context.registerCopy( + final SqmPluralValuedSimplePath path = context.registerCopy( this, new SqmPluralValuedSimplePath<>( getNavigablePathCopy( lhsCopy ), - getModel(), + (PluralPersistentAttribute) getModel(), lhsCopy, getExplicitAlias(), nodeBuilder() @@ -71,19 +76,13 @@ public SqmPluralValuedSimplePath copy(SqmCopyContext context) { return path; } - @Override - public PluralPersistentAttribute getReferencedPathSource() { - return (PluralPersistentAttribute) super.getReferencedPathSource(); - } - - @Override - public PluralPersistentAttribute getModel() { - return (PluralPersistentAttribute) super.getModel(); + public PluralPersistentAttribute getPluralAttribute() { + return (PluralPersistentAttribute) getModel(); } @Override - public PluralPersistentAttribute getNodeType() { - return getReferencedPathSource(); + public JavaType getJavaTypeDescriptor() { + return getPluralAttribute().getAttributeJavaType(); } @Override @@ -125,12 +124,11 @@ public SqmPath resolveIndexedAccess( } SqmFrom path = pathRegistry.findFromByPath( navigablePath.getParent() ); if ( path == null ) { - final PluralPersistentAttribute referencedPathSource = getReferencedPathSource(); + final SqmPathSource referencedPathSource = getReferencedPathSource(); final SqmFrom parent = pathRegistry.resolveFrom( getLhs() ); final SqmQualifiedJoin join; final SqmExpression index; if ( referencedPathSource instanceof ListPersistentAttribute ) { - //noinspection unchecked join = new SqmListJoin<>( parent, (ListPersistentAttribute) referencedPathSource, @@ -142,7 +140,6 @@ public SqmPath resolveIndexedAccess( index = ( (SqmListJoin) join ).index(); } else if ( referencedPathSource instanceof MapPersistentAttribute ) { - //noinspection unchecked join = new SqmMapJoin<>( parent, (MapPersistentAttribute) referencedPathSource, @@ -171,38 +168,17 @@ else if ( referencedPathSource instanceof MapPersistentAttribute ) { } @Override - public SqmExpression> type() { + public SqmExpression> type() { throw new UnsupportedOperationException( "Cannot access the type of plural valued simple paths" ); } @Override - public SqmTreatedPath treatAs(Class treatJavaType) throws PathException { + public SqmTreatedPath treatAs(Class treatJavaType) throws PathException { throw new UnsupportedOperationException( "Cannot treat plural valued simple paths" ); } @Override - public SqmTreatedEntityValuedSimplePath treatAs(EntityDomainType treatTarget) throws PathException { + public SqmTreatedEntityValuedSimplePath treatAs(EntityDomainType treatTarget) throws PathException { throw new UnsupportedOperationException( "Cannot treat plural valued simple paths" ); } - -// @Override -// public DomainResult createDomainResult( -// String resultVariable, -// DomainResultCreationState creationState, -// DomainResultCreationContext creationContext) { -// return new CollectionResultImpl( -// getReferencedNavigable().getPluralAttribute().getDescribedAttribute(), -// getNavigablePath(), -// resultVariable, -// LockMode.NONE, -// getReferencedNavigable().getPluralAttribute().getCollectionKeyDescriptor().createDomainResult( -// getNavigablePath().append( "{id}" ), -// null, -// creationState, -// creationContext -// ), -// initializerProducerCreator.createProducer( resultVariable, creationState, creationContext ) -// ); -// } - } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmSingularJoin.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmSingularJoin.java index a5b2a78f8f12..307dde42d7e2 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmSingularJoin.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmSingularJoin.java @@ -14,6 +14,7 @@ import org.hibernate.metamodel.model.domain.SingularPersistentAttribute; import org.hibernate.metamodel.model.domain.TreatableDomainType; import org.hibernate.query.sqm.SemanticQueryWalker; +import org.hibernate.query.sqm.SqmPathSource; import org.hibernate.spi.NavigablePath; import org.hibernate.query.hql.spi.SqmCreationProcessingState; import org.hibernate.query.sqm.NodeBuilder; @@ -86,6 +87,16 @@ public SqmSingularJoin copy(SqmCopyContext context) { return path; } + @Override + public SqmPathSource getNodeType() { + return getReferencedPathSource(); + } + + @Override + public SqmPathSource getReferencedPathSource() { + return getModel().getPathSource(); + } + @Override public SingularPersistentAttribute getModel() { return (SingularPersistentAttribute) super.getNodeType(); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmBinaryArithmetic.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmBinaryArithmetic.java index dc80903ec5f2..aca3096867f2 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmBinaryArithmetic.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmBinaryArithmetic.java @@ -14,8 +14,6 @@ import org.hibernate.query.sqm.tree.SqmCopyContext; import org.hibernate.query.sqm.tree.select.SqmSelectableNode; -import org.checkerframework.checker.nullness.qual.Nullable; - import static org.hibernate.query.sqm.BinaryArithmeticOperator.ADD; import static org.hibernate.query.sqm.BinaryArithmeticOperator.SUBTRACT; import static org.hibernate.type.spi.TypeConfiguration.isDuration; @@ -37,8 +35,8 @@ public SqmBinaryArithmetic( //noinspection unchecked super( (SqmExpressible) domainModel.getTypeConfiguration().resolveArithmeticType( - lhsOperand.getNodeType(), - rhsOperand.getNodeType(), + lhsOperand.getExpressible(), + rhsOperand.getExpressible(), operator ), nodeBuilder @@ -52,8 +50,8 @@ public SqmBinaryArithmetic( ( operator == ADD || operator == SUBTRACT ) ) { return; } - this.lhsOperand.applyInferableType( rhsOperand.getNodeType() ); - this.rhsOperand.applyInferableType( lhsOperand.getNodeType() ); + this.lhsOperand.applyInferableType( rhsOperand.getExpressible() ); + this.rhsOperand.applyInferableType( lhsOperand.getExpressible() ); } public SqmBinaryArithmetic( diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmFieldLiteral.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmFieldLiteral.java index 1fb04fc91833..caa38969a5f0 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmFieldLiteral.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmFieldLiteral.java @@ -180,7 +180,8 @@ public SqmPredicate in(Expression... values) { @Override public SqmPredicate in(Collection values) { - return nodeBuilder().in( this, values ); + //noinspection unchecked + return nodeBuilder().in( this, (Collection) values ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmUnaryOperation.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmUnaryOperation.java index ba626e6d0a7f..6d4133262011 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmUnaryOperation.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmUnaryOperation.java @@ -25,7 +25,7 @@ public SqmUnaryOperation(UnaryArithmeticOperator operation, SqmExpression ope operation, operand, operand.nodeBuilder().getTypeConfiguration().getBasicTypeForJavaType( - operand.getNodeType().getRelationalJavaType().getJavaType() + operand.getExpressible().getRelationalJavaType().getJavaType() ) ); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/AbstractSqmInsertStatement.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/AbstractSqmInsertStatement.java index 889e7d6add97..125e90f8d8e8 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/AbstractSqmInsertStatement.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/AbstractSqmInsertStatement.java @@ -89,6 +89,10 @@ protected List> copyInsertionTargetPaths(SqmCopyContext context) { } } + void setConflictClause(SqmConflictClause conflictClause) { + this.conflictClause = conflictClause; + } + protected void verifyInsertTypesMatch( List> insertionTargetPaths, List> expressions) { diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmConflictClause.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmConflictClause.java index 547e03d94d6c..6c3f23056223 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmConflictClause.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmConflictClause.java @@ -54,7 +54,7 @@ private SqmConflictClause( this.insertStatement = insertStatement; this.excludedRoot = excludedRoot; this.constraintName = constraintName; - this.constraintPaths = Collections.unmodifiableList( constraintPaths ); + this.constraintPaths = constraintPaths == null ? null : Collections.unmodifiableList( constraintPaths ); this.updateAction = updateAction; } @@ -157,7 +157,7 @@ public SqmConflictClause copy(SqmCopyContext context) { insertStatement.copy( context ), excludedRoot.copy( context ), constraintName, - copyOf( constraintPaths, context ), + constraintPaths == null ? null : copyOf( constraintPaths, context ), updateAction == null ? null : updateAction.copy( context ) ) ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmInsertSelectStatement.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmInsertSelectStatement.java index 90d0396bf2a8..ba65f8c2ca6c 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmInsertSelectStatement.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmInsertSelectStatement.java @@ -77,19 +77,26 @@ public SqmInsertSelectStatement copy(SqmCopyContext context) { if ( existing != null ) { return existing; } - return context.registerCopy( - this, - new SqmInsertSelectStatement<>( - nodeBuilder(), - getQuerySource(), - copyParameters( context ), - copyCteStatements( context ), - getTarget().copy( context ), - copyInsertionTargetPaths( context ), - getConflictClause() == null ? null : getConflictClause().copy( context ), - selectQueryPart.copy( context ) - ) + final SqmInsertSelectStatement sqmInsertSelectStatementCopy = new SqmInsertSelectStatement<>( + nodeBuilder(), + context.getQuerySource() == null ? getQuerySource() : context.getQuerySource(), + copyParameters( context ), + copyCteStatements( context ), + getTarget().copy( context ), + null, + null, + selectQueryPart.copy( context ) ); + + context.registerCopy( this, sqmInsertSelectStatementCopy ); + + sqmInsertSelectStatementCopy.setInsertionTargetPaths( copyInsertionTargetPaths( context ) ); + + if ( getConflictClause() != null ) { + sqmInsertSelectStatementCopy.setConflictClause( getConflictClause().copy( context ) ); + } + + return sqmInsertSelectStatementCopy; } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmInsertValuesStatement.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmInsertValuesStatement.java index d51a1b097790..8fe1511a7e02 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmInsertValuesStatement.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmInsertValuesStatement.java @@ -82,19 +82,26 @@ public SqmInsertValuesStatement copy(SqmCopyContext context) { valuesList.add( sqmValues.copy( context ) ); } } - return context.registerCopy( + + final SqmInsertValuesStatement sqmInsertValuesStatementCopy = context.registerCopy( this, new SqmInsertValuesStatement<>( nodeBuilder(), - getQuerySource(), + context.getQuerySource() == null ? getQuerySource() : context.getQuerySource(), copyParameters( context ), copyCteStatements( context ), getTarget().copy( context ), copyInsertionTargetPaths( context ), - getConflictClause() == null ? null : getConflictClause().copy( context ), + null, valuesList ) ); + + if ( getConflictClause() != null ) { + sqmInsertValuesStatementCopy.setConflictClause( getConflictClause().copy( context ) ); + } + + return sqmInsertValuesStatementCopy; } public SqmInsertValuesStatement copyWithoutValues(SqmCopyContext context) { @@ -102,7 +109,7 @@ public SqmInsertValuesStatement copyWithoutValues(SqmCopyContext context) { this, new SqmInsertValuesStatement<>( nodeBuilder(), - getQuerySource(), + context.getQuerySource() == null ? getQuerySource() : context.getQuerySource(), copyParameters( context ), copyCteStatements( context ), getTarget().copy( context ), diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/predicate/SqmMemberOfPredicate.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/predicate/SqmMemberOfPredicate.java index 82769080643e..2e75e1176903 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/predicate/SqmMemberOfPredicate.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/predicate/SqmMemberOfPredicate.java @@ -37,7 +37,7 @@ public SqmMemberOfPredicate( this.pluralPath = pluralPath; this.leftHandExpression = leftHandExpression; - final SimpleDomainType simpleDomainType = pluralPath.getReferencedPathSource().getElementType(); + final SimpleDomainType simpleDomainType = pluralPath.getPluralAttribute().getElementType(); if ( !areTypesComparable(leftHandExpression.getNodeType(), simpleDomainType, nodeBuilder.getSessionFactory()) ) { throw new SemanticException( diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmOrderByClause.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmOrderByClause.java index adb5301a63d5..a8b4bc88f798 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmOrderByClause.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmOrderByClause.java @@ -8,13 +8,15 @@ import java.io.Serializable; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import org.hibernate.query.sqm.tree.SqmCopyContext; import org.hibernate.query.sqm.tree.expression.SqmAliasedNodeRef; import org.hibernate.query.sqm.tree.expression.SqmExpression; +import static java.util.Collections.emptyList; +import static java.util.Collections.unmodifiableList; + /** * @author Steve Ebersole */ @@ -71,12 +73,7 @@ public SqmOrderByClause addSortSpecification(SqmExpression expression) { } public List getSortSpecifications() { - if ( sortSpecifications == null ) { - return Collections.emptyList(); - } - else { - return Collections.unmodifiableList( sortSpecifications ); - } + return sortSpecifications == null ? emptyList() : unmodifiableList( sortSpecifications ); } public void setSortSpecifications(List sortSpecifications) { diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmQueryGroup.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmQueryGroup.java index f90c532e59b7..9f2fdd738d98 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmQueryGroup.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmQueryGroup.java @@ -170,7 +170,8 @@ private void validateQueryGroupFetchStructure(List> ty for ( int j = 0; j < firstSelectionSize; j++ ) { final SqmTypedNode firstSqmSelection = typedNodes.get( j ); final JavaType firstJavaType = firstSqmSelection.getNodeJavaType(); - if ( firstJavaType != selections.get( j ).getNodeJavaType() ) { + final JavaType nodeJavaType = selections.get( j ).getNodeJavaType(); + if ( nodeJavaType != null && firstJavaType != null && firstJavaType != nodeJavaType ) { throw new SemanticException( "Select items of the same index must have the same java type across all query parts" ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmQueryPart.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmQueryPart.java index 230ca8cf1eb6..22f494796130 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmQueryPart.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmQueryPart.java @@ -6,7 +6,6 @@ */ package org.hibernate.query.sqm.tree.select; -import java.util.Collections; import java.util.List; import org.hibernate.query.sqm.FetchClauseType; @@ -18,6 +17,8 @@ import org.hibernate.query.sqm.tree.SqmVisitableNode; import org.hibernate.query.sqm.tree.expression.SqmExpression; +import static java.util.Collections.emptyList; + /** * Defines the ordering and fetch/offset part of a query which is shared with query groups. * @@ -129,11 +130,7 @@ public FetchClauseType getFetchClauseType() { @Override public List getSortSpecifications() { - if ( getOrderByClause() == null ) { - return Collections.emptyList(); - } - - return getOrderByClause().getSortSpecifications(); + return getOrderByClause() == null ? emptyList() : getOrderByClause().getSortSpecifications(); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSelectStatement.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSelectStatement.java index 631d3eeff92c..6d23adb216ad 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSelectStatement.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSelectStatement.java @@ -140,7 +140,7 @@ private SqmSelectStatement createCopy(SqmCopyContext context, Class re nodeBuilder(), copyCteStatements( context ), resultType, - getQuerySource(), + context.getQuerySource() == null ? getQuerySource() : context.getQuerySource(), parameters ) ); @@ -492,6 +492,9 @@ public SqmSelectStatement createCountQuery() { final SqmSelectStatement query = nodeBuilder().createQuery( Long.class ); query.from( subquery ); query.select( nodeBuilder().count() ); + if ( subquery.getFetch() == null && subquery.getOffset() == null ) { + subquery.getQueryPart().setOrderByClause( null ); + } return query; } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSubQuery.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSubQuery.java index 87766ca7feb0..493e8aa216d1 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSubQuery.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSubQuery.java @@ -592,7 +592,8 @@ public SqmInPredicate in(Expression... values) { @Override public SqmInPredicate in(Collection values) { - return nodeBuilder().in( this, values ); + //noinspection unchecked + return nodeBuilder().in( this, (Collection) values ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/update/SqmUpdateStatement.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/update/SqmUpdateStatement.java index f3accf2906c7..050b292c56d7 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/update/SqmUpdateStatement.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/update/SqmUpdateStatement.java @@ -107,7 +107,7 @@ public SqmUpdateStatement copy(SqmCopyContext context) { this, new SqmUpdateStatement<>( nodeBuilder(), - getQuerySource(), + context.getQuerySource() == null ? getQuerySource() : context.getQuerySource(), copyParameters( context ), copyCteStatements( context ), getTarget().copy( context ) @@ -183,7 +183,9 @@ public SqmUpdateStatement set(SingularAttribute attribute, @Override public SqmUpdateStatement set(Path attribute, X value) { - applyAssignment( (SqmPath) attribute, (SqmExpression) nodeBuilder().value( value ) ); + final SqmCriteriaNodeBuilder nodeBuilder = (SqmCriteriaNodeBuilder) nodeBuilder(); + final SqmPath sqmAttribute = (SqmPath) attribute; + applyAssignment( sqmAttribute, nodeBuilder.value( value, sqmAttribute ) ); return this; } @@ -195,15 +197,15 @@ public SqmUpdateStatement set(Path attribute, Expression @Override @SuppressWarnings({"rawtypes", "unchecked"}) public SqmUpdateStatement set(String attributeName, Object value) { - final SqmPath sqmPath = getTarget().get(attributeName); + final SqmPath sqmPath = getTarget().get( attributeName ); final SqmExpression expression; if ( value instanceof SqmExpression ) { expression = (SqmExpression) value; } else { - expression = (SqmExpression) nodeBuilder().value( value ); + final SqmCriteriaNodeBuilder nodeBuilder = (SqmCriteriaNodeBuilder) nodeBuilder(); + expression = nodeBuilder.value( value, sqmPath ); } - assertAssignable( null, sqmPath, expression, nodeBuilder().getSessionFactory() ); applyAssignment( sqmPath, expression ); return this; } diff --git a/hibernate-core/src/main/java/org/hibernate/resource/transaction/backend/jta/internal/JtaIsolationDelegate.java b/hibernate-core/src/main/java/org/hibernate/resource/transaction/backend/jta/internal/JtaIsolationDelegate.java index 51c1e3f80cd5..53cf8921b35b 100644 --- a/hibernate-core/src/main/java/org/hibernate/resource/transaction/backend/jta/internal/JtaIsolationDelegate.java +++ b/hibernate-core/src/main/java/org/hibernate/resource/transaction/backend/jta/internal/JtaIsolationDelegate.java @@ -117,7 +117,9 @@ private T doInSuspendedTransaction(HibernateCallable callable) { try { // First we suspend any current JTA transaction final Transaction surroundingTransaction = transactionManager.suspend(); - LOG.debugf( "Surrounding JTA transaction suspended [%s]", surroundingTransaction ); + if ( surroundingTransaction != null ) { + LOG.debugf( "Surrounding JTA transaction suspended [%s]", surroundingTransaction ); + } try { return callable.call(); @@ -127,8 +129,10 @@ private T doInSuspendedTransaction(HibernateCallable callable) { } finally { try { - transactionManager.resume( surroundingTransaction ); - LOG.debugf( "Surrounding JTA transaction resumed [%s]", surroundingTransaction ); + if ( surroundingTransaction != null ) { + transactionManager.resume( surroundingTransaction ); + LOG.debugf( "Surrounding JTA transaction resumed [%s]", surroundingTransaction ); + } } catch ( Throwable t2 ) { // if the actually work had an error use that, otherwise error based on t @@ -199,7 +203,11 @@ private T doTheWork(WorkExecutorVisitable work) { } } catch (SQLException e) { - throw sqlExceptionConverter().apply( e, "unable to obtain isolated JDBC connection" ); + final JDBCException jdbcException = sqlExceptionConverter().apply( e, "unable to obtain isolated JDBC connection" ); + if ( jdbcException == null ) { + throw new HibernateException( "Unable to obtain isolated JDBC connection", e ); + } + throw jdbcException; } } diff --git a/hibernate-core/src/main/java/org/hibernate/result/internal/OutputsImpl.java b/hibernate-core/src/main/java/org/hibernate/result/internal/OutputsImpl.java index 9728d5a4e38a..0d9a6bdb0dec 100644 --- a/hibernate-core/src/main/java/org/hibernate/result/internal/OutputsImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/result/internal/OutputsImpl.java @@ -16,6 +16,7 @@ import java.util.function.Supplier; import org.hibernate.JDBCException; +import org.hibernate.engine.jdbc.spi.JdbcCoordinator; import org.hibernate.engine.jdbc.spi.SqlStatementLogger; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.event.spi.EventManager; @@ -147,7 +148,9 @@ public boolean goToNext() { @Override public void release() { - context.getSession().getJdbcCoordinator().getLogicalConnection().getResourceRegistry().release( jdbcStatement ); + final JdbcCoordinator jdbcCoordinator = context.getSession().getJdbcCoordinator(); + jdbcCoordinator.getLogicalConnection().getResourceRegistry().release( jdbcStatement ); + jdbcCoordinator.afterStatementExecution(); } private List extractCurrentResults() { diff --git a/hibernate-core/src/main/java/org/hibernate/sql/Template.java b/hibernate-core/src/main/java/org/hibernate/sql/Template.java index 05c5a78d028c..7e8253d36a00 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/Template.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/Template.java @@ -19,6 +19,8 @@ import org.hibernate.query.sqm.function.SqmFunctionRegistry; import org.hibernate.type.spi.TypeConfiguration; +import static java.lang.Boolean.parseBoolean; +import static java.lang.Character.isLetter; import static org.hibernate.internal.util.StringHelper.WHITESPACE; /** @@ -92,6 +94,7 @@ public final class Template { LITERAL_PREFIXES.add("date"); LITERAL_PREFIXES.add("time"); LITERAL_PREFIXES.add("timestamp"); + LITERAL_PREFIXES.add("zone"); } public static final String TEMPLATE = "$PlaceHolder$"; @@ -139,12 +142,9 @@ public static String renderWhereStringTemplate( // which the tokens occur. Depending on the state of those flags we decide whether we need to qualify // identifier references. - String symbols = PUNCTUATION + - WHITESPACE + - dialect.openQuote() + - dialect.closeQuote(); - StringTokenizer tokens = new StringTokenizer( sqlWhereString, symbols, true ); - StringBuilder result = new StringBuilder(); + final String symbols = PUNCTUATION + WHITESPACE + dialect.openQuote() + dialect.closeQuote(); + final StringTokenizer tokens = new StringTokenizer( sqlWhereString, symbols, true ); + final StringBuilder result = new StringBuilder(); boolean quoted = false; boolean quotedIdentifier = false; @@ -168,7 +168,7 @@ public static String renderWhereStringTemplate( } if ( !quoted ) { - boolean isOpenQuote; + final boolean isOpenQuote; if ( "`".equals(token) ) { isOpenQuote = !quotedIdentifier; token = lcToken = isOpenQuote @@ -177,182 +177,27 @@ public static String renderWhereStringTemplate( quotedIdentifier = isOpenQuote; isQuoteCharacter = true; } - else if ( !quotedIdentifier && ( dialect.openQuote()==token.charAt(0) ) ) { + else if ( !quotedIdentifier && dialect.openQuote()==token.charAt(0) ) { isOpenQuote = true; quotedIdentifier = true; isQuoteCharacter = true; } - else if ( quotedIdentifier && ( dialect.closeQuote()==token.charAt(0) ) ) { + else if ( quotedIdentifier && dialect.closeQuote()==token.charAt(0) ) { quotedIdentifier = false; isQuoteCharacter = true; isOpenQuote = false; } - else if ( LITERAL_PREFIXES.contains( lcToken ) ) { - if ( "'".equals( nextToken ) ) { - // Don't prefix a literal - result.append( token ); - continue; - } - else if ( nextToken != null && Character.isWhitespace( nextToken.charAt( 0 ) ) ) { - final StringBuilder additionalTokens = new StringBuilder(); - TimeZoneTokens possibleNextToken = null; - do { - possibleNextToken = possibleNextToken == null - ? TimeZoneTokens.getPossibleNextTokens( lcToken ) - : possibleNextToken.nextToken(); - do { - additionalTokens.append( nextToken ); - hasMore = tokens.hasMoreTokens(); - nextToken = tokens.nextToken(); - } while ( nextToken != null && Character.isWhitespace( nextToken.charAt( 0 ) ) ); - } while ( nextToken != null && possibleNextToken.isToken( nextToken ) ); - if ( "'".equals( nextToken ) ) { - // Don't prefix a literal - result.append( token ); - result.append( additionalTokens ); - continue; - } - else { - isOpenQuote = false; - } - } - else { - isOpenQuote = false; - } - } else { isOpenQuote = false; } - if ( isOpenQuote ) { result.append( placeholder ).append( '.' ); } } - // Special processing for ANSI SQL EXTRACT function - if ( "extract".equals( lcToken ) && "(".equals( nextToken ) ) { - final String field = extractUntil( tokens, "from" ); - final String source = renderWhereStringTemplate( - extractUntil( tokens, ")" ), - placeholder, - dialect, - typeConfiguration, - functionRegistry - ); - result.append( "extract(" ).append( field ).append( " from " ).append( source ).append( ')' ); - - hasMore = tokens.hasMoreTokens(); - nextToken = hasMore ? tokens.nextToken() : null; - - continue; - } - - // Special processing for ANSI SQL TRIM function - if ( "trim".equals( lcToken ) && "(".equals( nextToken ) ) { - List operands = new ArrayList<>(); - StringBuilder builder = new StringBuilder(); - - boolean hasMoreOperands = true; - String operandToken = tokens.nextToken(); - switch ( operandToken.toLowerCase( Locale.ROOT ) ) { - case "leading": - case "trailing": - case "both": - operands.add( operandToken ); - if ( hasMoreOperands = tokens.hasMoreTokens() ) { - operandToken = tokens.nextToken(); - } - break; - } - boolean quotedOperand = false; - int parenthesis = 0; - while ( hasMoreOperands ) { - final boolean isQuote = "'".equals( operandToken ); - if ( isQuote ) { - quotedOperand = !quotedOperand; - if ( !quotedOperand ) { - operands.add( builder.append( '\'' ).toString() ); - builder.setLength( 0 ); - } - else { - builder.append( '\'' ); - } - } - else if ( quotedOperand ) { - builder.append( operandToken ); - } - else if ( parenthesis != 0 ) { - builder.append( operandToken ); - switch ( operandToken ) { - case "(": - parenthesis++; - break; - case ")": - parenthesis--; - break; - } - } - else { - builder.append( operandToken ); - switch ( operandToken.toLowerCase( Locale.ROOT ) ) { - case "(": - parenthesis++; - break; - case ")": - parenthesis--; - break; - case "from": - if ( builder.length() != 0 ) { - operands.add( builder.substring( 0, builder.length() - 4 ) ); - builder.setLength( 0 ); - operands.add( operandToken ); - } - break; - } - } - operandToken = tokens.nextToken(); - hasMoreOperands = tokens.hasMoreTokens() && ( parenthesis != 0 || ! ")".equals( operandToken ) ); - } - if ( builder.length() != 0 ) { - operands.add( builder.toString() ); - } - - TrimOperands trimOperands = new TrimOperands( operands ); - result.append( "trim(" ); - if ( trimOperands.trimSpec != null ) { - result.append( trimOperands.trimSpec ).append( ' ' ); - } - if ( trimOperands.trimChar != null ) { - if ( trimOperands.trimChar.startsWith( "'" ) && trimOperands.trimChar.endsWith( "'" ) ) { - result.append( trimOperands.trimChar ); - } - else { - result.append( - renderWhereStringTemplate( trimOperands.trimSpec, placeholder, dialect, typeConfiguration, functionRegistry ) - ); - } - result.append( ' ' ); - } - if ( trimOperands.from != null ) { - result.append( trimOperands.from ).append( ' ' ); - } - else if ( trimOperands.trimSpec != null || trimOperands.trimChar != null ) { - // I think ANSI SQL says that the 'from' is not optional if either trim-spec or trim-char is specified - result.append( "from " ); - } - - result.append( renderWhereStringTemplate( trimOperands.trimSource, placeholder, dialect, typeConfiguration, functionRegistry ) ) - .append( ')' ); - - hasMore = tokens.hasMoreTokens(); - nextToken = hasMore ? tokens.nextToken() : null; - - continue; - } - - boolean quotedOrWhitespace = quoted || quotedIdentifier || isQuoteCharacter - || Character.isWhitespace( token.charAt(0) ); - + final boolean quotedOrWhitespace = + quoted || quotedIdentifier || isQuoteCharacter + || token.isBlank(); if ( quotedOrWhitespace ) { result.append( token ); } @@ -370,8 +215,21 @@ else if ( afterFromTable ) { else if ( isNamedParameter(token) ) { result.append(token); } + else if ( isExtractFunction( lcToken, nextToken ) ) { + // Special processing for ANSI SQL EXTRACT function + handleExtractFunction( placeholder, dialect, typeConfiguration, functionRegistry, tokens, result ); + hasMore = tokens.hasMoreTokens(); + nextToken = hasMore ? tokens.nextToken() : null; + } + else if ( isTrimFunction( lcToken, nextToken ) ) { + // Special processing for ANSI SQL TRIM function + handleTrimFunction( placeholder, dialect, typeConfiguration, functionRegistry, tokens, result ); + hasMore = tokens.hasMoreTokens(); + nextToken = hasMore ? tokens.nextToken() : null; + } else if ( isIdentifier(token) - && !isFunctionOrKeyword(lcToken, nextToken, dialect, typeConfiguration, functionRegistry) ) { + && !isFunctionOrKeyword( lcToken, nextToken, dialect, typeConfiguration, functionRegistry ) + && !isLiteral( lcToken, nextToken, sqlWhereString, symbols, tokens ) ) { result.append(placeholder) .append('.') .append( dialect.quote(token) ); @@ -385,7 +243,7 @@ else if ( inFromClause && ",".equals(lcToken) ) { beforeTable = true; } if ( isBoolean( token ) ) { - token = dialect.toBooleanValueString( Boolean.parseBoolean( token ) ); + token = dialect.toBooleanValueString( parseBoolean( token ) ); } result.append(token); } @@ -401,37 +259,173 @@ else if ( inFromClause && ",".equals(lcToken) ) { return result.toString(); } - private enum TimeZoneTokens { - NONE, - WITH, - TIME, - ZONE; - - static TimeZoneTokens getPossibleNextTokens(String lctoken) { - switch ( lctoken ) { - case "time": - case "timestamp": - return WITH; - default: - return NONE; + private static boolean isTrimFunction(String lcToken, String nextToken) { + return "trim".equals(lcToken) && "(".equals(nextToken); + } + + private static boolean isExtractFunction(String lcToken, String nextToken) { + return "extract".equals(lcToken) && "(".equals(nextToken); + } + + private static boolean isLiteral( + String lcToken, String next, + String sqlWhereString, String symbols, StringTokenizer tokens) { + if ( LITERAL_PREFIXES.contains( lcToken ) && next != null ) { + // easy cases first + if ( "'".equals(next) ) { + return true; + } + else if ( !next.isBlank() ) { + return false; + } + else { + // we need to look ahead in the token stream + // to find the first non-blank token + final StringTokenizer lookahead = + new StringTokenizer( sqlWhereString, symbols, true ); + while ( lookahead.countTokens() > tokens.countTokens()+1 ) { + lookahead.nextToken(); + } + if ( lookahead.hasMoreTokens() ) { + String nextToken; + do { + nextToken = lookahead.nextToken().toLowerCase(Locale.ROOT); + } + while ( nextToken.isBlank() && lookahead.hasMoreTokens() ); + return "'".equals( nextToken ) + || lcToken.equals( "time" ) && "with".equals( nextToken ) + || lcToken.equals( "timestamp" ) && "with".equals( nextToken ) + || lcToken.equals( "time" ) && "zone".equals( nextToken ); + } + else { + return false; + } } } + else { + return false; + } + } - public TimeZoneTokens nextToken() { - if ( this == WITH ) { - return TIME; + private static void handleTrimFunction( + String placeholder, Dialect dialect, + TypeConfiguration typeConfiguration, + SqmFunctionRegistry functionRegistry, + StringTokenizer tokens, + StringBuilder result) { + final List operands = new ArrayList<>(); + final StringBuilder builder = new StringBuilder(); + + boolean hasMoreOperands = true; + String operandToken = tokens.nextToken(); + switch ( operandToken.toLowerCase( Locale.ROOT ) ) { + case "leading": + case "trailing": + case "both": + operands.add( operandToken ); + if ( hasMoreOperands = tokens.hasMoreTokens() ) { + operandToken = tokens.nextToken(); + } + break; + } + boolean quotedOperand = false; + int parenthesis = 0; + while ( hasMoreOperands ) { + final boolean isQuote = "'".equals( operandToken ); + if ( isQuote ) { + quotedOperand = !quotedOperand; + if ( !quotedOperand ) { + operands.add( builder.append( '\'' ).toString() ); + builder.setLength( 0 ); + } + else { + builder.append( '\'' ); + } } - else if ( this == TIME ) { - return ZONE; + else if ( quotedOperand ) { + builder.append( operandToken ); + } + else if ( parenthesis != 0 ) { + builder.append( operandToken ); + switch ( operandToken ) { + case "(": + parenthesis++; + break; + case ")": + parenthesis--; + break; + } } else { - return NONE; + builder.append( operandToken ); + switch ( operandToken.toLowerCase( Locale.ROOT ) ) { + case "(": + parenthesis++; + break; + case ")": + parenthesis--; + break; + case "from": + if ( builder.length() != 0 ) { + operands.add( builder.substring( 0, builder.length() - 4 ) ); + builder.setLength( 0 ); + operands.add( operandToken ); + } + break; + } } + operandToken = tokens.nextToken(); + hasMoreOperands = tokens.hasMoreTokens() + && ( parenthesis != 0 || ! ")".equals( operandToken ) ); + } + if ( builder.length() != 0 ) { + operands.add( builder.toString() ); } - public boolean isToken(String token) { - return this != NONE && name().equalsIgnoreCase( token ); + final TrimOperands trimOperands = new TrimOperands( operands ); + result.append( "trim(" ); + if ( trimOperands.trimSpec != null ) { + result.append( trimOperands.trimSpec ).append( ' ' ); + } + if ( trimOperands.trimChar != null ) { + if ( trimOperands.trimChar.startsWith( "'" ) && trimOperands.trimChar.endsWith( "'" ) ) { + result.append( trimOperands.trimChar ); + } + else { + result.append( + renderWhereStringTemplate( trimOperands.trimSpec, placeholder, dialect, typeConfiguration, functionRegistry ) + ); + } + result.append( ' ' ); } + if ( trimOperands.from != null ) { + result.append( trimOperands.from ).append( ' ' ); + } + else if ( trimOperands.trimSpec != null || trimOperands.trimChar != null ) { + // I think ANSI SQL says that the 'from' is not optional if either trim-spec or trim-char is specified + result.append( "from " ); + } + + result.append( renderWhereStringTemplate( trimOperands.trimSource, placeholder, dialect, typeConfiguration, functionRegistry ) ) + .append( ')' ); + } + + private static void handleExtractFunction( + String placeholder, + Dialect dialect, + TypeConfiguration typeConfiguration, + SqmFunctionRegistry functionRegistry, + StringTokenizer tokens, + StringBuilder result) { + final String field = extractUntil( tokens, "from" ); + final String source = renderWhereStringTemplate( + extractUntil( tokens, ")" ), + placeholder, + dialect, + typeConfiguration, + functionRegistry + ); + result.append( "extract(" ).append( field ).append( " from " ).append( source ).append( ')' ); } public static List collectColumnNames( @@ -754,7 +748,7 @@ else if ( size == 4 ) { } private static String extractUntil(StringTokenizer tokens, String delimiter) { - StringBuilder valueBuilder = new StringBuilder(); + final StringBuilder valueBuilder = new StringBuilder(); String token = tokens.nextToken(); while ( ! delimiter.equalsIgnoreCase( token ) ) { valueBuilder.append( token ); @@ -773,12 +767,21 @@ private static boolean isFunctionOrKeyword( Dialect dialect, TypeConfiguration typeConfiguration, SqmFunctionRegistry functionRegistry) { - return "(".equals( nextToken ) || - KEYWORDS.contains( lcToken ) || - isType( lcToken, typeConfiguration ) || - isFunction( lcToken, nextToken, functionRegistry ) || - dialect.getKeywords().contains( lcToken ) || - FUNCTION_KEYWORDS.contains( lcToken ); + if ( "(".equals( nextToken ) ) { + return true; + } + else if ( "date".equals( lcToken ) || "time".equals( lcToken ) ) { + // these can be column names on some databases + // TODO: treat 'current date' as a function + return false; + } + else { + return KEYWORDS.contains( lcToken ) + || isType( lcToken, typeConfiguration ) + || isFunction( lcToken, nextToken, functionRegistry ) + || dialect.getKeywords().contains( lcToken ) + || FUNCTION_KEYWORDS.contains( lcToken ); + } } private static boolean isType(String lcToken, TypeConfiguration typeConfiguration) { @@ -800,10 +803,11 @@ private static boolean isIdentifier(String token) { if ( isBoolean( token ) ) { return false; } - return token.charAt( 0 ) == '`' || ( //allow any identifier quoted with backtick - Character.isLetter( token.charAt( 0 ) ) && //only recognizes identifiers beginning with a letter + return token.charAt( 0 ) == '`' + || ( //allow any identifier quoted with backtick + isLetter( token.charAt( 0 ) ) && //only recognizes identifiers beginning with a letter token.indexOf( '.' ) < 0 - ); + ); } private static boolean isBoolean(String token) { diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/internal/TableGroupHelper.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/internal/TableGroupHelper.java new file mode 100644 index 000000000000..06567a2c02b8 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/internal/TableGroupHelper.java @@ -0,0 +1,282 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ + +package org.hibernate.sql.ast.internal; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.hibernate.internal.util.collections.CollectionHelper; +import org.hibernate.metamodel.mapping.internal.CaseStatementDiscriminatorMappingImpl; +import org.hibernate.persister.internal.SqlFragmentPredicate; +import org.hibernate.sql.ast.spi.AbstractSqlAstWalker; +import org.hibernate.sql.ast.tree.expression.AggregateColumnWriteExpression; +import org.hibernate.sql.ast.tree.expression.ColumnReference; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.SelfRenderingExpression; +import org.hibernate.sql.ast.tree.expression.SelfRenderingSqlFragmentExpression; +import org.hibernate.sql.ast.tree.expression.SqlTuple; +import org.hibernate.sql.ast.tree.expression.SqlTupleContainer; +import org.hibernate.sql.ast.tree.from.NamedTableReference; +import org.hibernate.sql.ast.tree.from.TableGroup; +import org.hibernate.sql.ast.tree.from.TableReference; +import org.hibernate.sql.ast.tree.from.TableReferenceJoin; +import org.hibernate.sql.ast.tree.predicate.ComparisonPredicate; +import org.hibernate.sql.ast.tree.predicate.FilterPredicate; +import org.hibernate.sql.ast.tree.predicate.Junction; +import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.sql.model.ast.ColumnWriteFragment; + +/** + * A simple walker that checks if a predicate contains qualifiers. + * + * @author Christian Beikov + */ +public class TableGroupHelper extends AbstractSqlAstWalker { + + public static final int REAL_TABLE_GROUP_REQUIRED = Integer.MAX_VALUE; + public static final int NO_TABLE_GROUP_REQUIRED = -1; + + private final String primaryQualifier; + private final Map qualifiers; + private final String[] qualifierFragments; + private Integer usedTableReferenceJoinIndex; + + private TableGroupHelper(String primaryQualifier, Map qualifiers) { + this.primaryQualifier = primaryQualifier; + this.qualifiers = qualifiers; + final String[] qualifierFragments = new String[qualifiers.size()]; + for ( Map.Entry entry : qualifiers.entrySet() ) { + qualifierFragments[entry.getValue()] = entry.getKey() + "."; + } + this.qualifierFragments = qualifierFragments; + } + + /** + * Returns the index of a table reference join which can be swapped with the primary table reference + * to avoid rendering a real nested table group. + * {@link #REAL_TABLE_GROUP_REQUIRED} is returned if swapping is not possible. + * {@code #NO_TABLE_GROUP_REQUIRED} is returned if no swapping is necessary. + */ + public static int findReferenceJoinForPredicateSwap(TableGroup tableGroup, Predicate predicate) { + if ( predicate != null && !tableGroup.getTableReferenceJoins().isEmpty() ) { + final TableReference primaryTableReference = tableGroup.getPrimaryTableReference(); + final HashMap qualifiers = CollectionHelper.mapOfSize( tableGroup.getTableReferenceJoins().size() ); + final List tableReferenceJoins = tableGroup.getTableReferenceJoins(); + for ( int i = 0; i < tableReferenceJoins.size(); i++ ) { + final TableReferenceJoin tableReferenceJoin = tableReferenceJoins.get( i ); + if ( !tableGroup.canUseInnerJoins() ) { + if ( !isSimplePredicate( tableGroup, i ) ){//|| isSimpleOrOuterJoin( tableReferenceJoin ) ) { + // Can't do avoid the real table group rendering in this case if it's not inner joined, + // because doing so might change the meaning of the SQL. Consider this example: + // `from tbl1 t1 left join (tbl2 t2 join tbl3 t3 on t2.id=t3.id and ...) on t1.fk=t2.id` + // + // To avoid the nested table group rendering, the join on `tbl3` has to switch to left join + // `from tbl1 t1 left join tbl2 t2 on t1.fk=t2.id left join tbl3 t3 on t2.id=t3.id and ...` + // The additional predicate in the `tbl3` join can make `t3` null even though `t2` is non-null + return REAL_TABLE_GROUP_REQUIRED; + } + } + qualifiers.put( tableReferenceJoin.getJoinedTableReference().getIdentificationVariable(), i ); + } + final TableGroupHelper qualifierCollector = new TableGroupHelper( + primaryTableReference.getIdentificationVariable(), + qualifiers + ); + try { + predicate.accept( qualifierCollector ); + if ( qualifierCollector.usedTableReferenceJoinIndex == null ) { + return NO_TABLE_GROUP_REQUIRED; + } + if ( qualifierCollector.usedTableReferenceJoinIndex != NO_TABLE_GROUP_REQUIRED + && !tableGroup.canUseInnerJoins() && !isSimpleTableReference( primaryTableReference ) ) { + // Can't reorder table reference join with primary table reference if the primary table reference + // might filter out elements, since that affects result count with outer joins + return REAL_TABLE_GROUP_REQUIRED; + } + return qualifierCollector.usedTableReferenceJoinIndex; + } + catch (MultipleUsesFoundException ex) { + return REAL_TABLE_GROUP_REQUIRED; + } + } + return NO_TABLE_GROUP_REQUIRED; + } + + private static boolean isSimpleTableReference(TableReference tableReference) { + return tableReference instanceof NamedTableReference && !tableReference.getTableId().startsWith( "(select" ); + } + + /** + * Checks if the table reference join at the given index uses a simple equality join predicate. + * Predicates that contain anything but comparisons of the primary table reference with table reference join columns + * are non-simple. + */ + private static boolean isSimplePredicate(TableGroup tableGroup, int index) { + final TableReference primaryTableReference = tableGroup.getPrimaryTableReference(); + final TableReferenceJoin tableReferenceJoin = tableGroup.getTableReferenceJoins().get( index ); + final NamedTableReference joinedTableReference = tableReferenceJoin.getJoinedTableReference(); + final Predicate predicate = tableReferenceJoin.getPredicate(); + if ( predicate instanceof Junction ) { + final Junction junction = (Junction) predicate; + if ( junction.getNature() == Junction.Nature.CONJUNCTION ) { + for ( Predicate subPredicate : junction.getPredicates() ) { + if ( !isComparison( subPredicate, primaryTableReference, joinedTableReference ) ) { + return false; + } + } + return true; + } + } + else { + return isComparison( predicate, primaryTableReference, joinedTableReference ); + } + return false; + } + + private static boolean isComparison(Predicate predicate, TableReference table1, TableReference table2) { + if ( predicate instanceof ComparisonPredicate ) { + final ComparisonPredicate comparisonPredicate = (ComparisonPredicate) predicate; + final Expression lhs = comparisonPredicate.getLeftHandExpression(); + final Expression rhs = comparisonPredicate.getRightHandExpression(); + final SqlTuple lhsTuple; + if ( lhs instanceof SqlTupleContainer && ( lhsTuple = ( (SqlTupleContainer) lhs ).getSqlTuple() ) != null ) { + final SqlTuple rhsTuple = ( (SqlTupleContainer) rhs ).getSqlTuple(); + final List lhsExpressions = lhsTuple.getExpressions(); + final List rhsExpressions = rhsTuple.getExpressions(); + for ( int i = 0; i < lhsExpressions.size(); i++ ) { + final ColumnReference lhsColumn = lhsExpressions.get( i ).getColumnReference(); + final ColumnReference rhsColumn = rhsExpressions.get( i ).getColumnReference(); + if ( !isComparison( table1, table2, lhsColumn, rhsColumn ) ) { + return false; + } + } + return true; + } + else { + return isComparison( table1, table2, lhs.getColumnReference(), rhs.getColumnReference() ); + } + } + return false; + } + + private static boolean isComparison( + TableReference table1, + TableReference table2, + ColumnReference column1, + ColumnReference column2) { + if ( column1 != null && column2 != null ) { + final String column1Qualifier = column1.getQualifier(); + final String column2Qualifier = column2.getQualifier(); + final String table1Qualifier = table1.getIdentificationVariable(); + final String table2Qualifier = table2.getIdentificationVariable(); + return column1Qualifier.equals( table1Qualifier ) && column2Qualifier.equals( table2Qualifier ) + || column1Qualifier.equals( table2Qualifier ) && column2Qualifier.equals( table1Qualifier ); + } + return false; + } + + private static class MultipleUsesFoundException extends RuntimeException { + public MultipleUsesFoundException() { + } + + @Override + public Throwable fillInStackTrace() { + return this; + } + } + + private void checkQualifier(String qualifier) { + if ( primaryQualifier.equals( qualifier ) ) { + if ( usedTableReferenceJoinIndex != null && usedTableReferenceJoinIndex != NO_TABLE_GROUP_REQUIRED ) { + throw new MultipleUsesFoundException(); + } + usedTableReferenceJoinIndex = NO_TABLE_GROUP_REQUIRED; + } + else { + final Integer index = qualifiers.get( qualifier ); + if ( index != null ) { + if ( usedTableReferenceJoinIndex != null && usedTableReferenceJoinIndex.intValue() != index ) { + throw new MultipleUsesFoundException(); + } + usedTableReferenceJoinIndex = index; + } + } + } + + private void checkSql(String sql) { + if ( sql.contains( primaryQualifier + "." ) ) { + if ( usedTableReferenceJoinIndex != null && usedTableReferenceJoinIndex != NO_TABLE_GROUP_REQUIRED ) { + throw new MultipleUsesFoundException(); + } + usedTableReferenceJoinIndex = NO_TABLE_GROUP_REQUIRED; + } + else { + for ( int i = 0; i < qualifierFragments.length; i++ ) { + if ( sql.contains( qualifierFragments[i] ) ) { + if ( usedTableReferenceJoinIndex != null && usedTableReferenceJoinIndex != i ) { + throw new MultipleUsesFoundException(); + } + usedTableReferenceJoinIndex = i; + } + } + } + } + + @Override + public void visitSelfRenderingExpression(SelfRenderingExpression expression) { + if ( expression instanceof SelfRenderingSqlFragmentExpression ) { + checkSql( ( (SelfRenderingSqlFragmentExpression) expression ).getExpression() ); + } + else if ( expression instanceof CaseStatementDiscriminatorMappingImpl.CaseStatementDiscriminatorExpression ) { + for ( TableReference usedTableReference : ( (CaseStatementDiscriminatorMappingImpl.CaseStatementDiscriminatorExpression) expression ).getUsedTableReferences() ) { + usedTableReference.accept( this ); + } + } + else { + super.visitSelfRenderingExpression( expression ); + } + } + + @Override + public void visitNamedTableReference(NamedTableReference tableReference) { + checkQualifier( tableReference.getIdentificationVariable() ); + } + + @Override + public void visitColumnReference(ColumnReference columnReference) { + checkQualifier( columnReference.getQualifier() ); + } + + @Override + public void visitAggregateColumnWriteExpression(AggregateColumnWriteExpression aggregateColumnWriteExpression) { + checkQualifier( aggregateColumnWriteExpression.getAggregateColumnReference().getQualifier() ); + } + + @Override + public void visitFilterPredicate(FilterPredicate filterPredicate) { + for ( FilterPredicate.FilterFragmentPredicate fragment : filterPredicate.getFragments() ) { + visitFilterFragmentPredicate( fragment ); + } + } + + @Override + public void visitFilterFragmentPredicate(FilterPredicate.FilterFragmentPredicate fragmentPredicate) { + checkSql( fragmentPredicate.getSqlFragment() ); + } + + @Override + public void visitSqlFragmentPredicate(SqlFragmentPredicate predicate) { + checkSql( predicate.getSqlFragment() ); + } + + @Override + public void visitColumnWriteFragment(ColumnWriteFragment columnWriteFragment) { + checkSql( columnWriteFragment.getFragment() ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java index 9a9c79266a0e..7fb0ba09e150 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java @@ -89,6 +89,7 @@ import org.hibernate.sql.ast.SqlAstNodeRenderingMode; import org.hibernate.sql.ast.SqlAstTranslator; import org.hibernate.sql.ast.SqlTreeCreationException; +import org.hibernate.sql.ast.internal.TableGroupHelper; import org.hibernate.sql.ast.internal.ParameterMarkerStrategyStandard; import org.hibernate.sql.ast.tree.AbstractUpdateOrDeleteStatement; import org.hibernate.sql.ast.tree.MutationStatement; @@ -6035,10 +6036,60 @@ protected void renderRootTableGroup(TableGroup tableGroup, List } protected void renderTableGroup(TableGroup tableGroup, Predicate predicate, List tableGroupJoinCollector) { - // Without reference joins or nested join groups, even a real table group does not need parenthesis - final boolean realTableGroup = tableGroup.isRealTableGroup() - && ( CollectionHelper.isNotEmpty( tableGroup.getTableReferenceJoins() ) - || hasNestedTableGroupsToRender( tableGroup.getNestedTableGroupJoins() ) ); + final boolean realTableGroup; + int swappedJoinIndex = -1; + boolean forceLeftJoin = false; + if ( tableGroup.isRealTableGroup() ) { + if ( hasNestedTableGroupsToRender( tableGroup.getNestedTableGroupJoins() ) ) { + // If there are nested table groups, we need to render a real table group + realTableGroup = true; + } + else { + // Determine the reference join indexes of the table reference used in the predicate + final int referenceJoinIndexForPredicateSwap = TableGroupHelper.findReferenceJoinForPredicateSwap( + tableGroup, + predicate + ); + if ( referenceJoinIndexForPredicateSwap == TableGroupHelper.REAL_TABLE_GROUP_REQUIRED ) { + // Means that real table group rendering is necessary + realTableGroup = true; + } + else if ( referenceJoinIndexForPredicateSwap == TableGroupHelper.NO_TABLE_GROUP_REQUIRED ) { + // Means that no swap is necessary to avoid the table group rendering + realTableGroup = false; + forceLeftJoin = !tableGroup.canUseInnerJoins(); + } + else { + // Means that real table group rendering can be avoided if the primary table reference is swapped + // with the table reference join at the given index + realTableGroup = false; + forceLeftJoin = !tableGroup.canUseInnerJoins(); + swappedJoinIndex = referenceJoinIndexForPredicateSwap; + + // Render the table reference of the table reference join first + final TableReferenceJoin tableReferenceJoin = tableGroup.getTableReferenceJoins().get( swappedJoinIndex ); + renderNamedTableReference( tableReferenceJoin.getJoinedTableReference(), LockMode.NONE ); + // along with the predicate for the table group + if ( predicate != null ) { + appendSql( " on " ); + predicate.accept( this ); + } + + // Then render the join syntax and fall through to rendering the primary table reference + appendSql( WHITESPACE ); + if ( tableGroup.canUseInnerJoins() ) { + appendSql( tableReferenceJoin.getJoinType().getText() ); + } + else { + append( "left " ); + } + appendSql( "join " ); + } + } + } + else { + realTableGroup = false; + } if ( realTableGroup ) { appendSql( OPEN_PARENTHESIS ); } @@ -6066,7 +6117,8 @@ protected void renderTableGroup(TableGroup tableGroup, Predicate predicate, List tableGroupJoins = null; } - if ( predicate != null ) { + // Predicate was already rendered when swappedJoinIndex is not equal to -1 + if ( predicate != null && swappedJoinIndex == -1 ) { appendSql( " on " ); predicate.accept( this ); } @@ -6084,7 +6136,7 @@ protected void renderTableGroup(TableGroup tableGroup, Predicate predicate, List } if ( !realTableGroup ) { - renderTableReferenceJoins( tableGroup ); + renderTableReferenceJoins( tableGroup, swappedJoinIndex, forceLeftJoin ); processNestedTableGroupJoins( tableGroup, tableGroupJoinCollector ); } if ( tableGroupJoinCollector != null ) { @@ -6363,21 +6415,43 @@ protected void registerAffectedTable(String tableExpression) { } protected void renderTableReferenceJoins(TableGroup tableGroup) { + renderTableReferenceJoins( tableGroup, -1, false ); + } + + protected void renderTableReferenceJoins(TableGroup tableGroup, int swappedJoinIndex, boolean forceLeftJoin) { final List joins = tableGroup.getTableReferenceJoins(); if ( joins == null || joins.isEmpty() ) { return; } - for ( TableReferenceJoin tableJoin : joins ) { - appendSql( WHITESPACE ); - appendSql( tableJoin.getJoinType().getText() ); - appendSql( "join " ); + if ( swappedJoinIndex != -1 ) { + // Finish the join against the primary table reference after the swap + final TableReferenceJoin swappedJoin = joins.get( swappedJoinIndex ); + if ( swappedJoin.getPredicate() != null && !swappedJoin.getPredicate().isEmpty() ) { + appendSql( " on " ); + swappedJoin.getPredicate().accept( this ); + } + } - renderNamedTableReference( tableJoin.getJoinedTableReference(), LockMode.NONE ); + for ( int i = 0; i < joins.size(); i++ ) { + // Skip the swapped join since it was already rendered + if ( swappedJoinIndex != i ) { + final TableReferenceJoin tableJoin = joins.get( i ); + appendSql( WHITESPACE ); + if ( forceLeftJoin ) { + append( "left " ); + } + else { + appendSql( tableJoin.getJoinType().getText() ); + } + appendSql( "join " ); - if ( tableJoin.getPredicate() != null && !tableJoin.getPredicate().isEmpty() ) { - appendSql( " on " ); - tableJoin.getPredicate().accept( this ); + renderNamedTableReference( tableJoin.getJoinedTableReference(), LockMode.NONE ); + + if ( tableJoin.getPredicate() != null && !tableJoin.getPredicate().isEmpty() ) { + appendSql( " on " ); + tableJoin.getPredicate().accept( this ); + } } } } @@ -6779,9 +6853,12 @@ private int getSortSelectionIndex(QuerySpec querySpec, SortSpecification sortSpe private boolean isFetchFirstRowOnly(QueryPart queryPart) { return queryPart.getFetchClauseType() == FetchClauseType.ROWS_ONLY - && queryPart.getFetchClauseExpression() instanceof QueryLiteral - && Integer.valueOf( 1 ) - .equals( ( (QueryLiteral) queryPart.getFetchClauseExpression() ).getLiteralValue() ); + && queryPart.getFetchClauseExpression() != null + && Integer.valueOf( 1 ).equals( getLiteralValue( queryPart.getFetchClauseExpression() ) ); + } + + public X getLiteralValue(Expression expression) { + return interpretExpression( expression, jdbcParameterBindings ); } private SelectStatement stripToSelectClause(SelectStatement statement) { @@ -8570,11 +8647,11 @@ protected String getFromDual() { * @return the SQL equivalent to Oracle's {@code dual}. */ protected String getDual() { - return "(values(0))"; + return dialect.getDual(); } protected String getFromDualForSelectOnly() { - return ""; + return dialect.getFromDualForSelectOnly(); } protected enum LockStrategy { diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/SqlAstCreationState.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/SqlAstCreationState.java index 8d3bfe0d23b6..4ae6d801e270 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/SqlAstCreationState.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/SqlAstCreationState.java @@ -9,6 +9,7 @@ import org.hibernate.Internal; import org.hibernate.LockMode; import org.hibernate.engine.spi.LoadQueryInfluencers; +import org.hibernate.metamodel.mapping.ordering.OrderByFragment; import org.hibernate.persister.entity.EntityNameUse; import org.hibernate.sql.ast.tree.from.TableGroup; @@ -49,4 +50,8 @@ default void registerEntityNameUsage( default boolean supportsEntityNameUsage() { return false; } + + @Internal + default void applyOrdering(TableGroup tableGroup, OrderByFragment orderByFragment) { + } } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/OneToManyTableGroup.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/OneToManyTableGroup.java index 45b250520c04..5d5b06ef8480 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/OneToManyTableGroup.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/OneToManyTableGroup.java @@ -63,9 +63,18 @@ public TableGroup getIndexTableGroup() { } public void registerIndexTableGroup(TableGroupJoin indexTableGroupJoin) { + registerIndexTableGroup( indexTableGroupJoin, true ); + } + + public void registerIndexTableGroup(TableGroupJoin indexTableGroupJoin, boolean nested) { assert this.indexTableGroup == null; this.indexTableGroup = indexTableGroupJoin.getJoinedGroup(); - addNestedTableGroupJoin( indexTableGroupJoin ); + if ( nested ) { + addNestedTableGroupJoin( indexTableGroupJoin ); + } + else { + addTableGroupJoin( indexTableGroupJoin ); + } } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/TableGroupJoinProducer.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/TableGroupJoinProducer.java index 80c5a392f01e..d2c795232d6b 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/TableGroupJoinProducer.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/TableGroupJoinProducer.java @@ -12,10 +12,11 @@ import org.hibernate.sql.ast.SqlAstJoinType; import org.hibernate.sql.ast.spi.SqlAliasBase; import org.hibernate.sql.ast.spi.SqlAliasBaseGenerator; -import org.hibernate.sql.ast.spi.SqlAstCreationContext; import org.hibernate.sql.ast.spi.SqlAstCreationState; import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.checkerframework.checker.nullness.qual.Nullable; + /** * @author Steve Ebersole */ @@ -41,9 +42,9 @@ public interface TableGroupJoinProducer extends TableGroupProducer { TableGroupJoin createTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType sqlAstJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType sqlAstJoinType, boolean fetched, boolean addsPredicate, SqlAstCreationState creationState); @@ -70,14 +71,14 @@ TableGroupJoin createTableGroupJoin( TableGroup createRootTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType sqlAstJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType sqlAstJoinType, boolean fetched, - Consumer predicateConsumer, + @Nullable Consumer predicateConsumer, SqlAstCreationState creationState); - default SqlAstJoinType determineSqlJoinType(TableGroup lhs, SqlAstJoinType requestedJoinType, boolean fetched) { + default SqlAstJoinType determineSqlJoinType(TableGroup lhs, @Nullable SqlAstJoinType requestedJoinType, boolean fetched) { if ( requestedJoinType != null ) { return requestedJoinType; } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/AbstractJdbcParameter.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/AbstractJdbcParameter.java index aa8e1d668365..dea2c41ed899 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/AbstractJdbcParameter.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/AbstractJdbcParameter.java @@ -18,7 +18,6 @@ import org.hibernate.metamodel.mapping.MappingModelExpressible; import org.hibernate.metamodel.mapping.MappingType; import org.hibernate.metamodel.mapping.SqlExpressible; -import org.hibernate.metamodel.model.domain.internal.BasicTypeImpl; import org.hibernate.query.BindableType; import org.hibernate.sql.ast.SqlAstWalker; import org.hibernate.sql.ast.tree.expression.JdbcParameter; @@ -27,6 +26,7 @@ import org.hibernate.sql.exec.spi.JdbcParameterBinder; import org.hibernate.sql.exec.spi.JdbcParameterBinding; import org.hibernate.sql.exec.spi.JdbcParameterBindings; +import org.hibernate.type.BasicType; import org.hibernate.type.descriptor.java.EnumJavaType; import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators; @@ -135,7 +135,7 @@ private JdbcMapping guessBindType(ExecutionContext executionContext, Object bind } } - private static > BasicTypeImpl createEnumType(ExecutionContext executionContext, Class enumClass) { + private static > BasicType createEnumType(ExecutionContext executionContext, Class enumClass) { final EnumJavaType enumJavaType = new EnumJavaType<>( enumClass ); final JdbcTypeIndicators indicators = executionContext.getSession().getTypeConfiguration().getCurrentBaseSqlTypeIndicators(); @@ -144,7 +144,7 @@ private static > BasicTypeImpl createEnumType(ExecutionCont // so just accept the default from the TypeConfiguration, which // is usually ORDINAL (the default according to JPA) enumJavaType.getRecommendedJdbcType(indicators); - return new BasicTypeImpl<>( enumJavaType, jdbcType ); + return indicators.getTypeConfiguration().getBasicTypeRegistry().resolve( enumJavaType, jdbcType ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/DeleteOrUpsertOperation.java b/hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/DeleteOrUpsertOperation.java index 2b629a707ff6..9c8ad687fcdf 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/DeleteOrUpsertOperation.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/DeleteOrUpsertOperation.java @@ -115,19 +115,25 @@ private void performDelete(JdbcValueBindings jdbcValueBindings, SharedSessionCon final PreparedStatementGroupSingleTable statementGroup = new PreparedStatementGroupSingleTable( upsertDelete, session ); final PreparedStatementDetails statementDetails = statementGroup.resolvePreparedStatementDetails( tableMapping.getTableName() ); - final PreparedStatement upsertDeleteStatement = statementDetails.resolveStatement(); - session.getJdbcServices().getSqlStatementLogger().logStatement( statementDetails.getSqlString() ); - - bindDeleteKeyValues( - jdbcValueBindings, - optionalTableUpdate.getParameters(), - statementDetails, - session - ); - final int rowCount = session.getJdbcCoordinator().getResultSetReturn() - .executeUpdate( upsertDeleteStatement, statementDetails.getSqlString() ); - MODEL_MUTATION_LOGGER.tracef( "`%s` rows upsert-deleted from `%s`", rowCount, tableMapping.getTableName() ); + try { + final PreparedStatement upsertDeleteStatement = statementDetails.resolveStatement(); + session.getJdbcServices().getSqlStatementLogger().logStatement( statementDetails.getSqlString() ); + + bindDeleteKeyValues( + jdbcValueBindings, + optionalTableUpdate.getParameters(), + statementDetails, + session + ); + + final int rowCount = session.getJdbcCoordinator().getResultSetReturn() + .executeUpdate( upsertDeleteStatement, statementDetails.getSqlString() ); + MODEL_MUTATION_LOGGER.tracef( "`%s` rows upsert-deleted from `%s`", rowCount, tableMapping.getTableName() ); + } + finally { + statementDetails.releaseStatement( session ); + } } private void bindDeleteKeyValues( @@ -189,14 +195,19 @@ private void performUpsert(JdbcValueBindings jdbcValueBindings, SharedSessionCon final PreparedStatementGroupSingleTable statementGroup = new PreparedStatementGroupSingleTable( upsertOperation, session ); final PreparedStatementDetails statementDetails = statementGroup.resolvePreparedStatementDetails( tableMapping.getTableName() ); - final PreparedStatement updateStatement = statementDetails.resolveStatement(); - session.getJdbcServices().getSqlStatementLogger().logStatement( statementDetails.getSqlString() ); + try { + final PreparedStatement updateStatement = statementDetails.resolveStatement(); + session.getJdbcServices().getSqlStatementLogger().logStatement( statementDetails.getSqlString() ); - jdbcValueBindings.beforeStatement( statementDetails ); + jdbcValueBindings.beforeStatement( statementDetails ); - final int rowCount = session.getJdbcCoordinator().getResultSetReturn() - .executeUpdate( updateStatement, statementDetails.getSqlString() ); + final int rowCount = session.getJdbcCoordinator().getResultSetReturn() + .executeUpdate( updateStatement, statementDetails.getSqlString() ); - MODEL_MUTATION_LOGGER.tracef( "`%s` rows upserted into `%s`", rowCount, tableMapping.getTableName() ); + MODEL_MUTATION_LOGGER.tracef( "`%s` rows upserted into `%s`", rowCount, tableMapping.getTableName() ); + } + finally { + statementDetails.releaseStatement( session ); + } } } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/OptionalTableUpdateOperation.java b/hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/OptionalTableUpdateOperation.java index 30d35748c942..d6f7f11b589d 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/OptionalTableUpdateOperation.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/OptionalTableUpdateOperation.java @@ -172,13 +172,20 @@ public void performMutation( private void performDelete(JdbcValueBindings jdbcValueBindings, SharedSessionContractImplementor session) { final JdbcDeleteMutation jdbcDelete = createJdbcDelete( session ); - final PreparedStatement deleteStatement = createStatementDetails( jdbcDelete, session ); + final JdbcCoordinator jdbcCoordinator = session.getJdbcCoordinator(); + final PreparedStatement deleteStatement = createStatementDetails( jdbcDelete, jdbcCoordinator ); session.getJdbcServices().getSqlStatementLogger().logStatement( jdbcDelete.getSqlString() ); bindKeyValues( jdbcValueBindings, deleteStatement, jdbcDelete, session ); - session.getJdbcCoordinator().getResultSetReturn() - .executeUpdate( deleteStatement, jdbcDelete.getSqlString() ); + try { + session.getJdbcCoordinator().getResultSetReturn() + .executeUpdate( deleteStatement, jdbcDelete.getSqlString() ); + } + finally { + jdbcCoordinator.getLogicalConnection().getResourceRegistry().release( deleteStatement ); + jdbcCoordinator.afterStatementExecution(); + } } private void bindKeyValues( @@ -374,12 +381,16 @@ private boolean performUpdate( statementDetails.getSqlString() ); } + finally { + statementDetails.releaseStatement( session ); + } } private void performInsert(JdbcValueBindings jdbcValueBindings, SharedSessionContractImplementor session) { final JdbcInsertMutation jdbcInsert = createJdbcInsert( session ); - final PreparedStatement insertStatement = createStatementDetails( jdbcInsert, session ); + final JdbcCoordinator jdbcCoordinator = session.getJdbcCoordinator(); + final PreparedStatement insertStatement = createStatementDetails( jdbcInsert, jdbcCoordinator ); try { session.getJdbcServices().getSqlStatementLogger().logStatement( jdbcInsert.getSqlString() ); @@ -408,11 +419,12 @@ private void performInsert(JdbcValueBindings jdbcValueBindings, SharedSessionCon } ); } - session.getJdbcCoordinator().getResultSetReturn() + jdbcCoordinator.getResultSetReturn() .executeUpdate( insertStatement, jdbcInsert.getSqlString() ); } finally { - session.getJdbcCoordinator().getLogicalConnection().getResourceRegistry().release( insertStatement ); + jdbcCoordinator.getLogicalConnection().getResourceRegistry().release( insertStatement ); + jdbcCoordinator.afterStatementExecution(); } } @@ -453,11 +465,10 @@ private JdbcInsertMutation createJdbcInsert(SharedSessionContractImplementor ses private static PreparedStatement createStatementDetails( PreparableMutationOperation operation, - SharedSessionContractImplementor session) { - final JdbcCoordinator jdbcCoordinator = session.getJdbcCoordinator(); + JdbcCoordinator jdbcCoordinator) { final MutationStatementPreparer statementPreparer = jdbcCoordinator.getMutationStatementPreparer(); final PreparedStatement statement = statementPreparer.prepareStatement( operation.getSqlString(), false ); - session.getJdbcCoordinator().getLogicalConnection().getResourceRegistry().register( null, statement ); + jdbcCoordinator.getLogicalConnection().getResourceRegistry().register( null, statement ); return statement; } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/collection/internal/ArrayInitializer.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/collection/internal/ArrayInitializer.java index 413221ae7c02..1417600a7f73 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/collection/internal/ArrayInitializer.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/collection/internal/ArrayInitializer.java @@ -6,6 +6,7 @@ */ package org.hibernate.sql.results.graph.collection.internal; +import java.lang.reflect.Array; import java.util.Iterator; import java.util.List; import java.util.function.BiConsumer; @@ -131,9 +132,11 @@ protected void resolveInstanceSubInitializers(ImmediateCollectionInitializerData final Initializer initializer = elementAssembler.getInitializer(); if ( initializer != null ) { final RowProcessingState rowProcessingState = data.getRowProcessingState(); - final Iterator iter = getCollectionInstance( data ).elements(); - while ( iter.hasNext() ) { - initializer.resolveInstance( iter.next(), rowProcessingState ); + final Integer index = listIndexAssembler.assemble( rowProcessingState ); + if ( index != null ) { + final PersistentArrayHolder arrayHolder = getCollectionInstance( data ); + assert arrayHolder != null; + initializer.resolveInstance( Array.get( arrayHolder.getArray(), index ), rowProcessingState ); } } } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/collection/internal/BagInitializer.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/collection/internal/BagInitializer.java index 00e207c4d4a9..b73643cd4711 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/collection/internal/BagInitializer.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/collection/internal/BagInitializer.java @@ -123,19 +123,7 @@ protected void initializeSubInstancesFromParent(ImmediateCollectionInitializerDa protected void resolveInstanceSubInitializers(ImmediateCollectionInitializerData data) { final Initializer initializer = elementAssembler.getInitializer(); if ( initializer != null ) { - final RowProcessingState rowProcessingState = data.getRowProcessingState(); - final PersistentCollection persistentCollection = getCollectionInstance( data ); - assert persistentCollection != null; - if ( persistentCollection instanceof PersistentBag ) { - for ( Object element : ( (PersistentBag) persistentCollection ) ) { - initializer.resolveInstance( element, rowProcessingState ); - } - } - else { - for ( Object element : ( (PersistentIdentifierBag) persistentCollection ) ) { - initializer.resolveInstance( element, rowProcessingState ); - } - } + initializer.resolveKey( data.getRowProcessingState() ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/collection/internal/ListInitializer.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/collection/internal/ListInitializer.java index 0850a0927df4..67e9c16a74fb 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/collection/internal/ListInitializer.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/collection/internal/ListInitializer.java @@ -123,10 +123,11 @@ protected void resolveInstanceSubInitializers(ImmediateCollectionInitializerData final Initializer initializer = elementAssembler.getInitializer(); if ( initializer != null ) { final RowProcessingState rowProcessingState = data.getRowProcessingState(); - final PersistentList list = getCollectionInstance( data ); - assert list != null; - for ( Object element : list ) { - initializer.resolveInstance( element, rowProcessingState ); + final Integer index = listIndexAssembler.assemble( rowProcessingState ); + if ( index != null ) { + final PersistentList list = getCollectionInstance( data ); + assert list != null; + initializer.resolveInstance( list.get( index ), rowProcessingState ); } } } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/collection/internal/MapInitializer.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/collection/internal/MapInitializer.java index 8c4f36185f31..49c21cc96579 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/collection/internal/MapInitializer.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/collection/internal/MapInitializer.java @@ -121,17 +121,27 @@ protected void initializeSubInstancesFromParent(ImmediateCollectionInitializerDa protected void resolveInstanceSubInitializers(ImmediateCollectionInitializerData data) { final Initializer keyInitializer = mapKeyAssembler.getInitializer(); final Initializer valueInitializer = mapValueAssembler.getInitializer(); - if ( keyInitializer != null || valueInitializer != null ) { - final RowProcessingState rowProcessingState = data.getRowProcessingState(); - final PersistentMap map = getCollectionInstance( data ); - assert map != null; - for ( Map.Entry entry : map.entrySet() ) { - if ( keyInitializer != null ) { - keyInitializer.resolveInstance( entry.getKey(), rowProcessingState ); - } - if ( valueInitializer != null ) { - valueInitializer.resolveInstance( entry.getValue(), rowProcessingState ); - } + final RowProcessingState rowProcessingState = data.getRowProcessingState(); + if ( keyInitializer == null && valueInitializer != null ) { + // For now, we only support resolving the value initializer instance when keys have no initializer, + // though we could also support map keys with an initializer given that the initialized java type: + // * is an entity that uses only the primary key in equals/hashCode. + // If the primary key type is an embeddable, the next condition must hold for that + // * or is an embeddable that has no initializers for fields being used in the equals/hashCode + // which violate this same requirement (recursion) + final Object key = mapKeyAssembler.assemble( rowProcessingState ); + if ( key != null ) { + final PersistentMap map = getCollectionInstance( data ); + assert map != null; + valueInitializer.resolveInstance( map.get( key ), rowProcessingState ); + } + } + else { + if ( keyInitializer != null ) { + keyInitializer.resolveKey( rowProcessingState ); + } + if ( valueInitializer != null ) { + valueInitializer.resolveKey( rowProcessingState ); } } } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/collection/internal/SetInitializer.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/collection/internal/SetInitializer.java index b2b86d88cc17..30909c6ce02d 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/collection/internal/SetInitializer.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/collection/internal/SetInitializer.java @@ -97,12 +97,7 @@ protected void initializeSubInstancesFromParent(ImmediateCollectionInitializerDa protected void resolveInstanceSubInitializers(ImmediateCollectionInitializerData data) { final Initializer initializer = elementAssembler.getInitializer(); if ( initializer != null ) { - final RowProcessingState rowProcessingState = data.getRowProcessingState(); - final PersistentSet set = getCollectionInstance( data ); - assert set != null; - for ( Object element : set ) { - initializer.resolveInstance( element, rowProcessingState ); - } + initializer.resolveKey( data.getRowProcessingState() ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/AbstractBatchEntitySelectFetchInitializer.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/AbstractBatchEntitySelectFetchInitializer.java index 793a5451b15c..5ac6c90bcdae 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/AbstractBatchEntitySelectFetchInitializer.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/AbstractBatchEntitySelectFetchInitializer.java @@ -10,10 +10,8 @@ import org.hibernate.FetchNotFoundException; import org.hibernate.Hibernate; import org.hibernate.annotations.NotFoundAction; -import org.hibernate.engine.spi.EntityHolder; -import org.hibernate.engine.spi.EntityKey; -import org.hibernate.engine.spi.PersistenceContext; -import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.bytecode.enhance.spi.interceptor.EnhancementAsProxyLazinessInterceptor; +import org.hibernate.engine.spi.*; import org.hibernate.metamodel.mapping.AttributeMapping; import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.internal.ToOneAttributeMapping; @@ -31,7 +29,9 @@ import org.checkerframework.checker.nullness.qual.Nullable; +import static org.hibernate.engine.internal.ManagedTypeHelper.isPersistentAttributeInterceptable; import static org.hibernate.proxy.HibernateProxy.extractLazyInitializer; +import static org.hibernate.sql.results.graph.entity.internal.EntityInitializerImpl.getAttributeInterceptor; public abstract class AbstractBatchEntitySelectFetchInitializer extends EntitySelectFetchInitializer implements EntityInitializer { @@ -111,6 +111,10 @@ public void resolveInstance(Data data) { return; } } + resolveInstanceFromIdentifier( data ); + } + + protected void resolveInstanceFromIdentifier(Data data) { if ( data.batchDisabled ) { initialize( data ); } @@ -137,21 +141,36 @@ public void resolveInstance(Object instance, Data data) { // Only need to extract the identifier if the identifier has a many to one final LazyInitializer lazyInitializer = extractLazyInitializer( instance ); data.entityKey = null; + data.entityIdentifier = null; if ( lazyInitializer == null ) { - // Entity is initialized - data.setState( State.INITIALIZED ); - if ( keyIsEager ) { + // Entity is most probably initialized + data.setInstance( instance ); + final PersistentAttributeInterceptor interceptor; + if ( concreteDescriptor.getBytecodeEnhancementMetadata().isEnhancedForLazyLoading() + && isPersistentAttributeInterceptable( instance ) + && ( interceptor = getAttributeInterceptor( instance ) ) instanceof EnhancementAsProxyLazinessInterceptor ) { + final EnhancementAsProxyLazinessInterceptor enhancementInterceptor = (EnhancementAsProxyLazinessInterceptor) interceptor; + if ( enhancementInterceptor.isInitialized() ) { + data.setState( State.INITIALIZED ); + } + else { + data.setState( State.RESOLVED ); + data.entityIdentifier = enhancementInterceptor.getIdentifier(); + } + } + else { + // If the entity initializer is null, we know the entity is fully initialized, + // otherwise it will be initialized by some other initializer + data.setState( State.RESOLVED ); + data.entityIdentifier = concreteDescriptor.getIdentifier( instance, rowProcessingState.getSession() ); + } + if ( keyIsEager && data.entityIdentifier == null ) { data.entityIdentifier = concreteDescriptor.getIdentifier( instance, rowProcessingState.getSession() ); } - data.setInstance( instance ); } else if ( lazyInitializer.isUninitialized() ) { data.setState( State.RESOLVED ); - if ( keyIsEager ) { - data.entityIdentifier = lazyInitializer.getIdentifier(); - } - // Resolve and potentially create the entity instance - registerToBatchFetchQueue( data ); + data.entityIdentifier = lazyInitializer.getIdentifier(); } else { // Entity is initialized @@ -161,6 +180,10 @@ else if ( lazyInitializer.isUninitialized() ) { } data.setInstance( lazyInitializer.getImplementation() ); } + + if ( data.getState() == State.RESOLVED ) { + resolveInstanceFromIdentifier( data ); + } if ( keyIsEager ) { final Initializer initializer = keyAssembler.getInitializer(); assert initializer != null; diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/BatchEntitySelectFetchInitializer.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/BatchEntitySelectFetchInitializer.java index 3eb69b6cd0b6..a5b623fe5a52 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/BatchEntitySelectFetchInitializer.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/BatchEntitySelectFetchInitializer.java @@ -67,14 +67,17 @@ protected InitializerData createInitializerData(RowProcessingState rowProcessing protected void registerResolutionListener(BatchEntitySelectFetchInitializerData data) { final RowProcessingState rowProcessingState = data.getRowProcessingState(); final InitializerData owningData = owningEntityInitializer.getData( rowProcessingState ); + HashMap> toBatchLoad = data.toBatchLoad; + if ( toBatchLoad == null ) { + toBatchLoad = data.toBatchLoad = new HashMap<>(); + } + // Always register the entity key for resolution + final List parentInfos = toBatchLoad.computeIfAbsent( data.entityKey, key -> new ArrayList<>() ); final AttributeMapping parentAttribute; + // But only add the parent info if the parent entity is not already initialized if ( owningData.getState() != State.INITIALIZED && ( parentAttribute = parentAttributes[owningEntityInitializer.getConcreteDescriptor( owningData ).getSubclassId()] ) != null ) { - HashMap> toBatchLoad = data.toBatchLoad; - if ( toBatchLoad == null ) { - toBatchLoad = data.toBatchLoad = new HashMap<>(); - } - toBatchLoad.computeIfAbsent( data.entityKey, key -> new ArrayList<>() ).add( + parentInfos.add( new ParentInfo( owningEntityInitializer.getTargetInstance( owningData ), parentAttribute.getStateArrayPosition() diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityDelayedFetchInitializer.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityDelayedFetchInitializer.java index bdcacb08ba82..7f081d1ecc41 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityDelayedFetchInitializer.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityDelayedFetchInitializer.java @@ -9,12 +9,17 @@ import java.util.function.BiConsumer; import org.hibernate.FetchNotFoundException; -import org.hibernate.bytecode.enhance.spi.LazyPropertyInitializer; +import org.hibernate.bytecode.enhance.spi.interceptor.LazyAttributeLoadingInterceptor; +import org.hibernate.engine.internal.ManagedTypeHelper; import org.hibernate.engine.spi.EntityHolder; import org.hibernate.engine.spi.EntityKey; import org.hibernate.engine.spi.EntityUniqueKey; import org.hibernate.engine.spi.PersistenceContext; +import org.hibernate.engine.spi.PersistentAttributeInterceptable; import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.graph.GraphSemantic; +import org.hibernate.graph.spi.AppliedGraph; +import org.hibernate.graph.spi.AttributeNodeImplementor; import org.hibernate.internal.log.LoggingHelper; import org.hibernate.metamodel.mapping.ModelPart; import org.hibernate.metamodel.mapping.internal.ToOneAttributeMapping; @@ -37,6 +42,7 @@ import org.checkerframework.checker.nullness.qual.Nullable; +import static org.hibernate.bytecode.enhance.spi.LazyPropertyInitializer.UNFETCHED_PROPERTY; import static org.hibernate.sql.results.graph.entity.internal.EntityInitializerImpl.determineConcreteEntityDescriptor; /** @@ -193,19 +199,36 @@ public void resolveInstance(EntityDelayedFetchInitializerData data) { // For unique-key mappings, we always use bytecode-laziness if possible, // because we can't generate a proxy based on the unique key yet if ( referencedModelPart.isLazy() ) { - instance = LazyPropertyInitializer.UNFETCHED_PROPERTY; + instance = UNFETCHED_PROPERTY; } else { - instance = concreteDescriptor.loadByUniqueKey( - uniqueKeyPropertyName, - data.entityIdentifier, - session - ); - - // If the entity was not in the Persistence Context, but was found now, - // add it to the Persistence Context - if ( instance != null ) { - persistenceContext.addEntity( euk, instance ); + // Try to load a PersistentAttributeInterceptable. If we get one, we can add the lazy + // field to the interceptor. If we don't get one, we load the entity by unique key. + PersistentAttributeInterceptable persistentAttributeInterceptable = null; + if ( getParent().isEntityInitializer() && isLazyByGraph( rowProcessingState ) ) { + final Object resolvedInstance = + getParent().asEntityInitializer().getResolvedInstance( rowProcessingState ); + persistentAttributeInterceptable = + ManagedTypeHelper.asPersistentAttributeInterceptableOrNull( resolvedInstance ); + } + + if ( persistentAttributeInterceptable != null ) { + final LazyAttributeLoadingInterceptor persistentAttributeInterceptor = (LazyAttributeLoadingInterceptor) persistentAttributeInterceptable.$$_hibernate_getInterceptor(); + persistentAttributeInterceptor.addLazyFieldByGraph( navigablePath.getLocalName() ); + instance = UNFETCHED_PROPERTY; + } + else { + instance = concreteDescriptor.loadByUniqueKey( + uniqueKeyPropertyName, + data.entityIdentifier, + session + ); + + // If the entity was not in the Persistence Context, but was found now, + // add it to the Persistence Context + if ( instance != null ) { + persistenceContext.addEntity( euk, instance ); + } } } } @@ -224,7 +247,7 @@ public void resolveInstance(EntityDelayedFetchInitializerData data) { // For primary key based mappings we only use bytecode-laziness if the attribute is optional, // because the non-optionality implies that it is safe to have a proxy else if ( referencedModelPart.isOptional() && referencedModelPart.isLazy() ) { - instance = LazyPropertyInitializer.UNFETCHED_PROPERTY; + instance = UNFETCHED_PROPERTY; } else { instance = session.internalLoad( @@ -244,6 +267,19 @@ else if ( referencedModelPart.isOptional() && referencedModelPart.isLazy() ) { } } + private boolean isLazyByGraph(RowProcessingState rowProcessingState) { + final AppliedGraph appliedGraph = rowProcessingState.getQueryOptions().getAppliedGraph(); + if ( appliedGraph != null && appliedGraph.getSemantic() == GraphSemantic.FETCH ) { + final AttributeNodeImplementor attributeNode = appliedGraph.getGraph() + .findAttributeNode( navigablePath.getLocalName() ); + if ( attributeNode != null && attributeNode.getAttributeDescriptor() == getInitializedPart().asAttributeMapping() ) { + return false; + } + return true; + } + return false; + } + @Override public void resolveInstance(Object instance, EntityDelayedFetchInitializerData data) { if ( instance == null ) { diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityInitializerImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityInitializerImpl.java index fac0a5c17889..5ed94e2cae28 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityInitializerImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityInitializerImpl.java @@ -25,6 +25,7 @@ import org.hibernate.cache.spi.access.AccessType; import org.hibernate.cache.spi.access.EntityDataAccess; import org.hibernate.cache.spi.entry.CacheEntry; +import org.hibernate.engine.internal.ForeignKeys; import org.hibernate.engine.spi.EntityEntry; import org.hibernate.engine.spi.EntityHolder; import org.hibernate.engine.spi.EntityKey; @@ -77,6 +78,7 @@ import org.hibernate.sql.results.jdbc.spi.JdbcValuesSourceProcessingOptions; import org.hibernate.sql.results.jdbc.spi.RowProcessingState; import org.hibernate.stat.spi.StatisticsImplementor; +import org.hibernate.type.ManyToOneType; import org.hibernate.type.Type; import org.hibernate.type.descriptor.java.MutabilityPlan; @@ -86,8 +88,7 @@ import static org.hibernate.bytecode.enhance.spi.LazyPropertyInitializer.UNFETCHED_PROPERTY; import static org.hibernate.engine.internal.ManagedTypeHelper.asPersistentAttributeInterceptable; import static org.hibernate.engine.internal.ManagedTypeHelper.isPersistentAttributeInterceptable; -import static org.hibernate.internal.log.LoggingHelper.toLoggableString; -import static org.hibernate.metamodel.mapping.ForeignKeyDescriptor.Nature.TARGET; +import static org.hibernate.internal.util.NullnessUtil.castNonNull; import static org.hibernate.proxy.HibernateProxy.extractLazyInitializer; /** @@ -132,6 +133,7 @@ public class EntityInitializerImpl extends AbstractInitializer rowIdAssembler; private final DomainResultAssembler[][] assemblers; + private final @Nullable Initializer[] allInitializers; private final @Nullable Initializer[][] subInitializers; private final @Nullable Initializer[][] subInitializersForResolveFromInitialized; private final @Nullable Initializer[][] collectionContainingSubInitializers; @@ -278,8 +280,10 @@ public EntityInitializerImpl( ? rowIdResult.createResultAssembler( this, creationState ) : null; + final int fetchableCount = entityDescriptor.getNumberOfFetchables(); final Collection subMappingTypes = rootEntityDescriptor.getSubMappingTypes(); final DomainResultAssembler[][] assemblers = new DomainResultAssembler[subMappingTypes.size() + 1][]; + final Initializer[] allInitializers = new Initializer[fetchableCount]; final Initializer[][] subInitializers = new Initializer[subMappingTypes.size() + 1][]; final Initializer[][] eagerSubInitializers = new Initializer[subMappingTypes.size() + 1][]; final Initializer[][] collectionContainingSubInitializers = new Initializer[subMappingTypes.size() + 1][]; @@ -295,8 +299,7 @@ public EntityInitializerImpl( } boolean hasLazySubInitializers = false; - final int size = entityDescriptor.getNumberOfFetchables(); - for ( int i = 0; i < size; i++ ) { + for ( int i = 0; i < fetchableCount; i++ ) { final AttributeMapping attributeMapping = entityDescriptor.getFetchable( i ).asAttributeMapping(); final Fetch fetch = resultDescriptor.findFetch( attributeMapping ); final DomainResultAssembler stateAssembler = fetch == null @@ -309,12 +312,13 @@ public EntityInitializerImpl( final Initializer subInitializer = stateAssembler.getInitializer(); if ( subInitializer != null ) { + allInitializers[i] = subInitializer; if ( subInitializers[subclassId] == null ) { - subInitializers[subclassId] = new Initializer[size]; - eagerSubInitializers[subclassId] = new Initializer[size]; - collectionContainingSubInitializers[subclassId] = new Initializer[size]; - lazySets[subclassId] = new BitSet( size ); - maybeLazySets[subclassId] = new BitSet( size ); + subInitializers[subclassId] = new Initializer[fetchableCount]; + eagerSubInitializers[subclassId] = new Initializer[fetchableCount]; + collectionContainingSubInitializers[subclassId] = new Initializer[fetchableCount]; + lazySets[subclassId] = new BitSet( fetchableCount ); + maybeLazySets[subclassId] = new BitSet( fetchableCount ); } subInitializers[subclassId][stateArrayPosition] = subInitializer; if ( subInitializer.isEager() ) { @@ -348,11 +352,11 @@ public EntityInitializerImpl( updatableAttributeMutabilityPlans[subMappingType.getSubclassId()][stateArrayPosition] = updatableAttributeMutabilityPlans[subclassId][stateArrayPosition]; if ( subInitializer != null ) { if ( subInitializers[subMappingType.getSubclassId()] == null ) { - subInitializers[subMappingType.getSubclassId()] = new Initializer[size]; - eagerSubInitializers[subMappingType.getSubclassId()] = new Initializer[size]; - collectionContainingSubInitializers[subMappingType.getSubclassId()] = new Initializer[size]; - lazySets[subMappingType.getSubclassId()] = new BitSet(size); - maybeLazySets[subMappingType.getSubclassId()] = new BitSet(size); + subInitializers[subMappingType.getSubclassId()] = new Initializer[fetchableCount]; + eagerSubInitializers[subMappingType.getSubclassId()] = new Initializer[fetchableCount]; + collectionContainingSubInitializers[subMappingType.getSubclassId()] = new Initializer[fetchableCount]; + lazySets[subMappingType.getSubclassId()] = new BitSet(fetchableCount); + maybeLazySets[subMappingType.getSubclassId()] = new BitSet(fetchableCount); } subInitializers[subMappingType.getSubclassId()][stateArrayPosition] = subInitializer; eagerSubInitializers[subMappingType.getSubclassId()][stateArrayPosition] = eagerSubInitializers[subclassId][stateArrayPosition]; @@ -410,6 +414,7 @@ public EntityInitializerImpl( } this.assemblers = assemblers; + this.allInitializers = allInitializers; this.subInitializers = subInitializers; this.subInitializersForResolveFromInitialized = rootEntityDescriptor.getBytecodeEnhancementMetadata().isEnhancedForLazyLoading() ? subInitializers @@ -564,7 +569,7 @@ protected void resolveKey(EntityInitializerData data, boolean entityKeyOnly) { } if ( oldEntityKey != null && previousRowReuse && oldEntityInstance != null - && areKeysEqual( oldEntityKey.getIdentifier(), id ) ) { + && areKeysEqual( oldEntityKey.getIdentifier(), id ) && !oldEntityHolder.isDetached() ) { data.setState( State.INITIALIZED ); data.entityKey = oldEntityKey; data.setInstance( oldEntityInstance ); @@ -610,6 +615,8 @@ private boolean areKeysEqual(Object key1, Object key2) { protected void resolveInstanceSubInitializers(EntityInitializerData data) { final int subclassId = data.concreteDescriptor.getSubclassId(); final EntityEntry entityEntry = data.entityHolder.getEntityEntry(); + assert entityEntry != null : "This method should only be called if the entity is already initialized"; + final Initializer[] initializers; final ImmutableBitSet maybeLazySet; if ( data.entityHolder.getEntityInitializer() == this ) { @@ -627,9 +634,9 @@ protected void resolveInstanceSubInitializers(EntityInitializerData data) { } } final RowProcessingState rowProcessingState = data.getRowProcessingState(); - assert entityEntry == rowProcessingState.getSession() - .getPersistenceContextInternal() - .getEntry( data.entityInstanceForNotify ); + final PersistenceContext persistenceContext = rowProcessingState.getSession() + .getPersistenceContextInternal(); + assert entityEntry == persistenceContext.getEntry( data.entityInstanceForNotify ); final Object[] loadedState = entityEntry.getLoadedState(); final Object[] state; if ( loadedState == null ) { @@ -914,16 +921,36 @@ public void resolveInstance(Object instance, EntityInitializerData data) { ); data.entityHolder = persistenceContext.getEntityHolder( data.entityKey ); if ( data.entityHolder == null ) { - // Entity was most probably removed in the same session without setting the reference to null - resolveKey( data ); - assert data.getState() == State.MISSING; - assert referencedModelPart instanceof ToOneAttributeMapping - && ( (ToOneAttributeMapping) referencedModelPart ).getSideNature() == TARGET; - return; + // Entity was most probably removed in the same session without setting this association to null. + // Since this load request can happen through `find()` which doesn't auto-flush on association joins, + // the entity must be fully initialized, even if it is removed already + data.entityHolder = persistenceContext.claimEntityHolderIfPossible( + data.entityKey, + data.entityInstanceForNotify, + rowProcessingState.getJdbcValuesSourceProcessingState(), + this + ); + } + if ( data.concreteDescriptor.getBytecodeEnhancementMetadata().isEnhancedForLazyLoading() + && isPersistentAttributeInterceptable( data.entityInstanceForNotify ) + && getAttributeInterceptor( data.entityInstanceForNotify ) instanceof EnhancementAsProxyLazinessInterceptor + && !( (EnhancementAsProxyLazinessInterceptor) getAttributeInterceptor( data.entityInstanceForNotify ) ).isInitialized() ) { + data.setState( State.RESOLVED ); + } + else { + // If the entity initializer is null, we know the entity is fully initialized, + // otherwise it will be initialized by some other initializer + data.setState( data.entityHolder.getEntityInitializer() == null ? State.INITIALIZED : State.RESOLVED ); + } + + if ( data.getState() == State.RESOLVED ) { + data.entityHolder = persistenceContext.claimEntityHolderIfPossible( + data.entityKey, + data.entityInstanceForNotify, + rowProcessingState.getJdbcValuesSourceProcessingState(), + this + ); } - // If the entity initializer is null, we know the entity is fully initialized, - // otherwise it will be initialized by some other initializer - data.setState( data.entityHolder.getEntityInitializer() == null ? State.INITIALIZED : State.RESOLVED ); } else if ( lazyInitializer.isUninitialized() ) { data.setState( State.RESOLVED ); @@ -945,11 +972,13 @@ else if ( lazyInitializer.isUninitialized() ) { registerLoadingEntity( data, data.entityInstanceForNotify ); } else { - data.setState( State.INITIALIZED ); data.entityInstanceForNotify = lazyInitializer.getImplementation(); data.concreteDescriptor = session.getEntityPersister( null, data.entityInstanceForNotify ); resolveEntityKey( data, lazyInitializer.getIdentifier() ); data.entityHolder = persistenceContext.getEntityHolder( data.entityKey ); + // Even though the lazyInitializer reports it is initialized, check if the entity holder reports initialized, + // because in a nested initialization scenario, this nested initializer must initialize the entity + data.setState( data.entityHolder.isInitialized() ? State.INITIALIZED : State.RESOLVED ); } if ( identifierAssembler != null ) { final Initializer initializer = identifierAssembler.getInitializer(); @@ -1545,11 +1574,22 @@ protected void registerPossibleUniqueKeyEntries( // one used here, which it will be if ( resolvedEntityState[index] != null ) { + final Object key; + if ( type instanceof ManyToOneType ) { + key = ForeignKeys.getEntityIdentifierIfNotUnsaved( + ( (ManyToOneType) type ).getAssociatedEntityName(), + resolvedEntityState[index], + session + ); + } + else { + key = resolvedEntityState[index]; + } final EntityUniqueKey entityUniqueKey = new EntityUniqueKey( data.concreteDescriptor.getRootEntityDescriptor().getEntityName(), //polymorphism comment above ukName, - resolvedEntityState[index], + key, type, session.getFactory() ); @@ -1586,10 +1626,21 @@ public void resolveState(EntityInitializerData data) { if ( rowIdAssembler != null ) { rowIdAssembler.resolveState( data.getRowProcessingState() ); } + if ( data.concreteDescriptor == null ) { + data.concreteDescriptor = data.defaultConcreteDescriptor; + if ( data.concreteDescriptor == null ) { + data.concreteDescriptor = determineConcreteEntityDescriptor( + data.getRowProcessingState(), + castNonNull( discriminatorAssembler ), + entityDescriptor + ); + } + } resolveEntityState( data ); } protected void resolveEntityState(EntityInitializerData data) { + assert data.concreteDescriptor != null; final RowProcessingState rowProcessingState = data.getRowProcessingState(); for ( final DomainResultAssembler assembler : assemblers[data.concreteDescriptor.getSubclassId()] ) { if ( assembler != null ) { @@ -1715,11 +1766,9 @@ protected void forEachSubInitializer(BiConsumer, RowProcessingSta } final EntityInitializerData entityInitializerData = (EntityInitializerData) data; if ( entityInitializerData.concreteDescriptor == null ) { - for ( Initializer[] initializers : subInitializers ) { - for ( Initializer initializer : initializers ) { - if ( initializer != null ) { - consumer.accept( initializer, rowProcessingState ); - } + for ( Initializer initializer : allInitializers ) { + if ( initializer != null ) { + consumer.accept( initializer, rowProcessingState ); } } } @@ -1732,6 +1781,10 @@ protected void forEachSubInitializer(BiConsumer, RowProcessingSta } } + public static PersistentAttributeInterceptor getAttributeInterceptor(Object entity) { + return asPersistentAttributeInterceptable( entity ).$$_hibernate_getInterceptor(); + } + @Override public String toString() { return "EntityJoinedFetchInitializer(" + LoggingHelper.toLoggableString( getNavigablePath() ) + ")"; diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/internal/ResultsHelper.java b/hibernate-core/src/main/java/org/hibernate/sql/results/internal/ResultsHelper.java index 41f984c3c2b4..101991d67aae 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/internal/ResultsHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/internal/ResultsHelper.java @@ -49,6 +49,7 @@ import org.hibernate.sql.results.spi.RowReader; import org.hibernate.sql.results.spi.RowTransformer; import org.hibernate.stat.spi.StatisticsImplementor; +import org.hibernate.type.EntityType; /** * @author Steve Ebersole @@ -204,7 +205,7 @@ private static void addCollectionToCache( ); boolean isPutFromLoad = true; - if ( collectionDescriptor.getElementType().isAssociationType() ) { + if ( collectionDescriptor.getElementType() instanceof EntityType ) { final EntityPersister entityPersister = ( (QueryableCollection) collectionDescriptor ).getElementPersister(); for ( Object id : entry.getState() ) { if ( persistenceContext.wasInsertedDuringTransaction( entityPersister, id ) ) { diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/DeferredResultSetAccess.java b/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/DeferredResultSetAccess.java index 8965fb2603cd..dcf3e7ef7778 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/DeferredResultSetAccess.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/DeferredResultSetAccess.java @@ -17,6 +17,7 @@ import org.hibernate.dialect.Dialect; import org.hibernate.dialect.pagination.LimitHandler; import org.hibernate.dialect.pagination.NoopLimitHandler; +import org.hibernate.engine.jdbc.spi.JdbcCoordinator; import org.hibernate.engine.jdbc.spi.SqlStatementLogger; import org.hibernate.engine.spi.SessionEventListenerManager; import org.hibernate.engine.spi.SessionFactoryImplementor; @@ -324,8 +325,8 @@ protected LockMode determineFollowOnLockMode(LockOptions lockOptions) { @Override public void release() { - final LogicalConnectionImplementor logicalConnection = getPersistenceContext().getJdbcCoordinator() - .getLogicalConnection(); + final JdbcCoordinator jdbcCoordinator = getPersistenceContext().getJdbcCoordinator(); + final LogicalConnectionImplementor logicalConnection = jdbcCoordinator.getLogicalConnection(); if ( resultSet != null ) { logicalConnection.getResourceRegistry().release( resultSet, preparedStatement ); resultSet = null; @@ -334,9 +335,8 @@ public void release() { if ( preparedStatement != null ) { logicalConnection.getResourceRegistry().release( preparedStatement ); preparedStatement = null; + jdbcCoordinator.afterStatementExecution(); } - - logicalConnection.afterStatement(); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/JdbcValuesCacheHit.java b/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/JdbcValuesCacheHit.java index 62662d8f5e28..a2902861ea7c 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/JdbcValuesCacheHit.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/JdbcValuesCacheHit.java @@ -176,12 +176,12 @@ public Object getCurrentRowValue(int valueIndex) { if ( valueIndexesToCacheIndexes == null ) { return ( (Object[]) row )[valueIndex]; } - else if ( row.getClass() != Object[].class ) { - assert valueIndexesToCacheIndexes[valueIndex] == 0; - return row; + else if ( row instanceof Object[] ) { + return ( (Object[]) row )[valueIndexesToCacheIndexes[valueIndex]]; } else { - return ( (Object[]) row )[valueIndexesToCacheIndexes[valueIndex]]; + assert valueIndexesToCacheIndexes[valueIndex] == 0; + return row; } } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/JdbcValuesMappingProducerProviderStandard.java b/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/JdbcValuesMappingProducerProviderStandard.java index fa95470f6d4f..151bdb7fc1d8 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/JdbcValuesMappingProducerProviderStandard.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/JdbcValuesMappingProducerProviderStandard.java @@ -9,9 +9,15 @@ import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.query.results.ResultSetMapping; import org.hibernate.query.results.ResultSetMappingImpl; +import org.hibernate.sql.ast.spi.SqlSelection; +import org.hibernate.sql.ast.tree.select.QueryGroup; +import org.hibernate.sql.ast.tree.select.QueryPart; import org.hibernate.sql.ast.tree.select.SelectStatement; import org.hibernate.sql.results.jdbc.spi.JdbcValuesMappingProducer; import org.hibernate.sql.results.jdbc.spi.JdbcValuesMappingProducerProvider; +import org.hibernate.type.descriptor.jdbc.NullJdbcType; + +import java.util.List; /** * Standard JdbcValuesMappingProducerProvider implementation @@ -28,10 +34,20 @@ public class JdbcValuesMappingProducerProviderStandard implements JdbcValuesMapp public JdbcValuesMappingProducer buildMappingProducer( SelectStatement sqlAst, SessionFactoryImplementor sessionFactory) { - return new JdbcValuesMappingProducerStandard( - sqlAst.getQuerySpec().getSelectClause().getSqlSelections(), - sqlAst.getDomainResultDescriptors() - ); + return new JdbcValuesMappingProducerStandard( getSelections( sqlAst ), sqlAst.getDomainResultDescriptors() ); + } + + private static List getSelections(SelectStatement selectStatement) { + if ( selectStatement.getQueryPart() instanceof QueryGroup ) { + final QueryGroup queryGroup = (QueryGroup) selectStatement.getQueryPart(); + for ( QueryPart queryPart : queryGroup.getQueryParts() ) { + if ( !(queryPart.getFirstQuerySpec().getSelectClause().getSqlSelections() + .get( 0 ).getExpressionType().getSingleJdbcMapping().getJdbcType() instanceof NullJdbcType) ) { + return queryPart.getFirstQuerySpec().getSelectClause().getSqlSelections(); + } + } + } + return selectStatement.getQuerySpec().getSelectClause().getSqlSelections(); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/JdbcValuesMappingResolutionImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/JdbcValuesMappingResolutionImpl.java index 6fc595bbca4e..5abda091a405 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/JdbcValuesMappingResolutionImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/JdbcValuesMappingResolutionImpl.java @@ -6,7 +6,7 @@ */ package org.hibernate.sql.results.jdbc.internal; -import java.util.ArrayList; +import java.util.LinkedHashSet; import org.hibernate.sql.results.graph.DomainResultAssembler; import org.hibernate.sql.results.graph.Initializer; @@ -39,7 +39,7 @@ private JdbcValuesMappingResolutionImpl( } private static Initializer[] getResultInitializers(DomainResultAssembler[] resultAssemblers) { - final ArrayList> initializers = new ArrayList<>( resultAssemblers.length ); + final LinkedHashSet> initializers = new LinkedHashSet<>( resultAssemblers.length ); for ( DomainResultAssembler resultAssembler : resultAssemblers ) { resultAssembler.forEachResultAssembler( (initializer, list) -> list.add( initializer ), initializers ); } diff --git a/hibernate-core/src/main/java/org/hibernate/tool/schema/internal/SchemaCreatorImpl.java b/hibernate-core/src/main/java/org/hibernate/tool/schema/internal/SchemaCreatorImpl.java index 78ad8d519003..9afc9185e961 100644 --- a/hibernate-core/src/main/java/org/hibernate/tool/schema/internal/SchemaCreatorImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/tool/schema/internal/SchemaCreatorImpl.java @@ -60,7 +60,9 @@ import static org.hibernate.cfg.AvailableSettings.HBM2DDL_IMPORT_FILES; import static org.hibernate.cfg.AvailableSettings.HBM2DDL_LOAD_SCRIPT_SOURCE; import static org.hibernate.cfg.AvailableSettings.JAKARTA_HBM2DDL_LOAD_SCRIPT_SOURCE; +import static org.hibernate.cfg.SchemaToolingSettings.HBM2DDL_SKIP_DEFAULT_IMPORT_FILE; import static org.hibernate.internal.util.collections.CollectionHelper.setOfSize; +import static org.hibernate.internal.util.config.ConfigurationHelper.getBoolean; import static org.hibernate.internal.util.config.ConfigurationHelper.getString; import static org.hibernate.tool.schema.internal.Helper.applyScript; import static org.hibernate.tool.schema.internal.Helper.applySqlStrings; @@ -597,11 +599,24 @@ private void applyImportSources( commandExtractor, dialect, formatter, - hasDefaultImportFileScriptBeenExecuted ? "" : DEFAULT_IMPORT_FILE, + hasDefaultImportFileScriptBeenExecuted ? "" : getDefaultImportFile( options ), targets ); } + private String getDefaultImportFile(ExecutionOptions options) { + if ( skipDefaultFileImport( options ) ) { + return ""; + } + else { + return DEFAULT_IMPORT_FILE; + } + } + + private static boolean skipDefaultFileImport(ExecutionOptions options) { + return getBoolean( HBM2DDL_SKIP_DEFAULT_IMPORT_FILE, options.getConfigurationValues(), false ); + } + /** * In principle, we should format the commands in the import script if the * {@code format} parameter is {@code true}, and since it's supposed to be @@ -642,16 +657,19 @@ private boolean applyImportScript( formatter, targets ); - return containsDefaultImportFile( importScriptInput ); + return containsDefaultImportFile( importScriptInput, options ); } else { return false; } } - private boolean containsDefaultImportFile(ScriptSourceInput importScriptInput) { + private boolean containsDefaultImportFile(ScriptSourceInput importScriptInput,ExecutionOptions options ) { + if ( skipDefaultFileImport( options ) ) { + return false; + } final URL defaultImportFileUrl = getClassLoaderService().locateResource( DEFAULT_IMPORT_FILE ); - return defaultImportFileUrl != null && importScriptInput.containsScript(defaultImportFileUrl); + return defaultImportFileUrl != null && importScriptInput.containsScript( defaultImportFileUrl ); } /** diff --git a/hibernate-core/src/main/java/org/hibernate/tuple/PropertyFactory.java b/hibernate-core/src/main/java/org/hibernate/tuple/PropertyFactory.java index c8272bcb17ea..ecd7de0a0f68 100644 --- a/hibernate-core/src/main/java/org/hibernate/tuple/PropertyFactory.java +++ b/hibernate-core/src/main/java/org/hibernate/tuple/PropertyFactory.java @@ -20,8 +20,12 @@ import org.hibernate.tuple.entity.EntityBasedBasicAttribute; import org.hibernate.tuple.entity.EntityBasedCompositionAttribute; import org.hibernate.tuple.entity.VersionProperty; +import org.hibernate.type.AnyType; import org.hibernate.type.AssociationType; +import org.hibernate.type.CollectionType; +import org.hibernate.type.ComponentType; import org.hibernate.type.CompositeType; +import org.hibernate.type.EntityType; import org.hibernate.type.Type; /** @@ -218,22 +222,19 @@ public static NonIdentifierAttribute buildEntityBasedAttribute( } private static NonIdentifierAttributeNature decode(Type type) { - if ( type.isAssociationType() ) { - - if ( type.isComponentType() ) { - // an any type is both an association and a composite... - return NonIdentifierAttributeNature.ANY; - } - - return type.isCollectionType() - ? NonIdentifierAttributeNature.COLLECTION - : NonIdentifierAttributeNature.ENTITY; + if ( type instanceof CollectionType ) { + return NonIdentifierAttributeNature.COLLECTION; + } + else if ( type instanceof EntityType ) { + return NonIdentifierAttributeNature.ENTITY; + } + else if ( type instanceof AnyType ) { + return NonIdentifierAttributeNature.ANY; + } + else if ( type instanceof ComponentType ) { + return NonIdentifierAttributeNature.COMPOSITE; } else { - if ( type.isComponentType() ) { - return NonIdentifierAttributeNature.COMPOSITE; - } - return NonIdentifierAttributeNature.BASIC; } } diff --git a/hibernate-core/src/main/java/org/hibernate/tuple/entity/CompositeGeneratorBuilder.java b/hibernate-core/src/main/java/org/hibernate/tuple/entity/CompositeGeneratorBuilder.java index c5e2afddcfc2..455426f6abf6 100644 --- a/hibernate-core/src/main/java/org/hibernate/tuple/entity/CompositeGeneratorBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/tuple/entity/CompositeGeneratorBuilder.java @@ -171,7 +171,7 @@ public Object generate(SharedSessionContractImplementor session, Object owner, O final Object[] generatedValues = new Object[size]; for ( int i = 0; i < size; i++ ) { final Generator generator = generators.get(i); - if ( generator != null ) { + if ( generator != null && generator.getEventTypes().contains( eventType ) ) { generatedValues[i] = ((BeforeExecutionGenerator) generator) .generate( session, owner, null, eventType ); } @@ -182,7 +182,7 @@ public Object generate(SharedSessionContractImplementor session, Object owner, O else { for ( int i = 0; i < size; i++ ) { final Generator generator = generators.get(i); - if ( generator != null ) { + if ( generator != null && generator.getEventTypes().contains( eventType ) ) { final Object value = descriptor.getValue( currentValue, i ); final Object generatedValue = ((BeforeExecutionGenerator) generator) .generate( session, owner, value, eventType ); diff --git a/hibernate-core/src/main/java/org/hibernate/tuple/entity/EntityMetamodel.java b/hibernate-core/src/main/java/org/hibernate/tuple/entity/EntityMetamodel.java index adcdc3b5cc91..65740b2a384f 100644 --- a/hibernate-core/src/main/java/org/hibernate/tuple/entity/EntityMetamodel.java +++ b/hibernate-core/src/main/java/org/hibernate/tuple/entity/EntityMetamodel.java @@ -55,8 +55,11 @@ import org.hibernate.type.CompositeType; import org.hibernate.type.EntityType; import org.hibernate.type.ManyToOneType; +import org.hibernate.type.OneToOneType; import org.hibernate.type.Type; +import org.checkerframework.checker.nullness.qual.Nullable; + import static java.util.Collections.singleton; import static org.hibernate.internal.CoreLogging.messageLogger; import static org.hibernate.internal.util.ReflectHelper.isAbstractClass; @@ -95,6 +98,7 @@ public class EntityMetamodel implements Serializable { // temporary ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ private final String[] propertyNames; private final Type[] propertyTypes; + private final @Nullable Type[] dirtyCheckablePropertyTypes; private final boolean[] propertyLaziness; private final boolean[] propertyUpdateability; private final boolean[] nonlazyPropertyUpdateability; @@ -217,6 +221,7 @@ public EntityMetamodel( // temporary ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ propertyNames = new String[propertySpan]; propertyTypes = new Type[propertySpan]; + dirtyCheckablePropertyTypes = new Type[propertySpan]; propertyUpdateability = new boolean[propertySpan]; propertyInsertability = new boolean[propertySpan]; nonlazyPropertyUpdateability = new boolean[propertySpan]; @@ -306,6 +311,9 @@ public EntityMetamodel( propertyNames[i] = attribute.getName(); final Type propertyType = attribute.getType(); propertyTypes[i] = propertyType; + if ( attribute.isDirtyCheckable() && !( propertyType instanceof OneToOneType ) ) { + dirtyCheckablePropertyTypes[i] = propertyType; + } propertyNullability[i] = attribute.isNullable(); propertyUpdateability[i] = attribute.isUpdateable(); propertyInsertability[i] = attribute.isInsertable(); @@ -589,7 +597,7 @@ public Set getSubclassEntityNames() { } private static boolean indicatesCollection(Type type) { - if ( type.isCollectionType() ) { + if ( type instanceof CollectionType ) { return true; } else if ( type.isComponentType() ) { @@ -604,7 +612,7 @@ else if ( type.isComponentType() ) { } private static boolean indicatesOwnedCollection(Type type, MetadataImplementor metadata) { - if ( type.isCollectionType() ) { + if ( type instanceof CollectionType ) { final CollectionType collectionType = (CollectionType) type; return !metadata.getCollectionBinding( collectionType.getRole() ).isInverse(); } @@ -794,6 +802,10 @@ public Type[] getPropertyTypes() { return propertyTypes; } + public @Nullable Type[] getDirtyCheckablePropertyTypes() { + return dirtyCheckablePropertyTypes; + } + public boolean[] getPropertyLaziness() { return propertyLaziness; } diff --git a/hibernate-core/src/main/java/org/hibernate/type/AbstractType.java b/hibernate-core/src/main/java/org/hibernate/type/AbstractType.java index d67d3026ec96..2d9e196e121d 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/AbstractType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/AbstractType.java @@ -126,7 +126,8 @@ public Object replace( } private boolean needsReplacement(ForeignKeyDirection foreignKeyDirection) { - if ( isAssociationType() ) { + // Collection and OneToOne are the only associations that could be TO_PARENT + if ( this instanceof CollectionType || this instanceof OneToOneType ) { final AssociationType associationType = (AssociationType) this; return associationType.getForeignKeyDirection() == foreignKeyDirection; } diff --git a/hibernate-core/src/main/java/org/hibernate/type/AnyType.java b/hibernate-core/src/main/java/org/hibernate/type/AnyType.java index 67fc1cb09c55..f508d441daea 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/AnyType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/AnyType.java @@ -149,7 +149,7 @@ private Object extractIdentifier(Object entity, SessionFactoryImplementor factor final EntityPersister concretePersister = guessEntityPersister( entity, factory ); return concretePersister == null ? null - : concretePersister.getIdentifier( entity, null ); + : concretePersister.getIdentifier( entity, (SharedSessionContractImplementor) null ); } private EntityPersister guessEntityPersister(Object object, SessionFactoryImplementor factory) { diff --git a/hibernate-core/src/main/java/org/hibernate/type/BasicTypeRegistry.java b/hibernate-core/src/main/java/org/hibernate/type/BasicTypeRegistry.java index d994435cb1c7..3ec99729df2d 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/BasicTypeRegistry.java +++ b/hibernate-core/src/main/java/org/hibernate/type/BasicTypeRegistry.java @@ -13,6 +13,7 @@ import org.hibernate.HibernateException; import org.hibernate.Internal; +import org.hibernate.MappingException; import org.hibernate.internal.CoreLogging; import org.hibernate.internal.CoreMessageLogger; import org.hibernate.internal.util.StringHelper; @@ -68,59 +69,46 @@ private BasicType resolveTypeReference(String name) { if ( typeReference == null ) { return null; } - if ( !name.equals( typeReference.getName() ) ) { + else if ( !name.equals( typeReference.getName() ) ) { final BasicType basicType = typesByName.get( typeReference.getName() ); if ( basicType != null ) { return basicType; } } - final JavaType javaType = typeConfiguration.getJavaTypeRegistry().getDescriptor( - typeReference.getBindableJavaType() - ); - final JdbcType jdbcType = typeConfiguration.getJdbcTypeRegistry().getDescriptor( - typeReference.getSqlTypeCode() - ); - final BasicType type; + + return createBasicType( name, typeReference ); + } + + private BasicType createBasicType(String name, BasicTypeReference typeReference) { + final JavaType javaType = + typeConfiguration.getJavaTypeRegistry() + .getDescriptor( typeReference.getBindableJavaType() ); + final JdbcType jdbcType = + typeConfiguration.getJdbcTypeRegistry() + .getDescriptor( typeReference.getSqlTypeCode() ); + final BasicType createdType = createBasicType( typeReference, javaType, jdbcType ); + primeRegistryEntry( createdType ); + typesByName.put( typeReference.getName(), createdType ); + typesByName.put( name, createdType ); + return createdType; + } + + private static BasicType createBasicType( + BasicTypeReference typeReference, JavaType javaType, JdbcType jdbcType) { + final String name = typeReference.getName(); if ( typeReference.getConverter() == null ) { - if ( typeReference.isForceImmutable() ) { - type = new ImmutableNamedBasicTypeImpl<>( - javaType, - jdbcType, - typeReference.getName() - ); - } - else { - type = new NamedBasicTypeImpl<>( - javaType, - jdbcType, - typeReference.getName() - ); - } + return typeReference.isForceImmutable() + ? new ImmutableNamedBasicTypeImpl<>( javaType, jdbcType, name ) + : new NamedBasicTypeImpl<>( javaType, jdbcType, name ); } else { - //noinspection unchecked - final BasicValueConverter converter = (BasicValueConverter) typeReference.getConverter(); + final BasicValueConverter converter = typeReference.getConverter(); assert javaType == converter.getDomainJavaType(); - if ( typeReference.isForceImmutable() ) { - type = new CustomMutabilityConvertedBasicTypeImpl<>( - typeReference.getName(), - jdbcType, - converter, - ImmutableMutabilityPlan.instance() - ); - } - else { - type = new ConvertedBasicTypeImpl<>( - typeReference.getName(), - jdbcType, - converter - ); - } + return typeReference.isForceImmutable() + ? new CustomMutabilityConvertedBasicTypeImpl<>( name, jdbcType, converter, + ImmutableMutabilityPlan.instance() ) + : new ConvertedBasicTypeImpl<>( name, jdbcType, converter ); } - primeRegistryEntry( type ); - typesByName.put( typeReference.getName(), type ); - typesByName.put( name, type ); - return type; } public BasicType getRegisteredType(java.lang.reflect.Type javaType) { @@ -144,10 +132,7 @@ public BasicType resolve(java.lang.reflect.Type javaType, int sqlTypeCode } public BasicType resolve(JavaType javaType, int sqlTypeCode) { - return resolve( - javaType, - typeConfiguration.getJdbcTypeRegistry().getDescriptor( sqlTypeCode ) - ); + return resolve( javaType, typeConfiguration.getJdbcTypeRegistry().getDescriptor( sqlTypeCode ) ); } /** @@ -158,38 +143,44 @@ public BasicType resolve(JavaType javaType, JdbcType jdbcType) { return resolve( javaType, jdbcType, - () -> { - if ( javaType instanceof BasicPluralJavaType && jdbcType instanceof ArrayJdbcType ) { - //noinspection unchecked - final BasicPluralJavaType pluralJavaType = (BasicPluralJavaType) javaType; - final BasicType elementType = resolve( - pluralJavaType.getElementJavaType(), - ( (ArrayJdbcType) jdbcType ).getElementJdbcType() - ); - final BasicType resolvedType = pluralJavaType.resolveType( - typeConfiguration, - typeConfiguration.getCurrentBaseSqlTypeIndicators().getDialect(), - elementType, - null, - typeConfiguration.getCurrentBaseSqlTypeIndicators() - ); - if ( resolvedType instanceof BasicPluralType ) { - register( resolvedType ); - } - //noinspection unchecked - return (BasicType) resolvedType; - } - return new BasicTypeImpl<>( javaType, jdbcType ); - } + () -> resolvedType( javaType, jdbcType ) ); } - public BasicType resolve(JavaType javaType, JdbcType jdbcType, String baseTypeName) { - return resolve( - javaType, - jdbcType, - () -> new NamedBasicTypeImpl<>( javaType, jdbcType, baseTypeName ) + private BasicType resolvedType(JavaType javaType, JdbcType jdbcType) { + if ( javaType instanceof BasicPluralJavaType && jdbcType instanceof ArrayJdbcType ) { + //noinspection unchecked + return (BasicType) resolvedType( (ArrayJdbcType) jdbcType, (BasicPluralJavaType) javaType ); + } + else { + return new BasicTypeImpl<>( javaType, jdbcType ); + } + } + + private BasicType resolvedType(ArrayJdbcType arrayType, BasicPluralJavaType castPluralJavaType) { + final BasicType elementType = resolve( castPluralJavaType.getElementJavaType(), arrayType.getElementJdbcType() ); + final BasicType resolvedType = castPluralJavaType.resolveType( + typeConfiguration, + typeConfiguration.getCurrentBaseSqlTypeIndicators().getDialect(), + elementType, + null, + typeConfiguration.getCurrentBaseSqlTypeIndicators() ); + if ( resolvedType instanceof BasicPluralType ) { + register( resolvedType ); + } + else if ( resolvedType == null ) { + final Class elementJavaTypeClass = elementType.getJavaTypeDescriptor().getJavaTypeClass(); + if ( elementJavaTypeClass != null && elementJavaTypeClass.isArray() && elementJavaTypeClass != byte[].class ) { + // No support for nested arrays, except for byte[][] + throw new MappingException( "Nested arrays (with the exception of byte[][]) are not supported" ); + } + } + return resolvedType; + } + + public BasicType resolve(JavaType javaType, JdbcType jdbcType, String baseTypeName) { + return resolve( javaType, jdbcType, () -> new NamedBasicTypeImpl<>( javaType, jdbcType, baseTypeName ) ); } /** @@ -197,34 +188,48 @@ public BasicType resolve(JavaType javaType, JdbcType jdbcType, String * JdbcType combo or create (and register) one. */ public BasicType resolve(JavaType javaType, JdbcType jdbcType, Supplier> creator) { - final Map, BasicType> typeByJavaTypeForJdbcType = registryValues.computeIfAbsent( - jdbcType, - key -> new ConcurrentHashMap<>() - ); + final BasicType registeredBasicType = registryForJdbcType( jdbcType ).get( javaType ); + //noinspection unchecked + return registeredBasicType != null + ? (BasicType) registeredBasicType + : createIfUnregistered( javaType, jdbcType, creator ); + } - final BasicType foundBasicType = typeByJavaTypeForJdbcType.get( javaType ); - if ( foundBasicType != null ) { - //noinspection unchecked - return (BasicType) foundBasicType; - } + private BasicType createIfUnregistered( + JavaType javaType, + JdbcType jdbcType, + Supplier> creator) { // Before simply creating the type, we try to find if there is a registered type for this java type, // and if so, if the jdbc type descriptor matches. Unless it does, we at least reuse the name final BasicType registeredType = getRegisteredType( javaType.getJavaType() ); - if ( registeredType != null && registeredType.getJdbcType() == jdbcType && registeredType.getMappedJavaType() == javaType ) { + if ( registeredTypeMatches( javaType, jdbcType, registeredType ) ) { return registeredType; } - final BasicType createdBasicType = creator.get(); - typeByJavaTypeForJdbcType.put( javaType, createdBasicType ); - - // if we are still building mappings, register this ad-hoc type - // via a unique code. this is to support envers - try { - typeConfiguration.getMetadataBuildingContext().getBootstrapContext() - .registerAdHocBasicType( createdBasicType ); + else { + final BasicType createdType = creator.get(); + register( javaType, jdbcType, createdType ); + return createdType; } - catch (Exception ignore) { + } + + private static boolean registeredTypeMatches(JavaType javaType, JdbcType jdbcType, BasicType registeredType) { + return registeredType != null + && registeredType.getJdbcType() == jdbcType + && registeredType.getMappedJavaType() == javaType; + } + + private void register(JavaType javaType, JdbcType jdbcType, BasicType createdType) { + if ( createdType != null ) { + registryForJdbcType( jdbcType ).put( javaType, createdType ); + // if we are still building mappings, register this adhoc + // type via a unique code. (This is to support Envers.) + try { + typeConfiguration.getMetadataBuildingContext().getBootstrapContext() + .registerAdHocBasicType( createdType ); + } + catch (Exception ignore) { + } } - return createdBasicType; } @@ -260,16 +265,12 @@ public void register(BasicType type, String... keys) { } private void applyOrOverwriteEntry(BasicType type) { - final Map, BasicType> typeByJavaTypeForJdbcType = registryValues.computeIfAbsent( - type.getJdbcType(), - jdbcType -> new ConcurrentHashMap<>() - ); - - final BasicType existing = typeByJavaTypeForJdbcType.put( type.getMappedJavaType(), type ); + final JdbcType jdbcType = type.getJdbcType(); + final BasicType existing = registryForJdbcType( jdbcType ).put( type.getMappedJavaType(), type ); if ( existing != null ) { LOG.debugf( "BasicTypeRegistry registration overwritten (%s + %s); previous =`%s`", - type.getJdbcType().getFriendlyName(), + jdbcType.getFriendlyName(), type.getJavaTypeDescriptor(), existing ); @@ -360,26 +361,25 @@ public void addPrimeEntry(BasicTypeReference type, String legacyTypeClassName } private void primeRegistryEntry(BasicType type) { - final Map, BasicType> typeByJavaTypeForJdbcType = registryValues.computeIfAbsent( - type.getJdbcType(), - jdbcType -> new ConcurrentHashMap<>() - ); - - final BasicType existing = typeByJavaTypeForJdbcType.get( type.getMappedJavaType() ); - + final JdbcType jdbcType = type.getJdbcType(); + final BasicType existing = registryForJdbcType( jdbcType ).get( type.getMappedJavaType() ); if ( existing != null ) { LOG.debugf( "Skipping registration of BasicType (%s + %s); still priming. existing = %s", - type.getJdbcType().getFriendlyName(), + jdbcType.getFriendlyName(), type.getJavaTypeDescriptor(), existing ); } else { - typeByJavaTypeForJdbcType.put( type.getMappedJavaType(), type ); + registryForJdbcType( jdbcType ).put( type.getMappedJavaType(), type ); } } + private Map, BasicType> registryForJdbcType(JdbcType jdbcType) { + return registryValues.computeIfAbsent( jdbcType, key -> new ConcurrentHashMap<>() ); + } + private void applyRegistrationKeys(BasicType type, String[] keys) { for ( String key : keys ) { // be safe... diff --git a/hibernate-core/src/main/java/org/hibernate/type/CollectionType.java b/hibernate-core/src/main/java/org/hibernate/type/CollectionType.java index 2e6e1253958f..ef2c37f24262 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/CollectionType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/CollectionType.java @@ -25,6 +25,7 @@ import org.hibernate.Internal; import org.hibernate.MappingException; import org.hibernate.collection.spi.AbstractPersistentCollection; +import org.hibernate.collection.spi.PersistentArrayHolder; import org.hibernate.collection.spi.PersistentCollection; import org.hibernate.engine.spi.CollectionEntry; import org.hibernate.engine.spi.CollectionKey; @@ -469,7 +470,7 @@ public String getAssociatedEntityName(SessionFactoryImplementor factory) QueryableCollection collectionPersister = (QueryableCollection) factory.getRuntimeMetamodels().getMappingMetamodel().getCollectionDescriptor( role ); - if ( !collectionPersister.getElementType().isEntityType() ) { + if ( !( collectionPersister.getElementType() instanceof EntityType ) ) { throw new MappingException( "collection was not an association: " + collectionPersister.getRole() @@ -638,6 +639,39 @@ public Object replace( final Object owner, final Map copyCache) throws HibernateException { if ( original == null ) { + if ( target == null ) { + return null; + } + if ( target instanceof Collection ) { + ( (Collection) target ).clear(); + return target; + } + else if ( target instanceof Map ) { + ( (Map) target ).clear(); + return target; + } + else { + final PersistenceContext persistenceContext = session.getPersistenceContext(); + final PersistentCollection collectionHolder = persistenceContext + .getCollectionHolder( target ); + if ( collectionHolder != null ) { + if ( collectionHolder instanceof PersistentArrayHolder ) { + PersistentArrayHolder persistentArrayHolder = (PersistentArrayHolder) collectionHolder; + persistenceContext.removeCollectionHolder( target ); + persistentArrayHolder.beginRead(); + persistentArrayHolder.injectLoadedState( + persistenceContext.getCollectionEntry( collectionHolder ) + .getLoadedPersister() + .getAttributeMapping(), null + ); + persistentArrayHolder.endRead(); + persistentArrayHolder.dirty(); + persistenceContext.addCollectionHolder( collectionHolder ); + return persistentArrayHolder.getArray(); + } + } + } + return null; } if ( !Hibernate.isInitialized( original ) ) { diff --git a/hibernate-core/src/main/java/org/hibernate/type/ComponentType.java b/hibernate-core/src/main/java/org/hibernate/type/ComponentType.java index b9703191657f..cfccfbd4767d 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/ComponentType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/ComponentType.java @@ -440,7 +440,7 @@ public Object[] getPropertyValues(Object component, SharedSessionContractImpleme @Override public Object[] getPropertyValues(Object component) { if (component == null) { - return new Object[propertySpan]; + return new Object[propertySpan + discriminatorColumnSpan]; } else if ( component instanceof Object[] ) { // A few calls to hashCode pass the property values already in an diff --git a/hibernate-core/src/main/java/org/hibernate/type/EntityType.java b/hibernate-core/src/main/java/org/hibernate/type/EntityType.java index 3cbaf9b8e8a7..ba67db9a7c1b 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/EntityType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/EntityType.java @@ -277,14 +277,14 @@ else if ( y == null ) { final Object xId = extractIdentifier( x, factory ); final Object yId = extractIdentifier( y, factory ); - return getIdentifierType( factory ).compare( xId, yId ); + return getIdentifierType( factory ).compare( xId, yId, factory ); } private Object extractIdentifier(Object entity, SessionFactoryImplementor factory) { final EntityPersister concretePersister = getAssociatedEntityPersister( factory ); return concretePersister == null ? null - : concretePersister.getIdentifier( entity, null ); + : concretePersister.getIdentifier( entity, (SharedSessionContractImplementor) null ); } @Override @@ -352,8 +352,8 @@ public int getHashCode(Object x, SessionFactoryImplementor factory) { } else { final Class mappedClass = persister.getMappedClass(); - if ( mappedClass.isAssignableFrom( x.getClass() ) ) { - id = persister.getIdentifier( x, null ); + if ( mappedClass.isInstance( x ) ) { + id = persister.getIdentifier( x, (SharedSessionContractImplementor) null ); } else { id = x; @@ -363,8 +363,15 @@ public int getHashCode(Object x, SessionFactoryImplementor factory) { } else { assert uniqueKeyPropertyName != null; + final Object uniqueKey; final Type keyType = persister.getPropertyType( uniqueKeyPropertyName ); - return keyType.getHashCode( x, factory ); + if ( keyType.getReturnedClass().isInstance( x ) ) { + uniqueKey = x; + } + else { + uniqueKey = persister.getPropertyValue( x, uniqueKeyPropertyName ); + } + return keyType.getHashCode( uniqueKey, factory ); } } @@ -379,39 +386,62 @@ public boolean isEqual(Object x, Object y, SessionFactoryImplementor factory) { } final EntityPersister persister = getAssociatedEntityPersister( factory ); - final Class mappedClass = persister.getMappedClass(); - Object xid; - final LazyInitializer lazyInitializerX = extractLazyInitializer( x ); - if ( lazyInitializerX != null ) { - xid = lazyInitializerX.getInternalIdentifier(); - } - else { - if ( mappedClass.isAssignableFrom( x.getClass() ) ) { - xid = persister.getIdentifier( x, null ); + if ( isReferenceToPrimaryKey() ) { + final Class mappedClass = persister.getMappedClass(); + Object xid; + final LazyInitializer lazyInitializerX = extractLazyInitializer( x ); + if ( lazyInitializerX != null ) { + xid = lazyInitializerX.getInternalIdentifier(); } else { - //JPA 2 case where @IdClass contains the id and not the associated entity - xid = x; + if ( mappedClass.isInstance( x ) ) { + xid = persister.getIdentifier( x, (SharedSessionContractImplementor) null ); + } + else { + //JPA 2 case where @IdClass contains the id and not the associated entity + xid = x; + } } - } - Object yid; - final LazyInitializer lazyInitializerY = extractLazyInitializer( y ); - if ( lazyInitializerY != null ) { - yid = lazyInitializerY.getInternalIdentifier(); + Object yid; + final LazyInitializer lazyInitializerY = extractLazyInitializer( y ); + if ( lazyInitializerY != null ) { + yid = lazyInitializerY.getInternalIdentifier(); + } + else { + if ( mappedClass.isInstance( y ) ) { + yid = persister.getIdentifier( y, (SharedSessionContractImplementor) null ); + } + else { + //JPA 2 case where @IdClass contains the id and not the associated entity + yid = y; + } + } + + // Check for reference equality first as the type-specific checks by IdentifierType are sometimes non-trivial + return ( xid == yid ) || persister.getIdentifierType().isEqual( xid, yid, factory ); } else { - if ( mappedClass.isAssignableFrom( y.getClass() ) ) { - yid = persister.getIdentifier( y, null ); + assert uniqueKeyPropertyName != null; + final Object xUniqueKey; + final Type keyType = persister.getPropertyType( uniqueKeyPropertyName ); + if ( keyType.getReturnedClass().isInstance( x ) ) { + xUniqueKey = x; } else { - //JPA 2 case where @IdClass contains the id and not the associated entity - yid = y; + xUniqueKey = persister.getPropertyValue( x, uniqueKeyPropertyName ); } - } - // Check for reference equality first as the type-specific checks by IdentifierType are sometimes non-trivial - return ( xid == yid ) || persister.getIdentifierType().isEqual( xid, yid, factory ); + final Object yUniqueKey; + if ( keyType.getReturnedClass().isInstance( y ) ) { + yUniqueKey = y; + } + else { + yUniqueKey = persister.getPropertyValue( y, uniqueKeyPropertyName ); + } + return (xUniqueKey == yUniqueKey) + || keyType.isEqual( xUniqueKey, yUniqueKey, factory ); + } } /** @@ -497,7 +527,7 @@ else if ( isPersistentAttributeInterceptable( value ) ) { // we need to dig a little deeper, as that property might also be // an entity type, in which case we need to resolve its identifier final Type type = entityPersister.getPropertyType( uniqueKeyPropertyName ); - if ( type.isEntityType() ) { + if ( type instanceof EntityType ) { return ( (EntityType) type ).getIdentifier( propertyValue, session ); } else { @@ -522,7 +552,7 @@ else if ( value == null ) { // we need to dig a little deeper, as that property might also be // an entity type, in which case we need to resolve its identifier final Type type = entityPersister.getPropertyType( uniqueKeyPropertyName ); - if ( type.isEntityType() ) { + if ( type instanceof EntityType ) { return ( (EntityType) type ).getIdentifier( propertyValue, sessionFactory ); } else { @@ -564,7 +594,7 @@ public String toLoggableString(Object value, SessionFactoryImplementor factory) id = lazyInitializer.getInternalIdentifier(); } else { - id = persister.getIdentifier( value, null ); + id = persister.getIdentifier( value, (SharedSessionContractImplementor) null ); } result.append( '#' ) @@ -647,7 +677,7 @@ public final Type getIdentifierOrUniqueKeyType(Mapping factory) throws MappingEx } else { final Type type = factory.getReferencedPropertyType( getAssociatedEntityName(), uniqueKeyPropertyName ); - if ( type.isEntityType() ) { + if ( type instanceof EntityType ) { return ( (EntityType) type ).getIdentifierOrUniqueKeyType( factory ); } else { @@ -701,8 +731,13 @@ protected final Object resolveIdentifier(Object id, SharedSessionContractImpleme getAssociatedEntityPersister( session.getFactory() ) .isInstrumented(); - final Object proxyOrEntity = - session.internalLoad( getAssociatedEntityName(), id, isEager( overridingEager ), isNullable() ); + final boolean isEager = isEager( overridingEager ); + // If the association is lazy, retrieve the concrete type if required + final String entityName = isEager ? getAssociatedEntityName() + : getAssociatedEntityPersister( session.getFactory() ).resolveConcreteProxyTypeForId( id, session ) + .getEntityName(); + + final Object proxyOrEntity = session.internalLoad( entityName, id, isEager, isNullable() ); final LazyInitializer lazyInitializer = extractLazyInitializer( proxyOrEntity ); if ( lazyInitializer != null ) { diff --git a/hibernate-core/src/main/java/org/hibernate/type/ManyToOneType.java b/hibernate-core/src/main/java/org/hibernate/type/ManyToOneType.java index 3c27da133a2f..eefe40170f89 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/ManyToOneType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/ManyToOneType.java @@ -249,7 +249,7 @@ public boolean isDirty( } Object oldid = getIdentifier( old, session ); Object newid = getIdentifier( current, session ); - return getIdentifierType( session ).isDirty( oldid, newid, session ); + return getIdentifierOrUniqueKeyType( session.getFactory() ).isDirty( oldid, newid, session ); } @Override @@ -267,7 +267,7 @@ public boolean isDirty( } Object oldid = getIdentifier( old, session ); Object newid = getIdentifier( current, session ); - return getIdentifierType( session ).isDirty( oldid, newid, checkable, session ); + return getIdentifierOrUniqueKeyType( session.getFactory() ).isDirty( oldid, newid, checkable, session ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/type/TypeHelper.java b/hibernate-core/src/main/java/org/hibernate/type/TypeHelper.java index 9223fff3a31a..867ba6881aee 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/TypeHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/type/TypeHelper.java @@ -193,12 +193,12 @@ public static Object[] replaceAssociations( final Type type = types[i]; // AnyType is both a CompositeType and an AssociationType // but here we want to treat it as an association - if ( type.isAssociationType() ) { + if ( type instanceof EntityType || type instanceof CollectionType || type instanceof AnyType ) { copied[i] = types[i].replace( currentOriginal, target[i], session, owner, copyCache, foreignKeyDirection ); } else { - if ( type.isComponentType() ) { - final CompositeType compositeType = (CompositeType) type; + if ( type instanceof ComponentType ) { + final ComponentType compositeType = (ComponentType) type; if ( target[i] != null ) { // need to extract the component values and check for subtype replacements... final Object[] objects = replaceCompositeAssociations( @@ -224,7 +224,7 @@ private static Object[] replaceCompositeAssociations( Map copyCache, ForeignKeyDirection foreignKeyDirection, Object target, Object currentOriginal, - CompositeType compositeType) { + ComponentType compositeType) { final Type[] subtypes = compositeType.getSubtypes(); return replaceAssociations( currentOriginal == null diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/DateTimeUtils.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/DateTimeUtils.java index 42487e0c4af5..df7934e6141a 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/DateTimeUtils.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/DateTimeUtils.java @@ -477,7 +477,10 @@ public static T truncateToPrecision(T temporal, int precisi /** * Do the same conversion that databases do when they encounter a timestamp with a higher precision * than what is supported by a column, which is to round the excess fractions. + * + * @deprecated Use {@link #adjustToDefaultPrecision(Temporal, Dialect)} instead */ + @Deprecated(forRemoval = true, since = "6.6.1") public static T roundToDefaultPrecision(T temporal, Dialect d) { final int defaultTimestampPrecision = d.getDefaultTimestampPrecision(); if ( defaultTimestampPrecision >= 9 || !temporal.isSupported( ChronoField.NANO_OF_SECOND ) ) { @@ -491,6 +494,9 @@ public static T roundToDefaultPrecision(T temporal, Dialect } public static T roundToSecondPrecision(T temporal, int precision) { + if ( precision >= 9 || !temporal.isSupported( ChronoField.NANO_OF_SECOND ) ) { + return temporal; + } if ( precision == 0 ) { //noinspection unchecked return temporal.get( ChronoField.NANO_OF_SECOND ) >= 500_000_000L diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/AbstractArrayJavaType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/AbstractArrayJavaType.java index c0de85c7e4d5..5f4ecba47f65 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/AbstractArrayJavaType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/AbstractArrayJavaType.java @@ -20,7 +20,6 @@ import org.hibernate.type.descriptor.java.spi.UnknownBasicJavaType; import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators; -import org.hibernate.type.internal.BasicTypeImpl; import org.hibernate.type.spi.TypeConfiguration; public abstract class AbstractArrayJavaType extends AbstractClassJavaType @@ -49,7 +48,8 @@ public JdbcType getRecommendedJdbcType(JdbcTypeIndicators indicators) { // Always determine the recommended type to make sure this is a valid basic java type return indicators.getTypeConfiguration().getJdbcTypeRegistry().resolveTypeConstructorDescriptor( indicators.getPreferredSqlTypeCodeForArray(), - new BasicTypeImpl<>( componentJavaType, componentJavaType.getRecommendedJdbcType( indicators ) ), + indicators.getTypeConfiguration().getBasicTypeRegistry().resolve( + componentJavaType, componentJavaType.getRecommendedJdbcType( indicators ) ), ColumnTypeInformation.EMPTY ); } diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/BasicPluralJavaType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/BasicPluralJavaType.java index e0401ad4bd70..eb4f00529a90 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/BasicPluralJavaType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/BasicPluralJavaType.java @@ -19,7 +19,7 @@ * Descriptor for a basic plural Java type. * A basic plural type represents a type, that is mapped to a single column instead of multiple rows. * This is used for array or collection types, that are backed by e.g. SQL array or JSON/XML DDL types. - * + *

    * The interface can be implemented by a plural java type e.g. {@link org.hibernate.type.descriptor.java.spi.BasicCollectionJavaType} * and provides access to the element java type, as well as a hook to resolve the {@link BasicType} based on the element {@link BasicType}, * in order to gain enough information to implement storage and retrieval of the composite data type via JDBC. diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/BlobJavaType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/BlobJavaType.java index 57ee42fd64ac..1f5515a9dcc6 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/BlobJavaType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/BlobJavaType.java @@ -131,11 +131,8 @@ else if ( byte[].class.isAssignableFrom( type )) { return (X) DataHelper.extractBytes( value.getBinaryStream() ); } } - else if (Blob.class.isAssignableFrom( type )) { - final Blob blob = value instanceof WrappedBlob - ? ( (WrappedBlob) value ).getWrappedBlob() - : getOrCreateBlob(value, options); - return (X) blob; + else if ( Blob.class.isAssignableFrom( type ) ) { + return (X) getOrCreateBlob( value, options ); } } catch ( SQLException e ) { @@ -146,13 +143,16 @@ else if (Blob.class.isAssignableFrom( type )) { } private Blob getOrCreateBlob(Blob value, WrapperOptions options) throws SQLException { - if(options.getDialect().useConnectionToCreateLob()) { - if(value.length() == 0) { + if ( value instanceof WrappedBlob ) { + value = ( (WrappedBlob) value ).getWrappedBlob(); + } + if ( options.getDialect().useConnectionToCreateLob() ) { + if ( value.length() == 0 ) { // empty Blob - return options.getLobCreator().createBlob(new byte[0]); + return options.getLobCreator().createBlob( new byte[0] ); } else { - return options.getLobCreator().createBlob(value.getBytes(1, (int) value.length())); + return options.getLobCreator().createBlob( value.getBytes( 1, (int) value.length() ) ); } } else { diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/ClobJavaType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/ClobJavaType.java index b1248a76dacf..c3e5490444a8 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/ClobJavaType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/ClobJavaType.java @@ -104,11 +104,8 @@ else if ( String.class.isAssignableFrom( type ) ) { return (X) LobStreamDataHelper.extractString( value.getCharacterStream() ); } } - else if (Clob.class.isAssignableFrom( type )) { - final Clob clob = value instanceof WrappedClob - ? ( (WrappedClob) value ).getWrappedClob() - : getOrCreateClob(value, options); - return (X) clob; + else if ( Clob.class.isAssignableFrom( type ) ) { + return (X) getOrCreateClob( value, options ); } else if ( String.class.isAssignableFrom( type ) ) { if (value instanceof ClobImplementer) { @@ -129,13 +126,16 @@ else if ( String.class.isAssignableFrom( type ) ) { } private Clob getOrCreateClob(Clob value, WrapperOptions options) throws SQLException { - if(options.getDialect().useConnectionToCreateLob()) { - if(value.length() == 0) { + if ( value instanceof WrappedClob ) { + value = ( (WrappedClob) value ).getWrappedClob(); + } + if ( options.getDialect().useConnectionToCreateLob() ) { + if ( value.length() == 0 ) { // empty Clob - return options.getLobCreator().createClob(""); + return options.getLobCreator().createClob( "" ); } else { - return options.getLobCreator().createClob(value.getSubString(1, (int) value.length())); + return options.getLobCreator().createClob( value.getSubString( 1, (int) value.length() ) ); } } else { diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/JdbcTimestampJavaType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/JdbcTimestampJavaType.java index fbee5d86bd82..d59f30c164b4 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/JdbcTimestampJavaType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/JdbcTimestampJavaType.java @@ -27,7 +27,6 @@ import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.internal.util.CharSequenceHelper; import org.hibernate.sql.ast.spi.SqlAppender; -import org.hibernate.type.descriptor.DateTimeUtils; import org.hibernate.type.descriptor.WrapperOptions; import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators; @@ -231,13 +230,7 @@ public void appendEncodedString(SqlAppender sb, Date value) { @Override public Date fromEncodedString(CharSequence charSequence, int start, int end) { try { - final TemporalAccessor accessor = DateTimeUtils.DATE_TIME.parse( - CharSequenceHelper.subSequence( - charSequence, - start, - end - ) - ); + final TemporalAccessor accessor = ENCODED_FORMATTER.parse( CharSequenceHelper.subSequence( charSequence, start, end ) ); final Timestamp timestamp; if ( accessor.isSupported( ChronoField.INSTANT_SECONDS ) ) { timestamp = new Timestamp( accessor.getLong( ChronoField.INSTANT_SECONDS ) * 1000L ); diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/NClobJavaType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/NClobJavaType.java index 48d4c9c2b62e..4533df2e1530 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/NClobJavaType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/NClobJavaType.java @@ -103,11 +103,8 @@ public X unwrap(final NClob value, Class type, WrapperOptions options) { return (X) new CharacterStreamImpl( DataHelper.extractString( value.getCharacterStream() ) ); } } - else if (NClob.class.isAssignableFrom( type )) { - final NClob nclob = value instanceof WrappedNClob - ? ( (WrappedNClob) value ).getWrappedNClob() - : getOrCreateNClob(value, options); - return (X) nclob; + else if ( NClob.class.isAssignableFrom( type ) ) { + return (X) getOrCreateNClob( value, options ); } } catch ( SQLException e ) { @@ -118,13 +115,16 @@ else if (NClob.class.isAssignableFrom( type )) { } private NClob getOrCreateNClob(NClob value, WrapperOptions options) throws SQLException { - if(options.getDialect().useConnectionToCreateLob()) { - if(value.length() == 0) { + if ( value instanceof WrappedNClob ) { + value = ( (WrappedNClob) value ).getWrappedNClob(); + } + if ( options.getDialect().useConnectionToCreateLob() ) { + if ( value.length() == 0 ) { // empty NClob - return options.getLobCreator().createNClob(""); + return options.getLobCreator().createNClob( "" ); } else { - return options.getLobCreator().createNClob(value.getSubString(1, (int) value.length())); + return options.getLobCreator().createNClob( value.getSubString( 1, (int) value.length() ) ); } } else { @@ -132,7 +132,6 @@ private NClob getOrCreateNClob(NClob value, WrapperOptions options) throws SQLEx } } - public NClob wrap(X value, WrapperOptions options) { if ( value == null ) { return null; diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/VersionJavaType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/VersionJavaType.java index 1ab8dbcf47af..0c6ec96ceb51 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/VersionJavaType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/VersionJavaType.java @@ -16,6 +16,10 @@ public interface VersionJavaType extends JavaType { /** * Generate an initial version. + *

    + * Note that this operation is only used when the program sets a null or negative + * number as the value of the entity version field. It is not called when the + * program sets the version field to a sensible-looking version. * * @param length The length of the type * @param precision The precision of the type diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/spi/BasicCollectionJavaType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/spi/BasicCollectionJavaType.java index 70a7d653d021..31b679dfd8b8 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/spi/BasicCollectionJavaType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/spi/BasicCollectionJavaType.java @@ -40,7 +40,6 @@ import org.hibernate.type.descriptor.java.MutabilityPlan; import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators; -import org.hibernate.type.internal.BasicTypeImpl; import org.hibernate.type.spi.TypeConfiguration; /** @@ -78,7 +77,8 @@ public JdbcType getRecommendedJdbcType(JdbcTypeIndicators indicators) { // (even though we only use this inside the if block, we want it to throw here if something wrong) return indicators.getTypeConfiguration().getJdbcTypeRegistry().resolveTypeConstructorDescriptor( indicators.getPreferredSqlTypeCodeForArray(), - new BasicTypeImpl<>( componentJavaType, componentJavaType.getRecommendedJdbcType( indicators ) ), + indicators.getTypeConfiguration().getBasicTypeRegistry().resolve( + componentJavaType, componentJavaType.getRecommendedJdbcType( indicators ) ), ColumnTypeInformation.EMPTY ); } diff --git a/hibernate-core/src/test/java/org/hibernate/dialect/DB2iDialectTest.java b/hibernate-core/src/test/java/org/hibernate/dialect/DB2iDialectTest.java new file mode 100644 index 000000000000..4167d5d9dd76 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/dialect/DB2iDialectTest.java @@ -0,0 +1,69 @@ +package org.hibernate.dialect; + +import org.hibernate.testing.orm.junit.JiraKey; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class DB2iDialectTest { + + private static final String EXPECTED_FOR_UPDATE = " for update with rs"; + private static final String EXPECTED_FOR_UPDATE_SKIP_LOCK = " for update with rs skip locked data"; + + private DB2iDialect dialect; + + @BeforeEach + void setUp() { + dialect = new DB2iDialect(); + } + + @Test + @JiraKey("HHH-18560") + void getForUpdateString() { + String actual = dialect.getForUpdateString(); + assertEquals(EXPECTED_FOR_UPDATE, actual); + } + + @Test + @JiraKey("HHH-18560") + void getForUpdateSkipLockedString() { + String actual = dialect.getForUpdateSkipLockedString(); + assertEquals(EXPECTED_FOR_UPDATE_SKIP_LOCK, actual); + } + + @Test + @JiraKey("HHH-18560") + void testGetForUpdateSkipLockedString() { + String actual = dialect.getForUpdateSkipLockedString("alias"); + assertEquals(EXPECTED_FOR_UPDATE_SKIP_LOCK, actual); + } + + @Test + @JiraKey("HHH-18560") + void getWriteLockString_skiplocked() { + String actual = dialect.getWriteLockString(-2); + assertEquals(EXPECTED_FOR_UPDATE_SKIP_LOCK, actual); + } + + @Test + @JiraKey("HHH-18560") + void getWriteLockString() { + String actual = dialect.getWriteLockString(0); + assertEquals(EXPECTED_FOR_UPDATE, actual); + } + + @Test + @JiraKey("HHH-18560") + void getReadLockString() { + String actual = dialect.getReadLockString(0); + assertEquals(EXPECTED_FOR_UPDATE, actual); + } + + @Test + @JiraKey("HHH-18560") + void getReadLockString_skipLocked() { + String actual = dialect.getReadLockString(-2); + assertEquals(EXPECTED_FOR_UPDATE_SKIP_LOCK, actual); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/dialect/MySQLDialectDatabaseVersionTest.java b/hibernate-core/src/test/java/org/hibernate/dialect/MySQLDialectDatabaseVersionTest.java new file mode 100644 index 000000000000..466180eac632 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/dialect/MySQLDialectDatabaseVersionTest.java @@ -0,0 +1,97 @@ +package org.hibernate.dialect; + +import java.util.Map; +import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo; +import org.hibernate.testing.RequiresDialect; +import org.hibernate.testing.TestForIssue; +import org.hibernate.testing.junit4.BaseUnitTestCase; +import org.junit.Test; + + +import static org.junit.Assert.assertEquals; + +@RequiresDialect(MySQLDialect.class) +@TestForIssue(jiraKey = "HHH-18518") +public class MySQLDialectDatabaseVersionTest extends BaseUnitTestCase { + + @Test + public void versionWithSuffix() { + String version = "8.0.37-azure"; + Dialect dialect = new MySQLDialect( new TestingMySQLDialectResolutionInfo( version ) ); + + assertEquals(8, dialect.getVersion().getMajor()); + assertEquals(0, dialect.getVersion().getMinor()); + assertEquals(37, dialect.getVersion().getMicro()); + } + + @Test + public void releaseVersion() { + String version = "8.0.37"; + Dialect dialect = new MySQLDialect( new TestingMySQLDialectResolutionInfo( version ) ); + + assertEquals(8, dialect.getVersion().getMajor()); + assertEquals(0, dialect.getVersion().getMinor()); + assertEquals(37, dialect.getVersion().getMicro()); + } + + static final class TestingMySQLDialectResolutionInfo implements DialectResolutionInfo { + private final String databaseVersion; + + TestingMySQLDialectResolutionInfo(String databaseVersion) { + this.databaseVersion = databaseVersion; + } + + + @Override + public String getDatabaseName() { + return "MySQL"; + } + + @Override + public String getDatabaseVersion() { + return this.databaseVersion; + } + + @Override + public int getDatabaseMajorVersion() { + return 8; + } + + @Override + public int getDatabaseMinorVersion() { + return 0; + } + + @Override + public String getDriverName() { + return "MySQL JDBC Driver"; + } + + @Override + public int getDriverMajorVersion() { + return 8; + } + + @Override + public int getDriverMinorVersion() { + return 3; + } + + @Override + public String getSQLKeywords() { + return ""; + } + + @Override + public String toString() { + return "8.3.0"; + } + + @Override + public Map getConfigurationValues() { + return Map.of(); + } + + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/engine/internal/StatisticalLoggingSessionEventListenerTest.java b/hibernate-core/src/test/java/org/hibernate/engine/internal/StatisticalLoggingSessionEventListenerTest.java new file mode 100644 index 000000000000..e8c9065c691c --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/engine/internal/StatisticalLoggingSessionEventListenerTest.java @@ -0,0 +1,213 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.engine.internal; + +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.internal.CoreMessageLogger; + +import org.hibernate.testing.logger.LoggerInspectionRule; +import org.hibernate.testing.logger.Triggerable; +import org.hibernate.testing.orm.domain.gambit.BasicEntity; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.Rule; +import org.junit.jupiter.api.Test; + +import org.jboss.logging.Logger; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * @author Christian Bürgi + */ +@DomainModel(annotatedClasses = { + BasicEntity.class +}) +@SessionFactory(generateStatistics = true) +@ServiceRegistry(settings = { + @Setting(name = AvailableSettings.STATEMENT_BATCH_SIZE, value = "10"), + @Setting(name = AvailableSettings.ORDER_INSERTS, value = "true"), + @Setting(name = AvailableSettings.USE_QUERY_CACHE, value = "true"), + @Setting(name = AvailableSettings.USE_SECOND_LEVEL_CACHE, value = "true") +}) +@Jira("https://hibernate.atlassian.net/browse/HHH-18513") +public class StatisticalLoggingSessionEventListenerTest { + + @Rule + public LoggerInspectionRule logInspection = new LoggerInspectionRule( + Logger.getMessageLogger( + CoreMessageLogger.class, + StatisticalLoggingSessionEventListener.class.getName() + ) ); + + @Test + void testSessionMetricsLog(SessionFactoryScope scope) { + Triggerable triggerable = logInspection.watchForLogMessages( "Session Metrics {" ); + long startTime = System.nanoTime(); + + scope.inTransaction( session -> { + session.persist( new BasicEntity( 1, "fooData" ) ); + session.persist( new BasicEntity( 2, "fooData2" ) ); + session.flush(); + session.clear(); + for ( int i = 0; i < 2; i++ ) { + assertThat( session.createQuery( "select data from BasicEntity e where e.id=1", String.class ) + .setCacheable( true ) + .getSingleResult() ).isEqualTo( "fooData" ); + } + } ); + long sessionNanoDuration = System.nanoTime() - startTime; + + List messages = triggerable.triggerMessages(); + assertThat( messages ).hasSize( 1 ); + String sessionMetricsLog = messages.get( 0 ); + + // acquiring JDBC connections + SessionMetric acquiringJdbcConnectionsMetric = extractMetric( + sessionMetricsLog, + "([0-9]+) nanoseconds spent acquiring ([0-9]+) JDBC connections" + ); + assertThat( acquiringJdbcConnectionsMetric.getDuration() ).isGreaterThan( 0L ) + .isLessThan( sessionNanoDuration ); + + // releasing JDBC connections + SessionMetric releasingJdbcConnectionsMetric = extractMetric( + sessionMetricsLog, + "([0-9]+) nanoseconds spent releasing ([0-9]+) JDBC connections" + ); + assertThat( releasingJdbcConnectionsMetric.getDuration() ).isGreaterThan( 0L ) + .isLessThan( sessionNanoDuration ); + + // preparing JDBC statements + SessionMetric preparingJdbcStatmentsMetric = extractMetric( + sessionMetricsLog, + "([0-9]+) nanoseconds spent preparing ([0-9]+) JDBC statements" + ); + assertThat( preparingJdbcStatmentsMetric.getDuration() ).isGreaterThan( 0L ) + .isLessThan( sessionNanoDuration ); + + // executing JDBC statements + SessionMetric executingJdbcStatmentsMetric = extractMetric( + sessionMetricsLog, + "([0-9]+) nanoseconds spent executing ([0-9]+) JDBC statements" + ); + assertThat( executingJdbcStatmentsMetric.getDuration() ).isGreaterThan( 0L ) + .isLessThan( sessionNanoDuration ); + + // executing JDBC batches + SessionMetric executingJdbcBatchesMetric = extractMetric( + sessionMetricsLog, + "([0-9]+) nanoseconds spent executing ([0-9]+) JDBC batches" + ); + assertThat( executingJdbcBatchesMetric.getDuration() ).isGreaterThan( 0L ) + .isLessThan( sessionNanoDuration ); + + // performing L2C puts + SessionMetric performingL2CPutsMetric = extractMetric( + sessionMetricsLog, + "([0-9]+) nanoseconds spent performing ([0-9]+) L2C puts" + ); + assertThat( performingL2CPutsMetric.getDuration() ).isGreaterThan( 0L ) + .isLessThan( sessionNanoDuration ); + + // performing L2C hits + SessionMetric performingL2CHitsMetric = extractMetric( + sessionMetricsLog, + "([0-9]+) nanoseconds spent performing ([0-9]+) L2C hits" + ); + assertThat( performingL2CHitsMetric.getDuration() ).isGreaterThan( 0L ) + .isLessThan( sessionNanoDuration ); + + // performing L2C misses + SessionMetric performingL2CMissesMetric = extractMetric( + sessionMetricsLog, + "([0-9]+) nanoseconds spent performing ([0-9]+) L2C misses" + ); + assertThat( performingL2CMissesMetric.getDuration() ).isGreaterThan( 0L ) + .isLessThan( sessionNanoDuration ); + + // executing flushes + SessionMetric executingFlushesMetric = extractMetric( + sessionMetricsLog, + "([0-9]+) nanoseconds spent executing ([0-9]+) flushes" + ); + assertThat( executingFlushesMetric.getDuration() ).isGreaterThan( 0L ) + .isLessThan( sessionNanoDuration ); + + // executing pre-partial-flushes + SessionMetric executingPrePartialFlushesMetric = extractMetric( + sessionMetricsLog, + "([0-9]+) nanoseconds spent executing ([0-9]+) pre-partial-flushes" + ); + assertThat( executingPrePartialFlushesMetric.getDuration() ).isGreaterThan( 0L ) + .isLessThan( sessionNanoDuration ); + + // executing partial-flushes + SessionMetric executingPartialFlushesMetric = extractMetric( + sessionMetricsLog, + "([0-9]+) nanoseconds spent executing ([0-9]+) partial-flushes" + ); + assertThat( executingPartialFlushesMetric.getDuration() ).isGreaterThan( 0L ) + .isLessThan( sessionNanoDuration ); + + // Number of metrics + List metricList = List.of( + acquiringJdbcConnectionsMetric, + releasingJdbcConnectionsMetric, + preparingJdbcStatmentsMetric, + executingJdbcStatmentsMetric, + executingJdbcBatchesMetric, + performingL2CPutsMetric, + performingL2CHitsMetric, + performingL2CMissesMetric, + executingFlushesMetric, + executingPrePartialFlushesMetric, + executingPartialFlushesMetric + ); + int numberOfMetrics = metricList.size(); + // Number of lines + assertThat( sessionMetricsLog.lines().count() ) + .as( "The StatisticalLoggingSessionEventListener should write a line per metric (" + + numberOfMetrics + " lines) plus a header and a footer (2 lines)" ) + .isEqualTo( numberOfMetrics + 2 ); + // Total time + long sumDuration = metricList.stream().map( SessionMetric::getDuration ).mapToLong( Long::longValue ).sum(); + assertThat( sumDuration ).isLessThanOrEqualTo( sessionNanoDuration ); + } + + private SessionMetric extractMetric(String logMessage, String regex) { + Pattern pattern = Pattern.compile( regex ); + Matcher matcher = pattern.matcher( logMessage ); + assertTrue( matcher.find() ); + return new SessionMetric( Long.parseLong( matcher.group( 1 ) ), Integer.parseInt( matcher.group( 2 ) ) ); + } + + private static class SessionMetric { + private final long duration; + private final int count; + + public SessionMetric(long duration, int count) { + this.duration = duration; + this.count = count; + } + + public long getDuration() { + return duration; + } + + public int getCount() { + return count; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/CreationUpdatedTimestampInEmbeddableTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/CreationUpdatedTimestampInEmbeddableTest.java new file mode 100644 index 000000000000..1cf0fa0b94a5 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/CreationUpdatedTimestampInEmbeddableTest.java @@ -0,0 +1,86 @@ +package org.hibernate.orm.test.annotations; + +import jakarta.persistence.*; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel( + annotatedClasses = { + CreationUpdatedTimestampInEmbeddableTest.Event.class, + CreationUpdatedTimestampInEmbeddableTest.History.class, + } +) +@SessionFactory +class CreationUpdatedTimestampInEmbeddableTest { + + @BeforeEach + void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + Event event = new Event(); + event.id = 1L; + event.name = "conference"; + session.persist( event ); + } ); + } + + @AfterEach + void tearDown(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.createMutationQuery( "delete Event" ).executeUpdate(); + } ); + } + + @Test + void test(SessionFactoryScope scope) { + LocalDateTime created = scope.fromTransaction( session -> { + Event fruit = session.get( Event.class, 1L ); + return fruit.history.created; + } ); + + scope.inTransaction( session -> { + Event event = session.get( Event.class, 1L ); + event.name = "concert"; + } ); + + scope.inTransaction( session -> { + Event event = session.get( Event.class, 1L ); + assertThat( event.history.created ).isEqualTo( created ); + assertThat( event.history.updated ).isNotEqualTo( created ); + assertThat( event.name ).isEqualTo( "concert" ); + } ); + } + + @Entity(name = "Event") + public static class Event { + + @Id + public Long id; + + public String name; + + @Embedded + public History history; + + } + + @Embeddable + public static class History { + @Column + @CreationTimestamp + public LocalDateTime created; + + @Column + @UpdateTimestamp + public LocalDateTime updated; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/attributeoverride/MapAttributeOverrideTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/attributeoverride/MapAttributeOverrideTest.java new file mode 100644 index 000000000000..af9db96ccaa4 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/attributeoverride/MapAttributeOverrideTest.java @@ -0,0 +1,45 @@ +package org.hibernate.orm.test.annotations.attributeoverride; + +import java.time.LocalTime; +import java.util.Map; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.metamodel.EntityType; +import jakarta.persistence.metamodel.MapAttribute; + +import static jakarta.persistence.metamodel.Type.PersistenceType.BASIC; +import static jakarta.persistence.metamodel.Type.PersistenceType.EMBEDDABLE; +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel( + annotatedClasses = { + Schedule.class, + Route.class + } +) +@SessionFactory +class MapAttributeOverrideTest { + + @Test + @JiraKey("HHH-18516") + void testMapOfEmbeddableKeysWithAttributeOverridesAndBasicValues(SessionFactoryScope sessionFactoryScope) { + sessionFactoryScope.inTransaction(session -> { + EntityType scheduleType = session.getMetamodel().entity(Schedule.class); + MapAttribute departuresMapAttribute = scheduleType.getMap("departures"); + // Presence of @AttributeOverride-s for the key should only affect the type of the key, but not the type of the value + assertThat(departuresMapAttribute.getKeyType().getPersistenceType()).isEqualTo(EMBEDDABLE); + assertThat(departuresMapAttribute.getElementType().getPersistenceType()).isEqualTo(BASIC); + + session.persist(new Schedule(Map.of( + new Route("Hamburg", "Vienna"), LocalTime.NOON, + new Route("Warsaw", "Barcelona"), LocalTime.MIDNIGHT + ))); + assertThat(session.createQuery("FROM Schedule s", Schedule.class).getResultCount()).isOne(); + }); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/attributeoverride/Route.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/attributeoverride/Route.java new file mode 100644 index 000000000000..1e3951ea1c58 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/attributeoverride/Route.java @@ -0,0 +1,18 @@ +package org.hibernate.orm.test.annotations.attributeoverride; + +import jakarta.persistence.Embeddable; + +@Embeddable +public class Route { + + private String origin; + private String destination; + + public Route() { + } + + public Route(String origin, String destination) { + this.origin = origin; + this.destination = destination; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/attributeoverride/Schedule.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/attributeoverride/Schedule.java new file mode 100644 index 000000000000..1e5f0356219e --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/attributeoverride/Schedule.java @@ -0,0 +1,32 @@ +package org.hibernate.orm.test.annotations.attributeoverride; + +import java.time.LocalTime; +import java.util.Map; + +import jakarta.persistence.AttributeOverride; +import jakarta.persistence.Column; +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; + +@Entity +public class Schedule { + + @Id + @GeneratedValue + private Long id; + + @ElementCollection + @Column(name = "time") + @AttributeOverride(name = "key.origin", column = @Column(name = "orig")) + @AttributeOverride(name = "key.destination", column = @Column(name = "dest")) + private Map departures; + + public Schedule() { + } + + public Schedule(Map departures) { + this.departures = departures; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/basic/ListOfByteArrayTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/basic/ListOfByteArrayTest.java new file mode 100644 index 000000000000..4bcfc592ede2 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/basic/ListOfByteArrayTest.java @@ -0,0 +1,36 @@ +package org.hibernate.orm.test.annotations.basic; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import org.hibernate.MappingException; +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.Jpa; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +@JiraKey("HHH-17739") +@Jpa(annotatedClasses = ListOfByteArrayTest.Broken.class) +@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsStandardArrays.class) +public class ListOfByteArrayTest { + @Test void test(EntityManagerFactoryScope scope) { + try { + scope.getEntityManagerFactory(); + fail(); + } + catch (MappingException e) { + assertTrue( e.getMessage().contains("binaryList") ); + } + } + @Entity + static class Broken { + @Id long id; + List binaryList; // this is not supported + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/basic/ListOfStringTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/basic/ListOfStringTest.java new file mode 100644 index 000000000000..71711123647a --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/basic/ListOfStringTest.java @@ -0,0 +1,41 @@ +package org.hibernate.orm.test.annotations.basic; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.Jpa; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@JiraKey("HHH-17739") +@Jpa(annotatedClasses = ListOfStringTest.Unbroken.class) +@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsStandardArrays.class) +public class ListOfStringTest { + @Test void test(EntityManagerFactoryScope scope) { + scope.inTransaction(entityManager -> { + entityManager.persist( new Unbroken( List.of("hello", "world") ) ); + }); + scope.inTransaction(entityManager -> { + assertEquals( List.of("hello", "world"), + entityManager.find(Unbroken.class, 0).stringList ); + }); + + } + @Entity + static class Unbroken { + @Id long id; + List stringList; // this should be OK + + Unbroken(List stringList) { + this.stringList = stringList; + } + Unbroken() { + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/beanvalidation/CollectionActionsValidationTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/beanvalidation/CollectionActionsValidationTest.java new file mode 100644 index 000000000000..47e4acbf8001 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/beanvalidation/CollectionActionsValidationTest.java @@ -0,0 +1,257 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.annotations.beanvalidation; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.ValidationMode; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; +import jakarta.validation.Path; +import jakarta.validation.constraints.Future; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Size; +import org.hibernate.SessionFactory; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.Jpa; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@Jpa(annotatedClasses = { + CollectionActionsValidationTest.TestEntity.class, + CollectionActionsValidationTest.ChildEntity.class, +}, validationMode = ValidationMode.AUTO) +@Jira( "https://hibernate.atlassian.net/browse/HHH-19232" ) +public class CollectionActionsValidationTest { + @Test + public void testPersistEmpty(EntityManagerFactoryScope scope) { + scope.inTransaction( entityManager -> { + final ConstraintViolationException e = assertThrows( ConstraintViolationException.class, () -> { + final TestEntity entity = new TestEntity( 2L ); + assertThat( entity.getNotEmptySet() ).isNull(); + assertThat( entity.getMinSizeList() ).isNull(); + entityManager.persist( entity ); + entityManager.flush(); + } ); + assertThat( e.getConstraintViolations() ).hasSize( 1 ); + assertThat( getPropertyPaths( e ) ).containsOnly( "notEmptySet" ); + } ); + } + + @Test + public void testPersistInvalidChild(EntityManagerFactoryScope scope) { + scope.inTransaction( entityManager -> { + final ConstraintViolationException e = assertThrows( ConstraintViolationException.class, () -> { + final TestEntity entity = new TestEntity( 2L ); + entity.setNotEmptySet( Set.of( new ChildEntity( 2L, "" ) ) ); + entity.setMinSizeList( List.of( "test" ) ); + entityManager.persist( entity ); + entityManager.flush(); + } ); + assertThat( e.getConstraintViolations() ).hasSize( 1 ); + assertThat( getPropertyPaths( e ) ).containsOnly( "name" ); + } ); + } + + @Test + public void testUpdateEmptyUsingGetter(EntityManagerFactoryScope scope) { + scope.inTransaction( entityManager -> { + final ConstraintViolationException e = assertThrows( ConstraintViolationException.class, () -> { + final TestEntity entity = entityManager.find( TestEntity.class, 1L ); + entity.getNotEmptySet().clear(); + entityManager.flush(); + } ); + assertThat( e.getConstraintViolations() ).hasSize( 1 ); + assertThat( getPropertyPaths( e ) ).containsOnly( "notEmptySet" ); + + entityManager.clear(); + + final ConstraintViolationException e2 = assertThrows( ConstraintViolationException.class, () -> { + final TestEntity entity = entityManager.find( TestEntity.class, 1L ); + entity.getMinSizeList().clear(); + entityManager.flush(); + } ); + assertThat( e2.getConstraintViolations() ).hasSize( 1 ); + assertThat( getPropertyPaths( e2 ) ).containsOnly( "minSizeList" ); + } ); + } + + @Test + public void testUpdateEmptyUsingSetter(EntityManagerFactoryScope scope) { + scope.inTransaction( entityManager -> { + final ConstraintViolationException e = assertThrows( ConstraintViolationException.class, () -> { + final TestEntity entity = entityManager.find( TestEntity.class, 1L ); + entity.setNotEmptySet( Set.of() ); + entityManager.flush(); + } ); + assertThat( e.getConstraintViolations() ).hasSize( 1 ); + assertThat( getPropertyPaths( e ) ).containsOnly( "notEmptySet" ); + + entityManager.clear(); + + final ConstraintViolationException e2 = assertThrows( ConstraintViolationException.class, () -> { + final TestEntity entity = entityManager.find( TestEntity.class, 1L ); + entity.setMinSizeList( List.of() ); + entityManager.flush(); + } ); + assertThat( e2.getConstraintViolations() ).hasSize( 1 ); + assertThat( getPropertyPaths( e2 ) ).containsOnly( "minSizeList" ); + } ); + } + + @Test + public void testUpdateNull(EntityManagerFactoryScope scope) { + scope.inTransaction( entityManager -> { + final TestEntity entity = new TestEntity( 3L ); + entity.setNotEmptySet( Set.of( new ChildEntity( 3L, "child_3" ) ) ); + entity.setMinSizeList( List.of( "three" ) ); + entityManager.persist( entity ); + } ); + scope.inTransaction( entityManager -> { + final ConstraintViolationException e = assertThrows( ConstraintViolationException.class, () -> { + final TestEntity entity = entityManager.find( TestEntity.class, 3L ); + entity.setNotEmptySet( null ); + entityManager.flush(); + } ); + assertThat( e.getConstraintViolations() ).hasSize( 1 ); + assertThat( getPropertyPaths( e ) ).containsOnly( "notEmptySet" ); + } ); + } + + @Test + public void testUpdateInvalidChild(EntityManagerFactoryScope scope) { + scope.inTransaction( entityManager -> { + final ConstraintViolationException e = assertThrows( ConstraintViolationException.class, () -> { + final TestEntity entity = entityManager.find( TestEntity.class, 1L ); + final ChildEntity child = entity.getNotEmptySet().iterator().next(); + child.setName( "" ); + entityManager.flush(); + } ); + assertThat( e.getConstraintViolations() ).hasSize( 1 ); + assertThat( getPropertyPaths( e ) ).containsOnly( "name" ); + } ); + } + + @Test + public void testUpdateCollectionUsingGetterAndBasicProperty(EntityManagerFactoryScope scope) { + scope.inTransaction( entityManager -> { + final ConstraintViolationException e = assertThrows( ConstraintViolationException.class, () -> { + final TestEntity entity = entityManager.find( TestEntity.class, 1L ); + entity.getNotEmptySet().clear(); + entity.setExpiryDate( LocalDate.now().minusDays( 1L ) ); + entityManager.flush(); + } ); + assertThat( e.getConstraintViolations() ).hasSize( 2 ); + assertThat( getPropertyPaths( e ) ).containsOnly( "notEmptySet", "expiryDate" ); + } ); + } + + private static List getPropertyPaths(ConstraintViolationException e) { + return e.getConstraintViolations().stream().map( ConstraintViolation::getPropertyPath ).map( Path::toString ) + .collect( Collectors.toList() ); + } + + @BeforeEach + public void setUp(EntityManagerFactoryScope scope) { + scope.inTransaction( entityManager -> { + final TestEntity a = new TestEntity( 1L ); + a.setNotEmptySet( Set.of( new ChildEntity( 1L, "child_1" ) ) ); + a.setMinSizeList( List.of( "one" ) ); + entityManager.persist( a ); + } ); + } + + @AfterEach + public void tearDown(EntityManagerFactoryScope scope) { + scope.getEntityManagerFactory().unwrap( SessionFactory.class ).getSchemaManager().truncateMappedObjects(); + } + + @Entity(name = "TestEntity") + static class TestEntity { + @Id + private Long id; + + @ManyToMany(cascade = CascadeType.PERSIST) + @NotEmpty + private Set notEmptySet; + + @ElementCollection + @Size(min = 1) + private List minSizeList; + + @Future + private LocalDate expiryDate = LocalDate.now().plusMonths( 1L ); + + public TestEntity() { + } + + public TestEntity(Long id) { + this.id = id; + } + + public Set getNotEmptySet() { + return notEmptySet; + } + + public void setNotEmptySet(Set notEmptySet) { + this.notEmptySet = notEmptySet; + } + + public List getMinSizeList() { + return minSizeList; + } + + public void setMinSizeList(List minSizeList) { + this.minSizeList = minSizeList; + } + + public LocalDate getExpiryDate() { + return expiryDate; + } + + public void setExpiryDate(LocalDate updateDate) { + this.expiryDate = updateDate; + } + } + + @Entity(name = "ChildEntity") + static class ChildEntity { + @Id + private Long id; + + @NotBlank + private String name; + + public ChildEntity() { + } + + public ChildEntity(Long id, String name) { + this.id = id; + this.name = name; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/cid/NestedCompositeIdWithOrderedUpdatesTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/cid/NestedCompositeIdWithOrderedUpdatesTest.java new file mode 100644 index 000000000000..4c3284067cc0 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/cid/NestedCompositeIdWithOrderedUpdatesTest.java @@ -0,0 +1,250 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.annotations.cid; + + +import jakarta.persistence.*; +import org.hibernate.cfg.BatchSettings; +import org.hibernate.testing.orm.junit.*; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.fail; + +/** + * @author Torsten Landmann + * @author Jan Schatteman + */ +@ServiceRegistry( + settings = { + @Setting(name = BatchSettings.ORDER_UPDATES, value = "true") + } +) +@DomainModel( + annotatedClasses = { + NestedCompositeIdWithOrderedUpdatesTest.A.class, + NestedCompositeIdWithOrderedUpdatesTest.AId.class, + NestedCompositeIdWithOrderedUpdatesTest.B.class, + NestedCompositeIdWithOrderedUpdatesTest.BId.class, + NestedCompositeIdWithOrderedUpdatesTest.C.class + } +) +@SessionFactory +public class NestedCompositeIdWithOrderedUpdatesTest { + + @Test + public void testUpdateOrderingWithNestedCompositeIds(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + // set up entities + C c1 = new C(); + c1.setCvalue("sample_c1"); + session.persist(c1); + C c2 = new C(); + c2.setCvalue("sample_c2"); + session.persist(c2); + + B b1 = new B(); + b1.setId(new BId(c1, "b1_key")); + b1.setBvalue("sample_b1"); + session.persist(b1); + B b2 = new B(); + b2.setId(new BId(c2, "b2_key")); + b2.setBvalue("sample_b2"); + session.persist(b2); + + A a1 = new A(); + a1.setId(new AId(b1, "a1")); + a1.setAvalue("sample_a1"); + session.persist(a1); + A a2 = new A(); + a2.setId(new AId(b2, "a2")); + a2.setAvalue("sample_a2"); + session.persist(a2); + } + ); + + try { + scope.inTransaction( + session -> { + TypedQuery query = session.createQuery("select a from A a", A.class); + List aList = query.getResultList(); + for (A curA : aList) { + curA.setAvalue(curA.getAvalue() + "_modified"); + session.persist(curA); + } + } + ); + } catch (UnsupportedOperationException uoe) { + fail("Shouldn't throw an UnsupportedOperationException!"); + } + } + + @Entity(name = "A") + public static class A + { + @EmbeddedId + private AId id; + + private String avalue; + + public AId getId() + { + return id; + } + + public void setId(AId id) + { + this.id=id; + } + + public String getAvalue() + { + return avalue; + } + + public void setAvalue(String avalue) + { + this.avalue=avalue; + } + } + + @Embeddable + public static class AId + { + @ManyToOne(cascade={}, // cascade nothing + fetch=FetchType.LAZY, + optional=false) + private B b; + + /** + * "key" won't work because h2 database considers it a reserved word, and hibernate doesn't escape it. + * furthermore, the variable name must be after {@link #b}, alphabetically, to account for hibernate's internal sorting. + */ + private String zkey; + + public AId() + { + } + + public AId(B b, String key) + { + this.b=b; + this.zkey=key; + } + + public B getB() + { + return b; + } + + public void setB(B b) + { + this.b=b; + } + + public String getZkey() + { + return zkey; + } + + public void setZkey(String zkey) + { + this.zkey=zkey; + } + } + + @Entity(name = "B") + public static class B + { + @EmbeddedId + private BId id; + + private String bvalue; + + public BId getId() + { + return id; + } + + public void setId(BId id) + { + this.id=id; + } + + public String getBvalue() + { + return bvalue; + } + + public void setBvalue(String bvalue) + { + this.bvalue=bvalue; + } + } + + @Embeddable + public static class BId + { + @ManyToOne(cascade={}, // cascade nothing + fetch=FetchType.LAZY, + optional=false) + private C c; + + /** + * "key" won't work because h2 database considers it a reserved word, and hibernate doesn't escape it. + * furthermore, the variable name must be after {@link #c}, alphabetically, to account for hibernate's internal sorting. + */ + private String zkey; + + public BId() + { + } + + public BId(C c, String key) + { + this.c=c; + this.zkey=key; + } + + public C getC() + { + return c; + } + + public void setC(C c) + { + this.c=c; + } + + public String getZkey() + { + return zkey; + } + + public void setZkey(String zkey) + { + this.zkey=zkey; + } + } + + @Entity(name = "C") + public static class C + { + @Id + private String cvalue; + + public String getCvalue() + { + return cvalue; + } + + public void setCvalue(String cvalue) + { + this.cvalue=cvalue; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/comment/CommentElementCollectionTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/comment/CommentElementCollectionTest.java new file mode 100644 index 000000000000..b32b25120536 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/comment/CommentElementCollectionTest.java @@ -0,0 +1,81 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.annotations.comment; + + +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.OrderColumn; +import org.hibernate.annotations.Comment; +import org.hibernate.boot.Metadata; +import org.hibernate.boot.spi.BootstrapContext; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.integrator.spi.Integrator; +import org.hibernate.mapping.PersistentClass; +import org.hibernate.service.spi.SessionFactoryServiceRegistry; +import org.hibernate.testing.orm.junit.BootstrapServiceRegistry; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import java.util.Collection; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@JiraKey(value = "HHH-19011") +@DomainModel(annotatedClasses = {CommentElementCollectionTest.MainEntity.class}) +@SessionFactory +@BootstrapServiceRegistry(integrators = CommentElementCollectionTest.MetadataIntegrator.class) +public class CommentElementCollectionTest { + private static Metadata METADATA; + + @Test + public void testTableCommentsPlacement(SessionFactoryScope scope) { + scope.inSession(session -> { + Collection entityBindings = METADATA.getEntityBindings(); + assertThat( entityBindings.iterator().next().getTable().getComment() ).isEqualTo( + MainEntity.EXPECTED_MAIN_ENTITY_TABLE_COMMENT ); + + Collection collectionBindings = METADATA.getCollectionBindings(); + assertThat( collectionBindings.iterator().next().getCollectionTable().getComment() ).isEqualTo( + MainEntity.EXPECTED_ELEMENT_COLLECTION_TABLE_COMMENT ); + }); + } + + @Entity + @Comment(MainEntity.EXPECTED_MAIN_ENTITY_TABLE_COMMENT) + static class MainEntity { + public static final String EXPECTED_MAIN_ENTITY_TABLE_COMMENT = "Expected main entity table level comment"; + public static final String EXPECTED_ELEMENT_COLLECTION_TABLE_COMMENT = "Expected element collection table level comment"; + + @Id + @GeneratedValue + public long id; + + @ElementCollection + @OrderColumn(name = "elementOrder", nullable = false) + @Comment(EXPECTED_ELEMENT_COLLECTION_TABLE_COMMENT) + public List embeddableValues; + } + + public static class MetadataIntegrator implements Integrator { + @Override + public void integrate(Metadata metadata, BootstrapContext bootstrapContext, SessionFactoryImplementor sessionFactory) { + METADATA = metadata; + } + + @Override + public void disintegrate(SessionFactoryImplementor sessionFactory, SessionFactoryServiceRegistry serviceRegistry) { + // do nothing + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/derivedidentities/OneToManyEmbeddableId.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/derivedidentities/OneToManyEmbeddableId.java new file mode 100644 index 000000000000..66906a093907 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/derivedidentities/OneToManyEmbeddableId.java @@ -0,0 +1,176 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.annotations.derivedidentities; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@JiraKey("HHH-18702") +@DomainModel( + annotatedClasses = { + OneToManyEmbeddableId.Parent.class, + OneToManyEmbeddableId.FirstChild.class, + OneToManyEmbeddableId.SecondChild.class, + } +) +@SessionFactory +public class OneToManyEmbeddableId { + private static final BigDecimal FIRST_CHILD_CODE = new BigDecimal( 2 ); + + @BeforeAll + public static void init(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( BigDecimal.TEN, "Lio" ); + FirstChild firstChild = new FirstChild( parent, BigDecimal.ONE, FIRST_CHILD_CODE ); + SecondChild secondChild = new SecondChild( firstChild, BigDecimal.TEN, "Al" ); + firstChild.addChild( secondChild ); + session.persist( parent ); + session.persist( firstChild ); + session.persist( secondChild ); + } + ); + } + + @Test + public void propertyNavigationTest(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + FirstChild firstChild = session.createQuery( + "select f from FirstChild f where f.code = :code", FirstChild.class ) + .setParameter( "code", FIRST_CHILD_CODE ).getSingleResult(); + + assertThat( firstChild ).isNotNull(); + + assertThat( firstChild.getChildren() ).hasSize( 1 ); + } + ); + + } + + @Entity(name = "TestEntity") + public static class Parent { + @Id + private BigDecimal id; + + private String name; + + public Parent() { + } + + public Parent(BigDecimal id, String name) { + this.id = id; + this.name = name; + } + + @OneToMany(mappedBy = "id.parent") + private List children = new ArrayList<>(); + + void addChild(FirstChild firstChild) { + children.add( firstChild ); + } + } + + @Entity(name = "FirstChild") + public static class FirstChild { + @EmbeddedId + private FirstChildId id; + + @Column(name = "FIRST_CHILD_CODE") + private BigDecimal code; + + @OneToMany(mappedBy = "id.firstChild") + private List children = new ArrayList<>(); + + public FirstChildId getId() { + return id; + } + + public FirstChild() { + } + + public FirstChild(Parent parent, BigDecimal bigDecimalNum, BigDecimal code) { + this.id = new FirstChildId( bigDecimalNum, parent ); + parent.addChild( this ); + this.code = code; + } + + public List getChildren() { + return children; + } + + void addChild(SecondChild child) { + children.add( child ); + } + } + + @Embeddable + public static class FirstChildId { + private BigDecimal bigDecimalNum; + + @ManyToOne() + private Parent parent; + + public FirstChildId() { + } + + public FirstChildId(BigDecimal bigDecimalNum, Parent parent) { + this.bigDecimalNum = bigDecimalNum; + this.parent = parent; + } + } + + @Entity(name = "SecondChild") + public static class SecondChild { + @EmbeddedId + private SecondChildId id; + + private String name; + + public SecondChild() { + } + + public SecondChild(FirstChild firstChild, BigDecimal bigDecimalNum, String name) { + this.id = new SecondChildId( bigDecimalNum, firstChild ); + this.name = name; + } + } + + @Embeddable + public static class SecondChildId { + private BigDecimal bigDecimalNum; + + @ManyToOne + @JoinColumn(name = "FIRST_CHILD_CODE", referencedColumnName = "FIRST_CHILD_CODE") + private FirstChild firstChild; + + public SecondChildId() { + } + + public SecondChildId(BigDecimal bigDecimalNum, FirstChild firstChild) { + this.bigDecimalNum = bigDecimalNum; + this.firstChild = firstChild; + } + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/entity/Forest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/entity/Forest.java index 93aa841e523b..2d9814b4d373 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/entity/Forest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/entity/Forest.java @@ -27,6 +27,7 @@ import org.hibernate.annotations.SelectBeforeUpdate; import org.hibernate.annotations.Where; +import jakarta.persistence.Column; import jakarta.persistence.Convert; import jakarta.persistence.ElementCollection; import jakarta.persistence.Entity; @@ -63,6 +64,7 @@ public class Forest { @OptimisticLock(excluded=true) @JdbcTypeCode( Types.LONGVARCHAR ) + @Column(length = 10000) public String getLongDescription() { return longDescription; } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/naturalid/ByteArrayNaturalIdTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/naturalid/ByteArrayNaturalIdTest.java new file mode 100644 index 000000000000..641f397ed077 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/naturalid/ByteArrayNaturalIdTest.java @@ -0,0 +1,85 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.annotations.naturalid; + +import org.hibernate.annotations.NaturalId; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + + +@DomainModel( + annotatedClasses = { + ByteArrayNaturalIdTest.TestEntity.class, + } +) +@SessionFactory +@JiraKey("HHH-18409") +public class ByteArrayNaturalIdTest { + + private static final String NATURAL_ID_1 = "N1"; + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( + session -> + session.createMutationQuery( "delete TestEntity" ).executeUpdate() + ); + } + + @Test + public void testSelectByNaturalId(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + TestEntity entity = new TestEntity( 1L, new byte[] { 1, 2 }, NATURAL_ID_1 ); + session.persist( entity ); + TestEntity testEntity = session.byNaturalId( TestEntity.class ) + .using( "naturalId2", NATURAL_ID_1 ) + .using( "naturalId1", new byte[] { 1, 2 } ) + .load(); + + assertThat( testEntity ).as( "Loading the entity by its natural id failed" ).isNotNull(); + TestEntity testEntity2 = session.byNaturalId( TestEntity.class ) + .using( "naturalId2", NATURAL_ID_1 ) + .using( "naturalId1", new byte[] { 1, 3 } ) + .load(); + assertThat( testEntity2 ).as( "Loading the entity using wrong natural id failed" ).isNull(); + + } + ); + } + + @Entity(name = "TestEntity") + public static class TestEntity { + @Id + private Long id; + + @NaturalId + private byte[] naturalId1; + + @NaturalId + private String naturalId2; + + public TestEntity() { + } + + public TestEntity(Long id, byte[] naturalId1, String naturalId2) { + this.id = id; + this.naturalId1 = naturalId1; + this.naturalId2 = naturalId2; + } + + + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/secondary/RefToSecondaryTableTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/secondary/RefToSecondaryTableTest.java index 2ae3ccc0bc6a..405b3606b9d7 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/secondary/RefToSecondaryTableTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/secondary/RefToSecondaryTableTest.java @@ -5,10 +5,8 @@ import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactoryScope; import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -//@Disabled("test produces broken SQL and issue needs to be fixed") @TestForIssue(jiraKey = "HHH-15933") @SessionFactory @DomainModel(annotatedClasses = { Split.class, Reference.class }) diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/uniqueconstraint/UniqueConstraintTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/uniqueconstraint/UniqueConstraintTest.java index 5b2c2f935f2c..8a14234ba0a0 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/uniqueconstraint/UniqueConstraintTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/uniqueconstraint/UniqueConstraintTest.java @@ -7,6 +7,7 @@ package org.hibernate.orm.test.annotations.uniqueconstraint; import org.hibernate.JDBCException; +import org.hibernate.community.dialect.InformixDialect; import org.hibernate.dialect.SybaseDialect; import org.hibernate.testing.orm.junit.DomainModel; @@ -27,6 +28,9 @@ */ @DomainModel( annotatedClasses = { Room.class, Building.class, House.class } ) @SessionFactory +@SkipForDialect( dialectClass = InformixDialect.class, + matchSubTypes = true, + reason = "Informix does not properly support unique constraints on nullable columns" ) @SkipForDialect( dialectClass = SybaseDialect.class, matchSubTypes = true, reason = "Sybase does not properly support unique constraints on nullable columns" ) diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/associations/ManyToOneUniqueKeyReferenceWithCustomIdTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/associations/ManyToOneUniqueKeyReferenceWithCustomIdTest.java new file mode 100644 index 000000000000..b8d92ec659ee --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/associations/ManyToOneUniqueKeyReferenceWithCustomIdTest.java @@ -0,0 +1,219 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.associations; + +import java.io.Serializable; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.util.Objects; + +import org.hibernate.HibernateException; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.Type; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.usertype.EnhancedUserType; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; + +/** + * @author Kowsar Atazadeh + */ +@SessionFactory +@DomainModel(annotatedClasses = + { + ManyToOneUniqueKeyReferenceWithCustomIdTest.Phone.class, + ManyToOneUniqueKeyReferenceWithCustomIdTest.User.class + }) +@JiraKey("HHH-18764") +public class ManyToOneUniqueKeyReferenceWithCustomIdTest { + + @Test + void test(SessionFactoryScope scope) { + scope.inTransaction( session -> { + Phone phone = new Phone(); + + User user1 = new User( new CustomId( "u1" ), "Kowsar" ); + session.persist( user1 ); + phone.setUser( user1 ); + session.persist( phone ); + + User user2 = new User( new CustomId( "u2" ), "Someone" ); + session.persist( user2 ); + phone.setUser( user2 ); + session.persist( phone ); + } ); + } + + @Entity(name = "Phone") + static class Phone { + @Id + @GeneratedValue + Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(referencedColumnName = "name", nullable = false) + User user; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + } + + @Entity(name = "_User") + static class User { + @Id + @Type(CustomIdType.class) + CustomId id; + + @NaturalId + String name; + + public User() { + } + + public User(CustomId id, String name) { + this.id = id; + this.name = name; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public CustomId getId() { + return id; + } + + public void setId(CustomId id) { + this.id = id; + } + } + + static class CustomIdType implements EnhancedUserType { + @Override + public String toSqlLiteral(CustomId value) { + return "'" + value.toString() + "'"; + } + + @Override + public String toString(CustomId value) throws HibernateException { + return value.toString(); + } + + @Override + public void nullSafeSet( + PreparedStatement st, CustomId value, int position, + SharedSessionContractImplementor session) throws SQLException { + st.setObject( position, value.toString(), getSqlType() ); + } + + @Override + public CustomId nullSafeGet(ResultSet rs, int position, SharedSessionContractImplementor session, Object owner) + throws SQLException { + String idValue = rs.getString( position ); + return idValue != null ? fromStringValue( idValue ) : null; + } + + @Override + public CustomId fromStringValue(CharSequence sequence) throws HibernateException { + return new CustomId( sequence.toString() ); + } + + @Override + public int getSqlType() { + return Types.VARCHAR; + } + + @Override + public Class returnedClass() { + return CustomId.class; + } + + @Override + public CustomId deepCopy(CustomId value) { + return new CustomId( value.getId() ); + } + + @Override + public boolean isMutable() { + return false; + } + + @Override + public Serializable disassemble(CustomId value) { + return value; + } + + @Override + public CustomId assemble(Serializable cached, Object owner) { + return (CustomId) cached; + } + + @Override + public boolean equals(CustomId x, CustomId y) { + return Objects.equals( x, y ); + } + + @Override + public int hashCode(CustomId x) { + return x != null ? x.hashCode() : 0; + } + } + + static class CustomId implements Serializable { + private String id; + + public CustomId() { + } + + public CustomId(String id) { + this.id = id; + } + + @Override + public String toString() { + return id; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/boot/database/qualfiedTableNaming/DefaultCatalogAndSchemaTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/boot/database/qualfiedTableNaming/DefaultCatalogAndSchemaTest.java index 9a0b383913ef..81536f20816f 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/boot/database/qualfiedTableNaming/DefaultCatalogAndSchemaTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/boot/database/qualfiedTableNaming/DefaultCatalogAndSchemaTest.java @@ -34,6 +34,7 @@ import org.hibernate.boot.spi.BootstrapContext; import org.hibernate.boot.spi.MetadataImplementor; import org.hibernate.cfg.AvailableSettings; +import org.hibernate.community.dialect.InformixDialect; import org.hibernate.dialect.Dialect; import org.hibernate.dialect.SQLServerDialect; import org.hibernate.dialect.SybaseDialect; @@ -309,12 +310,17 @@ public void createSchema_fromSessionFactory() { @Test @SkipForDialect(value = SQLServerDialect.class, - comment = "SQL Server and Sybase support catalogs but their implementation of DatabaseMetaData" + comment = "SQL Server support catalogs but their implementation of DatabaseMetaData" + " throws exceptions when calling getSchemas/getTables with a non-existing catalog," + " which results in nasty errors when generating an update script" + " and some catalogs don't exist.") @SkipForDialect(value = SybaseDialect.class, - comment = "SQL Server and Sybase support catalogs but their implementation of DatabaseMetaData" + comment = "Sybase support catalogs but their implementation of DatabaseMetaData" + + " throws exceptions when calling getSchemas/getTables with a non-existing catalog," + + " which results in nasty errors when generating an update script" + + " and some catalogs don't exist.") + @SkipForDialect(value = InformixDialect.class, + comment = "Informix support catalogs but their implementation of DatabaseMetaData" + " throws exceptions when calling getSchemas/getTables with a non-existing catalog," + " which results in nasty errors when generating an update script" + " and some catalogs don't exist.") @@ -772,8 +778,8 @@ String patternStringForNameWithDifferentQualifier(String patternStringForName) { } private String patternStringForQualifier() { - return ( catalog != null ? Pattern.quote( catalog + "." ) : "" ) - + ( schema != null ? Pattern.quote( schema + "." ) : "" ); + return ( catalog != null ? Pattern.quote( catalog ) + "." : "" ) + + ( schema != null ? Pattern.quote( schema ) + "." : "" ); } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/ForeignPackageSuperclassAccessorTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/ForeignPackageSuperclassAccessorTest.java new file mode 100644 index 000000000000..c3dd6f8d9df5 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/ForeignPackageSuperclassAccessorTest.java @@ -0,0 +1,31 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.bytecode; + +import org.hibernate.orm.test.bytecode.foreignpackage.ConcreteEntity; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +@SessionFactory +@DomainModel(annotatedClasses = { + ConcreteEntity.class, + SuperclassEntity.class +}) +@Jira("https://hibernate.atlassian.net/browse/HHH-19369") +public class ForeignPackageSuperclassAccessorTest { + + @Test + public void test(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.find( SuperclassEntity.class, 1L ); + } ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/SuperclassEntity.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/SuperclassEntity.java new file mode 100644 index 000000000000..ac4bc286b95c --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/SuperclassEntity.java @@ -0,0 +1,30 @@ +package org.hibernate.orm.test.bytecode; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; + +@Entity +@Inheritance(strategy = InheritanceType.JOINED) +public class SuperclassEntity { + @Id + protected long id; + protected String name; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/access/HierarchyPropertyAccessTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/access/HierarchyPropertyAccessTest.java new file mode 100644 index 000000000000..b60561d80eb2 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/access/HierarchyPropertyAccessTest.java @@ -0,0 +1,148 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.access; + +import jakarta.persistence.Access; +import jakarta.persistence.AccessType; +import jakarta.persistence.DiscriminatorColumn; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.MappedSuperclass; +import jakarta.persistence.Transient; +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel(annotatedClasses = { + HierarchyPropertyAccessTest.AbstractSuperclass.class, + HierarchyPropertyAccessTest.ParentEntity.class, + HierarchyPropertyAccessTest.ChildEntity.class, +}) +@SessionFactory +@Jira( "https://hibernate.atlassian.net/browse/HHH-19140" ) +@Jira( "https://hibernate.atlassian.net/browse/HHH-19059" ) +@BytecodeEnhanced +public class HierarchyPropertyAccessTest { + @Test + public void testParent(SessionFactoryScope scope) { + assertThat( scope.getSessionFactory().getMappingMetamodel().findEntityDescriptor( ParentEntity.class ) + .getBytecodeEnhancementMetadata().isEnhancedForLazyLoading() ).isTrue(); + scope.inTransaction( session -> session.persist( new ParentEntity( 1L, "field", "transient: property" ) ) ); + + scope.inTransaction( session -> { + final ParentEntity entity = session.find( ParentEntity.class, 1L ); + assertThat( entity.getPersistProperty() ).isEqualTo( "property" ); + assertThat( entity.getProperty() ).isEqualTo( "transient: property" ); + assertThat( entity.getSuperProperty() ).isEqualTo( 8 ); + + entity.setProperty( "transient: updated" ); + } ); + + scope.inTransaction( session -> { + final ParentEntity entity = session.find( ParentEntity.class, 1L ); + assertThat( entity.getPersistProperty() ).isEqualTo( "updated" ); + assertThat( entity.getProperty() ).isEqualTo( "transient: updated" ); + } ); + } + + @Test + public void testChild(SessionFactoryScope scope) { + assertThat( scope.getSessionFactory().getMappingMetamodel().findEntityDescriptor( ChildEntity.class ) + .getBytecodeEnhancementMetadata().isEnhancedForLazyLoading() ).isTrue(); + scope.inTransaction( session -> session.persist( new ChildEntity( 2L, "field", "transient: property" ) ) ); + + scope.inTransaction( session -> { + ChildEntity entity = session.find( ChildEntity.class, 2L ); + assertThat( entity.getPersistProperty() ).isEqualTo( "property" ); + assertThat( entity.getProperty() ).isEqualTo( "transient: property" ); + assertThat( entity.getSuperProperty() ).isEqualTo( 8 ); + + entity.setProperty( "transient: updated" ); + } ); + + scope.inTransaction( session -> { + ChildEntity entity = session.find( ChildEntity.class, 2L ); + assertThat( entity.getPersistProperty() ).isEqualTo( "updated" ); + assertThat( entity.getProperty() ).isEqualTo( "transient: updated" ); + } ); + } + + @AfterAll + public void cleanup(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @MappedSuperclass + static abstract class AbstractSuperclass { + protected Integer superProperty; + } + + @Entity(name = "ParentEntity") + @DiscriminatorColumn(name = "entity_type") + static class ParentEntity extends AbstractSuperclass { + @Id + private Long id; + + private String field; + + private String persistProperty; + + @Transient + private String property; + + public ParentEntity() { + } + + public ParentEntity(Long id, String field, String property) { + this.id = id; + this.field = field; + this.property = property; + } + + @Access(AccessType.PROPERTY) + public String getPersistProperty() { + this.persistProperty = this.property.substring( 11 ); + return this.persistProperty; + } + + public void setPersistProperty(String persistProperty) { + this.property = "transient: " + persistProperty; + this.persistProperty = persistProperty; + } + + public String getProperty() { + return this.property; + } + + public void setProperty(String property) { + this.property = property; + } + + @Access(AccessType.PROPERTY) + public Integer getSuperProperty() { + return getPersistProperty().length(); + } + + public void setSuperProperty(Integer superProperty) { + this.superProperty = superProperty; + } + } + + @Entity(name = "ChildEntity") + static class ChildEntity extends ParentEntity { + public ChildEntity() { + } + + public ChildEntity(Long id, String field, String property) { + super( id, field, property ); + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/access/InvalidPropertyNameTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/access/InvalidPropertyNameTest.java new file mode 100644 index 000000000000..461ea8cb9861 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/access/InvalidPropertyNameTest.java @@ -0,0 +1,172 @@ +package org.hibernate.orm.test.bytecode.enhancement.access; + +import jakarta.persistence.Access; +import jakarta.persistence.AccessType; +import jakarta.persistence.Basic; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Transient; +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.FailureExpected; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel( + annotatedClasses = { + InvalidPropertyNameTest.SomeEntity.class, + InvalidPropertyNameTest.SomeEntityWithFalsePositive.class + } +) +@SessionFactory +@BytecodeEnhanced +public class InvalidPropertyNameTest { + + + @Test + @FailureExpected(jiraKey = "HHH-16572") + @JiraKey("HHH-16572") + public void test(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.persist( new SomeEntity( 1L, "field", "property" ) ); + } ); + + scope.inTransaction( session -> { + SomeEntity entity = session.get( SomeEntity.class, 1L ); + assertThat( entity.property ).isEqualTo( "from getter: property" ); + + entity.setPropertyMethod( "updated" ); + } ); + + scope.inTransaction( session -> { + SomeEntity entity = session.get( SomeEntity.class, 1L ); + assertThat( entity.property ).isEqualTo( "from getter: updated" ); + } ); + } + + @Test + @JiraKey("HHH-18832") + public void testNoFalsePositive(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.persist( new SomeEntityWithFalsePositive( 1L, "property1-initial", "property2-initial" ) ); + } ); + + // Before HHH-18832 was fixed, lazy-loading enhancement was (incorrectly) skipped, + // resulting at best in `property1` being null in the code below, + // at worst in other errors such as java.lang.NoSuchMethodError: 'java.lang.String org.hibernate.orm.test.bytecode.enhancement.access.InvalidPropertyNameTest$SomeEntityWithFalsePositive.$$_hibernate_read_property1()' + // (see https://hibernate.zulipchat.com/#narrow/channel/132094-hibernate-orm-dev/topic/HHH-16572/near/481330806) + scope.inTransaction( session -> { + SomeEntityWithFalsePositive entity = session.getReference( SomeEntityWithFalsePositive.class, 1L ); + // Lazy-loading triggered by field access + // Proves bytecode enhancement is effective + assertThat( entity.property1 ).isEqualTo( "property1-initial" ); + } ); + + scope.inTransaction( session -> { + SomeEntityWithFalsePositive entity = session.getReference( SomeEntityWithFalsePositive.class, 1L ); + // Proves bytecode enhancement is effective even for the transient method + assertThat( entity.getProperty() ).isEqualTo( "property1-initial property2-initial" ); + } ); + } + + @AfterEach + public void cleanup(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.createQuery( "delete from SomeEntity" ).executeUpdate(); + session.createQuery( "delete from SomeEntityWithFalsePositive" ).executeUpdate(); + } ); + } + + @Entity(name = "SomeEntity") + @Table(name = "SOME_ENTITY") + static class SomeEntity { + @Id + Long id; + + @Basic + String field; + + String property; + + public SomeEntity() { + } + + public SomeEntity(Long id, String field, String property) { + this.id = id; + this.field = field; + this.property = property; + } + + /** + * The following property accessor methods are purposely named incorrectly to + * not match the "property" field. The HHH-16572 change ensures that + * this entity is not (bytecode) enhanced. Eventually further changes will be made + * such that this entity is enhanced in which case the FailureExpected can be removed + * and the cleanup() uncommented. + */ + @Basic + @Access(AccessType.PROPERTY) + public String getPropertyMethod() { + return "from getter: " + property; + } + + public void setPropertyMethod(String property) { + this.property = property; + } + } + + @Entity(name = "SomeEntityWithFalsePositive") + static class SomeEntityWithFalsePositive { + + private Long id; + + private String property1; + + private String property2; + + public SomeEntityWithFalsePositive() { + } + + public SomeEntityWithFalsePositive(Long id, String property1, String property2) { + this.id = id; + this.property1 = property1; + this.property2 = property2; + } + + @Id + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getProperty1() { + return property1; + } + + public void setProperty1(String property1) { + this.property1 = property1; + } + + public String getProperty2() { + return property2; + } + + public void setProperty2(String property2) { + this.property2 = property2; + } + + @Transient + public String getProperty() { + return property1 + " " + property2; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/access/PropertyAccessMemberTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/access/PropertyAccessMemberTest.java new file mode 100644 index 000000000000..fca1ed7f3efa --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/access/PropertyAccessMemberTest.java @@ -0,0 +1,99 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.access; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.metamodel.Attribute; +import jakarta.persistence.metamodel.ManagedType; +import jakarta.persistence.metamodel.Metamodel; +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Member; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * @author Scott Marlow + */ +@SessionFactory +@DomainModel(annotatedClasses = { + PropertyAccessMemberTest.PropertyEntity.class, + PropertyAccessMemberTest.FieldEntity.class +}) +@BytecodeEnhanced +public class PropertyAccessMemberTest { + @Test + public void testPropertyAccessMember(SessionFactoryScope scope) { + scope.inTransaction( entityManager -> { + final Metamodel metaModel = entityManager.getMetamodel(); + final ManagedType managedType = metaModel.managedType( PropertyEntity.class ); + final Attribute attribute = managedType.getDeclaredAttribute( "total" ); + final Member member = attribute.getJavaMember(); + assertEquals( "getTotal", member.getName() ); + } ); + } + + @Test + public void testFieldAccessMember(SessionFactoryScope scope) { + scope.inTransaction( entityManager -> { + final Metamodel metaModel = entityManager.getMetamodel(); + final ManagedType managedType = metaModel.managedType( FieldEntity.class ); + final Attribute attribute = managedType.getDeclaredAttribute( "total" ); + final Member member = attribute.getJavaMember(); + assertEquals( "total", member.getName() ); + } ); + } + + @Entity(name = "PropertyEntity") + static class PropertyEntity { + private int id; + private int total; + + @Id + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public int getTotal() { + return total; + } + + public void setTotal(int total) { + this.total = total; + } + } + + @Entity(name = "FieldEntity") + static class FieldEntity { + @Id + private int id; + private int total; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public int getTotal() { + return total; + } + + public void setTotal(int total) { + this.total = total; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/access/UnsupportedEnhancementStrategyTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/access/UnsupportedEnhancementStrategyTest.java new file mode 100644 index 000000000000..d131692062a9 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/access/UnsupportedEnhancementStrategyTest.java @@ -0,0 +1,290 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.access; + +import jakarta.persistence.Access; +import jakarta.persistence.AccessType; +import jakarta.persistence.Basic; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.MappedSuperclass; +import jakarta.persistence.PostLoad; +import jakarta.persistence.PrePersist; +import jakarta.persistence.Transient; + +import org.hibernate.bytecode.enhance.internal.bytebuddy.EnhancerImpl; +import org.hibernate.bytecode.enhance.spi.EnhancementContext; +import org.hibernate.bytecode.enhance.spi.EnhancementException; +import org.hibernate.bytecode.enhance.spi.Enhancer; +import org.hibernate.bytecode.enhance.spi.UnsupportedEnhancementStrategy; +import org.hibernate.bytecode.internal.bytebuddy.ByteBuddyState; +import org.hibernate.bytecode.spi.ByteCodeHelper; +import org.hibernate.testing.bytecode.enhancement.EnhancerTestContext; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.JiraKey; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.io.InputStream; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@JiraKey("HHH-18833") +public class UnsupportedEnhancementStrategyTest { + + @Test + public void skip() throws IOException { + var context = new EnhancerTestContext() { + @Override + public UnsupportedEnhancementStrategy getUnsupportedEnhancementStrategy() { + // This is currently the default, but we don't care about what's the default here + return UnsupportedEnhancementStrategy.SKIP; + } + }; + byte[] enhancedBytes = doEnhance( SomeEntity.class, context ); + assertThat( enhancedBytes ).isNull(); // null means "not enhanced" + } + + @Test + public void fail() throws IOException { + var context = new UnsupportedEnhancerContext(); + assertThatThrownBy( () -> doEnhance( SomeEntity.class, context ) ).isInstanceOf( EnhancementException.class ) + .hasMessageContainingAll( + String.format( + "Enhancement of [%s] failed because no field named [%s] could be found for property accessor method [%s].", + SomeEntity.class.getName(), + "propertyMethod", + "getPropertyMethod" + ), "To fix this, make sure all property accessor methods have a matching field." + ); + assertThatThrownBy( () -> doEnhance( SomeOtherEntity.class, context ) ).isInstanceOf( EnhancementException.class ) + .hasMessageContainingAll( + String.format( + "Enhancement of [%s] failed because no field named [%s] could be found for property accessor method [%s].", + SomeOtherEntity.class.getName(), + "propertyMethod", + "setPropertyMethod" + ), "To fix this, make sure all property accessor methods have a matching field." + ); + } + + @Test + @SuppressWarnings("deprecation") + public void legacy() throws IOException { + var context = new EnhancerTestContext() { + @Override + public UnsupportedEnhancementStrategy getUnsupportedEnhancementStrategy() { + // This is currently the default, but we don't care about what's the default here + return UnsupportedEnhancementStrategy.LEGACY; + } + }; + byte[] enhancedBytes = doEnhance( SomeEntity.class, context ); + assertThat( enhancedBytes ).isNotNull(); // non-null means enhancement _was_ performed + } + + @Test + @Jira( "https://hibernate.atlassian.net/browse/HHH-18903" ) + @Jira( "https://hibernate.atlassian.net/browse/HHH-18904" ) + public void testEntityListeners() throws IOException { + // non-null means check passed and enhancement _was_ performed + assertThat( doEnhance( EventListenersEntity.class, new UnsupportedEnhancerContext() ) ).isNotNull(); + } + + @Test + @Jira( "https://hibernate.atlassian.net/browse/HHH-18903" ) + @Jira( "https://hibernate.atlassian.net/browse/HHH-18904" ) + public void testAccessTypeFieldEntity() throws IOException { + var context = new UnsupportedEnhancerContext(); + // non-null means check passed and enhancement _was_ performed + assertThat( doEnhance( ExplicitAccessTypeFieldEntity.class, context ) ).isNotNull(); + assertThat( doEnhance( ImplicitAccessTypeFieldEntity.class, context ) ).isNotNull(); + } + + @Test + @Jira( "https://hibernate.atlassian.net/browse/HHH-19059" ) + public void testAccessTypePropertyInherited() throws IOException { + var context = new UnsupportedEnhancerContext(); + // non-null means check passed and enhancement _was_ performed + assertThat( doEnhance( PropertyAccessInheritedEntity.class, context ) ).isNotNull(); + } + + private static byte[] doEnhance(Class entityClass, EnhancementContext context) throws IOException { + final ByteBuddyState byteBuddyState = new ByteBuddyState(); + final Enhancer enhancer = new EnhancerImpl( context, byteBuddyState ); + return enhancer.enhance( entityClass.getName(), getAsBytes( entityClass ) ); + } + + private static byte[] getAsBytes(Class clazz) throws IOException { + final String classFile = clazz.getName().replace( '.', '/' ) + ".class"; + try (InputStream classFileStream = clazz.getClassLoader().getResourceAsStream( classFile )) { + return ByteCodeHelper.readByteCode( classFileStream ); + } + } + + static class UnsupportedEnhancerContext extends EnhancerTestContext { + @Override + public UnsupportedEnhancementStrategy getUnsupportedEnhancementStrategy() { + return UnsupportedEnhancementStrategy.FAIL; + } + } + + @Entity(name = "SomeEntity") + static class SomeEntity { + @Id + Long id; + + @Basic + String field; + + String property; + + /** + * The following property accessor methods are purposely named incorrectly to + * not match the "property" field. The HHH-16572 change ensures that + * this entity is not (bytecode) enhanced. Eventually further changes will be made + * such that this entity is enhanced in which case the FailureExpected can be removed + * and the cleanup() uncommented. + */ + @Basic + @Access(AccessType.PROPERTY) + public String getPropertyMethod() { + return "from getter: " + property; + } + + public void setPropertyMethod(String property) { + this.property = property; + } + } + + @Entity(name = "SomeOtherEntity") + static class SomeOtherEntity { + @Id + Long id; + + @Basic + String field; + + String property; + @Access(AccessType.PROPERTY) + public void setPropertyMethod(String property) { + this.property = property; + } + } + + @Entity(name = "EventListenersEntity") + static class EventListenersEntity { + private UUID id; + + private String status = "new"; + + @Id + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + // special case, we should let it through + public UUID get() { + return id; + } + + @PrePersist + public void setId() { + id = UUID.randomUUID(); + } + + @Transient + public String getState() { + return status; + } + + @PostLoad + public void setState(String state) { + status = "loaded"; + } + + @Transient + public boolean isLoaded() { + return status.equals( "loaded" ); + } + } + + @Entity(name = "ExplicitAccessTypeFieldEntity") + @Access( AccessType.FIELD ) + static class ExplicitAccessTypeFieldEntity { + @Id + private Long id; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long get() { + return id; + } + + public String getSomething() { + return "something"; + } + } + + @Entity(name = "ImplicitAccessTypeFieldEntity") + static class ImplicitAccessTypeFieldEntity { + @Id + private Long id; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long get() { + return id; + } + + public String getAnother() { + return "another"; + } + } + + @MappedSuperclass + static abstract class AbstractSuperclass { + protected String property; + } + + @Entity(name="PropertyAccessInheritedEntity") + static class PropertyAccessInheritedEntity extends AbstractSuperclass { + private Long id; + + @Id + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getProperty() { + return property; + } + + public void setProperty(String property) { + this.property = property; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/basic/ReloadAssociatedEntitiesTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/basic/ReloadAssociatedEntitiesTest.java new file mode 100644 index 000000000000..35acb17bfded --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/basic/ReloadAssociatedEntitiesTest.java @@ -0,0 +1,415 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.bytecode.enhancement.basic; + +import java.util.HashSet; +import java.util.Set; + +import org.hibernate.Hibernate; + +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.MappedSuperclass; +import jakarta.persistence.OneToMany; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel( + annotatedClasses = { + ReloadAssociatedEntitiesTest.SimpleOne.class, + ReloadAssociatedEntitiesTest.SimpleTwo.class, + ReloadAssociatedEntitiesTest.SimpleThree.class, + ReloadAssociatedEntitiesTest.ConcreteOne.class, + ReloadAssociatedEntitiesTest.ConcreteTwo.class, + ReloadAssociatedEntitiesTest.ConcreteThree.class, + ReloadAssociatedEntitiesTest.AbsOne.class, + ReloadAssociatedEntitiesTest.AbsTwo.class, + } +) +@SessionFactory +@BytecodeEnhanced(runNotEnhancedAsWell = true) +@JiraKey("HHH-18565") +public class ReloadAssociatedEntitiesTest { + + private Long oneId; + private Long threeId; + private Long simpleOneId; + private Long simpleThreeId; + + @BeforeEach + public void before(SessionFactoryScope scope) { + scope.inTransaction( s -> { + final var three = new ConcreteThree(); + final var two = new ConcreteTwo( "two", three ); + final var one = new ConcreteOne( "one", two ); + two.getOnes().add( one ); + three.getTwos().add( two ); + + s.persist( one ); + s.persist( two ); + s.persist( three ); + oneId = one.getId(); + threeId = three.getId(); + + final var simpleThree = new SimpleThree( "simple three" ); + final var simpleTwo = new SimpleTwo( "simple two", simpleThree ); + final var simpleOne = new SimpleOne( "simple one", simpleTwo ); + + s.persist( simpleOne ); + s.persist( simpleTwo ); + s.persist( simpleThree ); + simpleOneId = simpleOne.getId(); + simpleThreeId = simpleThree.getId(); + } ); + } + + @AfterEach + void after(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.createMutationQuery( session.getCriteriaBuilder().createCriteriaDelete( ConcreteOne.class ) ) + .executeUpdate(); + session.createMutationQuery( session.getCriteriaBuilder().createCriteriaDelete( ConcreteTwo.class ) ) + .executeUpdate(); + session.createMutationQuery( session.getCriteriaBuilder().createCriteriaDelete( ConcreteThree.class ) ) + .executeUpdate(); + } ); + } + + @Test + public void reloadToOneFromSimpleEntity(SessionFactoryScope scope) { + scope.inTransaction( s -> { + SimpleOne one = s.createQuery( + "select o from SimpleOne o join fetch o.two t where o.id = :oneId", + SimpleOne.class + ) + .setParameter( "oneId", simpleOneId ).getSingleResult(); + + assertThat( one ).isNotNull(); + assertThat( Hibernate.isInitialized( one ) ).isTrue(); + assertThat( Hibernate.isPropertyInitialized( one, "two" ) ).isTrue(); + SimpleTwo two = one.getTwo(); + assertThat( Hibernate.isInitialized( two.getThree() ) ).isFalse(); + + SimpleOne one2 = s.createQuery( + "select o from SimpleOne o join fetch o.two t join fetch t.three rh where o.id = :oneId", + SimpleOne.class + ) + .setParameter( "oneId", simpleOneId ).getSingleResult(); + + assertThat( one2 ).isNotNull(); + assertThat( one2 ).isSameAs( one ); + assertThat( Hibernate.isInitialized( one2 ) ).isTrue(); + assertThat( Hibernate.isPropertyInitialized( one2, "two" ) ).isTrue(); + SimpleTwo two2 = one2.getTwo(); + assertThat( two2 ).isSameAs( two ); + assertThat( Hibernate.isInitialized( two2.getThree() ) ).isTrue(); + } ); + } + + @Test + public void reloadToOneFromParameterizedEntity(SessionFactoryScope scope) { + scope.inTransaction( s -> { + ConcreteOne one = s.createQuery( + "select o from ConcreteOne o join fetch o.two t where o.id = :oneId", + ConcreteOne.class + ) + .setParameter( "oneId", oneId ).getSingleResult(); + + assertThat( one ).isNotNull(); + + ConcreteOne one2 = s.createQuery( + "select o from ConcreteOne o join fetch o.two t join fetch t.three rh where o.id = :oneId", + ConcreteOne.class + ) + .setParameter( "oneId", oneId ).getSingleResult(); + + assertThat( one2 ).isNotNull(); + assertThat( one2 ).isSameAs( one ); + } ); + } + + @Test + public void reloadToManyFromParameterizedEntity(SessionFactoryScope scope) { + scope.inTransaction( s -> { + ConcreteThree three = s.createQuery( + "select t from ConcreteThree t join t.twos tw where t.id = :threeId", + ConcreteThree.class + ) + .setParameter( "threeId", threeId ).getSingleResult(); + + assertThat( three ).isNotNull(); + + ConcreteThree three1 = s.createQuery( + "select t from ConcreteThree t join fetch t.twos tw join fetch tw.ones o where t.id = :threeId", + ConcreteThree.class + ) + .setParameter( "threeId", threeId ).getSingleResult(); + + assertThat( three1 ).isNotNull(); + assertThat( three1 ).isSameAs( three ); + } ); + } + + @Test + public void reloadToManyFromSimpleEntity(SessionFactoryScope scope) { + scope.inTransaction( s -> { + SimpleThree three = s.createQuery( + "select t from SimpleThree t join t.twos tw where t.id = :threeId", + SimpleThree.class + ) + .setParameter( "threeId", simpleThreeId ).getSingleResult(); + + assertThat( three ).isNotNull(); + + SimpleThree three1 = s.createQuery( + "select t from SimpleThree t join fetch t.twos tw join fetch tw.ones o where t.id = :threeId", + SimpleThree.class + ) + .setParameter( "threeId", simpleThreeId ).getSingleResult(); + + assertThat( three1 ).isNotNull(); + assertThat( three1 ).isSameAs( three ); + } ); + } + + @Entity(name = "ConcreteOne") + public static class ConcreteOne extends AbsOne { + + public ConcreteOne() { + } + + public ConcreteOne(String name, ConcreteTwo two) { + super( name, two ); + two.getOnes().add( this ); + } + } + + @MappedSuperclass + public static abstract class AbsOne> { + @Id + @GeneratedValue + private Long id; + + private String name; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "two_id") + private TWO two; + + public AbsOne() { + } + + public AbsOne(String name, TWO two) { + this.two = two; + this.name = name; + } + + public Long getId() { + return id; + } + + public TWO getTwo() { + return two; + } + + public String getName() { + return name; + } + } + + @Entity(name = "ConcreteTwo") + public static class ConcreteTwo extends AbsTwo { + public ConcreteTwo() { + } + + public ConcreteTwo(String name, ConcreteThree concreteThree) { + super( name, concreteThree ); + } + } + + @MappedSuperclass + public static abstract class AbsTwo, THREE> { + @Id + @GeneratedValue + private Long id; + + private String name; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "three_id") + private THREE three; + + @OneToMany(mappedBy = "two") + private Set ones = new HashSet<>(); + + public AbsTwo() { + } + + public AbsTwo(String name, THREE three) { + this.name = name; + this.three = three; + } + + public Long getId() { + return id; + } + + + public THREE getThree() { + return three; + } + + + public Set getOnes() { + return ones; + } + + } + + @Entity(name = "ConcreteThree") + public static class ConcreteThree { + @Id + @GeneratedValue + private Long id; + + private String name; + + @OneToMany(mappedBy = "three") + private Set twos = new HashSet<>(); + + public ConcreteThree() { + } + + public ConcreteThree(String name, Set twos) { + this.name = name; + this.twos = twos; + } + + public Long getId() { + return id; + } + + public Set getTwos() { + return twos; + } + + public String getName() { + return name; + } + } + + @Entity(name = "SimpleOne") + public static class SimpleOne { + @Id + @GeneratedValue + private Long id; + + private String name; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "two_id") + private SimpleTwo two; + + public SimpleOne() { + } + + public SimpleOne(String name, SimpleTwo two) { + this.name = name; + this.two = two; + two.ones.add( this ); + } + + public Long getId() { + return id; + } + + + public SimpleTwo getTwo() { + return two; + } + + } + + @Entity(name = "SimpleTwo") + public static class SimpleTwo { + @Id + @GeneratedValue + private Long id; + + private String name; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "three_id") + private SimpleThree three; + + @OneToMany(mappedBy = "two") + private Set ones = new HashSet<>(); + + public SimpleTwo() { + } + + public SimpleTwo(String name, SimpleThree three) { + this.name = name; + this.three = three; + three.twos.add( this ); + } + + public Long getId() { + return id; + } + + public SimpleThree getThree() { + return three; + } + + public Set getOnes() { + return ones; + } + + } + + @Entity(name = "SimpleThree") + public static class SimpleThree { + @Id + @GeneratedValue + private Long id; + + private String name; + + @OneToMany(mappedBy = "three") + private Set twos = new HashSet<>(); + + public SimpleThree() { + } + + public SimpleThree(String name) { + this.name = name; + } + + public Long getId() { + return id; + } + + public Set getTwos() { + return twos; + } + + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/batch/BatchLazyProxyTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/batch/BatchLazyProxyTest.java new file mode 100644 index 000000000000..86ccfe27c532 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/batch/BatchLazyProxyTest.java @@ -0,0 +1,209 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.bytecode.enhancement.batch; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import org.hibernate.Hibernate; +import org.hibernate.annotations.BatchSize; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.graph.GraphSemantic; +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@DomainModel( + annotatedClasses = { + BatchLazyProxyTest.User.class, + BatchLazyProxyTest.UserInfo.class, + BatchLazyProxyTest.Phone.class, + } + +) +@SessionFactory +@ServiceRegistry( + settings = { + @Setting(name = AvailableSettings.DEFAULT_BATCH_FETCH_SIZE, value = "100") + + } +) +@JiraKey("HHH-18645") +@BytecodeEnhanced(runNotEnhancedAsWell = true) +public class BatchLazyProxyTest { + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + UserInfo info = new UserInfo( "info" ); + Phone phone = new Phone( "123456" ); + info.addPhone( phone ); + User user = new User( 1L, "user1", info ); + session.persist( user ); + } + ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.createMutationQuery( "delete User" ).executeUpdate(); + session.createMutationQuery( "delete Phone" ).executeUpdate(); + session.createMutationQuery( "delete UserInfo" ).executeUpdate(); + } + ); + } + + @Test + public void testBatchInitialize(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + User user = session.createQuery( "select u from User u where u.id = :id", User.class ) + .setEntityGraph( session.createEntityGraph( User.class ), GraphSemantic.FETCH ) + .setParameter( "id", 1L ) + .getSingleResult(); + assertThat( Hibernate.isInitialized( user.getInfo() ) ).isFalse(); + session.createQuery( "select u from User u where u.id = :id", User.class ) + .setParameter( "id", 1L ) + .getSingleResult(); + assertThat( Hibernate.isInitialized( user.getInfo() ) ).isTrue(); + } + ); + } + + @Entity(name = "User") + @Table(name = "USER_TABLE") + @BatchSize(size = 5) + public static class User { + + @Id + private Long id; + + @Column + private String name; + + @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.EAGER) + @JoinColumn(name = "INFO_ID", referencedColumnName = "ID") + private UserInfo info; + + public User() { + } + + public User(long id, String name, UserInfo info) { + this.id = id; + this.name = name; + this.info = info; + info.user = this; + } + + public long getId() { + return id; + } + + public String getName() { + return name; + } + + public UserInfo getInfo() { + return info; + } + } + + @Entity(name = "UserInfo") + public static class UserInfo { + @Id + @GeneratedValue + private Long id; + + @OneToOne(mappedBy = "info", fetch = FetchType.LAZY) + private User user; + + private String info; + + @OneToMany(mappedBy = "info", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + private List phoneList; + + public long getId() { + return id; + } + + public UserInfo() { + } + + public UserInfo(String info) { + this.info = info; + } + + public User getUser() { + return user; + } + + public String getInfo() { + return info; + } + + public List getPhoneList() { + return phoneList; + } + + public void addPhone(Phone phone) { + if ( phoneList == null ) { + phoneList = new ArrayList<>(); + } + this.phoneList.add( phone ); + phone.info = this; + } + } + + @Entity(name = "Phone") + public static class Phone { + @Id + @Column(name = "PHONE_NUMBER") + private String number; + + @ManyToOne + @JoinColumn(name = "INFO_ID") + private UserInfo info; + + public Phone() { + } + + public Phone(String number) { + this.number = number; + } + + public String getNumber() { + return number; + } + + public UserInfo getInfo() { + return info; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/batch/RefreshAndBatchTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/batch/RefreshAndBatchTest.java new file mode 100644 index 000000000000..499fa1582035 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/batch/RefreshAndBatchTest.java @@ -0,0 +1,205 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.batch; + +import java.util.ArrayList; +import java.util.List; + +import org.hibernate.Hibernate; +import org.hibernate.annotations.BatchSize; +import org.hibernate.cfg.AvailableSettings; + +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@DomainModel( + annotatedClasses = { + RefreshAndBatchTest.User.class, + RefreshAndBatchTest.UserInfo.class, + RefreshAndBatchTest.Phone.class, + } + +) +@SessionFactory +@ServiceRegistry( + settings = { + @Setting(name = AvailableSettings.DEFAULT_BATCH_FETCH_SIZE, value = "100") + + } +) +@JiraKey("HHH-18608") +@BytecodeEnhanced(runNotEnhancedAsWell = true) +public class RefreshAndBatchTest { + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + UserInfo info = new UserInfo( "info" ); + Phone phone = new Phone( "123456" ); + info.addPhone( phone ); + User user = new User( 1L, "user1", info ); + session.persist( user ); + } + ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.createMutationQuery( "delete User" ).executeUpdate(); + session.createMutationQuery( "delete Phone" ).executeUpdate(); + session.createMutationQuery( "delete UserInfo" ).executeUpdate(); + } + ); + } + + @Test + public void testRefresh(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + User user = session.createQuery( "select u from User u where u.id = :id", User.class ) + .setParameter( "id", 1L ) + .getSingleResult(); + assertThat( Hibernate.isInitialized( user.getInfo() ) ).isFalse(); + session.refresh( user.getInfo() ); + assertThat( Hibernate.isInitialized( user.getInfo() ) ).isTrue(); + } + ); + } + + @Entity(name = "User") + @Table(name = "USER_TABLE") + @BatchSize(size = 5) + public static class User { + + @Id + private Long id; + + @Column + private String name; + + @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @JoinColumn(name = "INFO_ID", referencedColumnName = "ID") + private UserInfo info; + + public User() { + } + + public User(long id, String name, UserInfo info) { + this.id = id; + this.name = name; + this.info = info; + info.user = this; + } + + public long getId() { + return id; + } + + public String getName() { + return name; + } + + public UserInfo getInfo() { + return info; + } + } + + @Entity(name = "UserInfo") + public static class UserInfo { + @Id + @GeneratedValue + private Long id; + + @OneToOne(mappedBy = "info", fetch = FetchType.LAZY) + private User user; + + private String info; + + @OneToMany(mappedBy = "info", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + private List phoneList; + + public long getId() { + return id; + } + + public UserInfo() { + } + + public UserInfo(String info) { + this.info = info; + } + + public User getUser() { + return user; + } + + public String getInfo() { + return info; + } + + public List getPhoneList() { + return phoneList; + } + + public void addPhone(Phone phone) { + if ( phoneList == null ) { + phoneList = new ArrayList<>(); + } + this.phoneList.add( phone ); + phone.info = this; + } + } + + @Entity(name = "Phone") + public static class Phone { + @Id + @Column(name = "PHONE_NUMBER") + private String number; + + @ManyToOne + @JoinColumn(name = "INFO_ID") + private UserInfo info; + + public Phone() { + } + + public Phone(String number) { + this.number = number; + } + + public String getNumber() { + return number; + } + + public UserInfo getInfo() { + return info; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/batch/RefreshAndBatchTest2.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/batch/RefreshAndBatchTest2.java new file mode 100644 index 000000000000..849033fb6b7d --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/batch/RefreshAndBatchTest2.java @@ -0,0 +1,209 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.batch; + +import java.util.ArrayList; +import java.util.List; + +import org.hibernate.Hibernate; +import org.hibernate.annotations.BatchSize; +import org.hibernate.cfg.AvailableSettings; + +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@DomainModel( + annotatedClasses = { + RefreshAndBatchTest2.User.class, + RefreshAndBatchTest2.UserInfo.class, + RefreshAndBatchTest2.Phone.class, + } + +) +@SessionFactory +@ServiceRegistry( + settings = { + @Setting(name = AvailableSettings.DEFAULT_BATCH_FETCH_SIZE, value = "100") + + } +) +@JiraKey("HHH-18608") +@BytecodeEnhanced(runNotEnhancedAsWell = true) +public class RefreshAndBatchTest2 { + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + User parentUser = new User( 1L, "user1", null ); + session.persist( parentUser ); + UserInfo info = new UserInfo( "info" ); + Phone phone = new Phone( "123456" ); + info.addPhone( phone ); + info.parentUser = parentUser; + User user = new User( 2L, "user1", info ); + session.persist( user ); + } + ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.createMutationQuery( "update UserInfo set parentUser = null" ).executeUpdate(); + session.createMutationQuery( "delete User" ).executeUpdate(); + session.createMutationQuery( "delete Phone" ).executeUpdate(); + session.createMutationQuery( "delete UserInfo" ).executeUpdate(); + } + ); + } + + @Test + public void testRefresh(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + User user = session.createQuery( "select u from User u where u.id = :id", User.class ) + .setParameter( "id", 2L ) + .getSingleResult(); + assertThat( Hibernate.isInitialized( user.getInfo() ) ).isFalse(); + session.refresh( user.getInfo() ); + assertThat( Hibernate.isInitialized( user.getInfo() ) ).isTrue(); + } + ); + } + + @Entity(name = "User") + @Table(name = "USER_TABLE") + @BatchSize(size = 5) + public static class User { + + @Id + private Long id; + + @Column + private String name; + + @ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY) + private UserInfo info; + + public User() { + } + + public User(long id, String name, UserInfo info) { + this.id = id; + this.name = name; + if ( info != null ) { + this.info = info; + info.parentUser = this; + } + } + + public long getId() { + return id; + } + + public String getName() { + return name; + } + + public UserInfo getInfo() { + return info; + } + } + + @Entity(name = "UserInfo") + public static class UserInfo { + @Id + @GeneratedValue + private Long id; + + @ManyToOne + private User parentUser; + + private String info; + + @OneToMany(mappedBy = "info", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + private List phoneList; + + public long getId() { + return id; + } + + public UserInfo() { + } + + public UserInfo(String info) { + this.info = info; + } + + public User getParentUser() { + return parentUser; + } + + public String getInfo() { + return info; + } + + public List getPhoneList() { + return phoneList; + } + + public void addPhone(Phone phone) { + if ( phoneList == null ) { + phoneList = new ArrayList<>(); + } + this.phoneList.add( phone ); + phone.info = this; + } + } + + @Entity(name = "Phone") + public static class Phone { + @Id + @Column(name = "PHONE_NUMBER") + private String number; + + @ManyToOne + @JoinColumn(name = "INFO_ID") + private UserInfo info; + + public Phone() { + } + + public Phone(String number) { + this.number = number; + } + + public String getNumber() { + return number; + } + + public UserInfo getInfo() { + return info; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/RemoveDetachedInstanceTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/RemoveDetachedInstanceTest.java new file mode 100644 index 000000000000..c4d16a1a78ec --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/RemoveDetachedInstanceTest.java @@ -0,0 +1,231 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.detached; + +import java.util.ArrayList; +import java.util.List; + +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OrderColumn; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel( + annotatedClasses = { + RemoveDetachedInstanceTest.Parent.class, + RemoveDetachedInstanceTest.Child.class, + RemoveDetachedInstanceTest.ParentChild.class + } +) +@SessionFactory +@BytecodeEnhanced(runNotEnhancedAsWell = true) +@JiraKey("HHH-18631") +public class RemoveDetachedInstanceTest { + private static final Long PARENT_ID = 1L; + private static final Long CHILD_ID = 2L; + private static final Long PARENT_CHILD_ID = 3L; + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + Parent parent = new Parent( PARENT_ID, "parent" ); + Child child = new Child( CHILD_ID, "child" ); + ParentChild parentChild = new ParentChild( PARENT_CHILD_ID, parent, child ); + + session.persist( parent ); + session.persist( child ); + session.persist( parentChild ); + } ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.createMutationQuery( "delete from ParentChild" ).executeUpdate(); + session.createMutationQuery( "delete from Child" ).executeUpdate(); + session.createMutationQuery( "delete from Parent" ).executeUpdate(); + } ); + } + + @Test + void testRemoveDetachedInstance(SessionFactoryScope scope) { + ParentChild parentChild = scope.fromTransaction( session -> session.get( ParentChild.class, PARENT_CHILD_ID ) ); + assertThat( parentChild ).isNotNull(); + + scope.inTransaction( session -> { + session.remove( parentChild ); + Parent parent = session.get( Parent.class, PARENT_ID ); + assertThat( parent ).isNotNull(); + List pc = parent.getChildren(); + assertThat( pc ).isNotNull(); + assertThat( pc.size() ).isEqualTo( 1 ); + assertThat( pc.get( 0 ) ).isSameAs( parentChild ); + Child child = session.get( Child.class, CHILD_ID ); + assertThat( child ).isNotNull(); + } ); + + scope.inTransaction( session -> { + ParentChild pc = session.get( ParentChild.class, PARENT_CHILD_ID ); + assertThat( pc ).isNull(); + Parent parent = session.get( Parent.class, PARENT_ID ); + assertThat( parent ).isNotNull(); + assertThat( parent.getChildren() ).isEmpty(); + Child child = session.get( Child.class, CHILD_ID ); + assertThat( child ).isNotNull(); + assertThat( child.getChildren() ).isEmpty(); + + } ); + } + + @Test + void testRemoveDetachedInstance2(SessionFactoryScope scope) { + ParentChild parentChild = scope.fromTransaction( session -> session.get( ParentChild.class, PARENT_CHILD_ID ) ); + assertThat( parentChild ).isNotNull(); + + scope.inTransaction( session -> { + session.remove( parentChild ); + session.remove( parentChild.getChild() ); + Parent parent = session.get( Parent.class, PARENT_ID ); + assertThat( parent ).isNotNull(); + List pc = parent.getChildren(); + assertThat( pc ).isNotNull(); + assertThat( pc.size() ).isEqualTo( 1 ); + assertThat( pc.get( 0 ) ).isSameAs( parentChild ); + Child child = session.get( Child.class, CHILD_ID ); + assertThat( child ).isNull(); + } ); + + scope.inTransaction( session -> { + ParentChild pc = session.get( ParentChild.class, PARENT_CHILD_ID ); + assertThat( pc ).isNull(); + Parent parent = session.get( Parent.class, PARENT_ID ); + assertThat( parent ).isNotNull(); + assertThat( parent.getChildren() ).isEmpty(); + Child child = session.get( Child.class, CHILD_ID ); + assertThat( child ).isNull(); + } ); + } + + @Entity(name = "Parent") + public static class Parent { + @Id + private Long id; + + private String name; + + @OneToMany(mappedBy = "parent", fetch = FetchType.EAGER) + List children = new ArrayList<>(); + + public Parent() { + } + + public Parent(Long id, String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public List getChildren() { + return children; + } + + public void setChildren(List children) { + this.children = children; + } + } + + @Entity(name = "ParentChild") + public static class ParentChild { + @Id + private Long id; + + @ManyToOne + private Parent parent; + + @ManyToOne + private Child child; + + public ParentChild() { + } + + public ParentChild(Long id, Parent parent, Child child) { + this.id = id; + this.parent = parent; + this.child = child; + parent.getChildren().add( this ); + child.getChildren().add( this ); + } + + public Long getId() { + return id; + } + + public Parent getParent() { + return parent; + } + + public Child getChild() { + return child; + } + + } + + @Entity(name = "Child") + public static class Child { + + @Id + private Long id; + + private String name; + + @OneToMany(mappedBy = "child") + @OrderColumn + List children = new ArrayList<>(); + + public Child() { + } + + public Child(Long id, String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public List getChildren() { + return children; + } + + public void setChildren(List children) { + this.children = children; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/dirty/DirtyTrackingIdTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/dirty/DirtyTrackingIdTest.java new file mode 100644 index 000000000000..e645a7510c10 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/dirty/DirtyTrackingIdTest.java @@ -0,0 +1,114 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.dirty; + +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import org.hibernate.testing.bytecode.enhancement.EnhancementOptions; +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import java.util.Objects; + +import static org.assertj.core.api.Assertions.assertThat; + +@JiraKey("HHH-19206") +@DomainModel( + annotatedClasses = { + DirtyTrackingIdTest.MyEntity.class + } +) +@SessionFactory +@BytecodeEnhanced +@EnhancementOptions(lazyLoading = true, inlineDirtyChecking = true, extendedEnhancement = true) +public class DirtyTrackingIdTest { + + @Test + public void test(SessionFactoryScope scope) { + scope.inTransaction( session -> { + MyEntity myEntity = new MyEntity(); + myEntity.setAnId( new MyEntityId( 1L ) ); + myEntity.setData( "initial" ); + session.persist( myEntity ); + + // This is unnecessary, but should be harmless... + // Unfortunately it causes dirty checking to misbehave. + // Comment it, and the test will pass. + myEntity.setAnId( new MyEntityId( 1L ) ); + + myEntity.setData( "updated" ); + } ); + scope.inTransaction( session -> { + var entityFromDb = session.find( MyEntity.class, new MyEntityId( 1L ) ); + assertThat( entityFromDb.getData() ).isEqualTo( "updated" ); + } ); + } + + // --- // + + @Entity(name = "MyEntity") + public static class MyEntity { + // The name of this property must be (alphabetically) before the name of "data" to trigger the bug. + // Yes, it's weird. + @EmbeddedId + private MyEntityId anId; + private String data; + + public void setAnId(MyEntityId id) { + this.anId = id; + } + + public MyEntityId getAnId() { + return anId; + } + + public String getData() { + return data; + } + + public void setData(String name) { + this.data = name; + } + } + + @Embeddable + public static class MyEntityId { + private Long id; + + public MyEntityId() { + } + + public MyEntityId(Long id) { + this.id = id; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + @Override + public final boolean equals(Object o) { + if ( !(o instanceof MyEntityId) ) { + return false; + } + + return Objects.equals( id, ( (MyEntityId) o ).id ); + } + + @Override + public int hashCode() { + return Objects.hashCode( id ); + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/graph/LoadAndFetchGraphAssociationNotExplicitlySpecifiedTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/graph/LoadAndFetchGraphAssociationNotExplicitlySpecifiedTest.java new file mode 100644 index 000000000000..63111a85e015 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/graph/LoadAndFetchGraphAssociationNotExplicitlySpecifiedTest.java @@ -0,0 +1,443 @@ +package org.hibernate.orm.test.bytecode.enhancement.graph; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.function.Supplier; + +import org.hibernate.Hibernate; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.graph.GraphSemantic; + +import org.hibernate.testing.bytecode.enhancement.EnhancementOptions; +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.jdbc.SQLStatementInspector; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToOne; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.hibernate.graph.GraphSemantic.FETCH; + +/** + * Checks that associations that are **not** explicitly specified in a fetch/load graph + * are correctly initialized (or not) according to the graph semantics, + * for several association topologies. + */ +@DomainModel( + annotatedClasses = { + LoadAndFetchGraphAssociationNotExplicitlySpecifiedTest.RootEntity.class, + LoadAndFetchGraphAssociationNotExplicitlySpecifiedTest.ContainedEntity.class + } +) +@SessionFactory(useCollectingStatementInspector = true) +@JiraKey("HHH-18489") +@BytecodeEnhanced() +@EnhancementOptions(lazyLoading = true) +@ServiceRegistry(settings = @Setting(name = AvailableSettings.MAX_FETCH_DEPTH, value = "")) +public class LoadAndFetchGraphAssociationNotExplicitlySpecifiedTest { + + @BeforeEach + void init(SessionFactoryScope scope) { + scope.inTransaction( session -> { + for ( long i = 0; i < 3; ++i ) { + RootEntity root = new RootEntity( i * 100 ); + + long j = i * 100; + root.setLazyOneToOneOwned( new ContainedEntity( ++j ) ); + root.setLazyManyToOneOwned( new ContainedEntity( ++j ) ); + root.setEagerOneToOneOwned( new ContainedEntity( ++j ) ); + root.setEagerManyToOneOwned( new ContainedEntity( ++j ) ); + + session.persist( root ); + + ContainedEntity contained; + + contained = new ContainedEntity( ++j ); + root.setLazyOneToOneUnowned( contained ); + contained.setInverseSideOfLazyOneToOneUnowned( root ); + session.persist( contained ); + + contained = new ContainedEntity( ++j ); + root.setEagerOneToOneUnowned( contained ); + contained.setInverseSideOfEagerOneToOneUnowned( root ); + session.persist( contained ); + } + } ); + } + + @AfterEach + void cleanUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.createMutationQuery( + "update ContainedEntity set" + + " inverseSideOfLazyOneToOneUnowned = null" + + ", inverseSideOfEagerOneToOneUnowned = null" + ).executeUpdate(); + session.createMutationQuery( "delete RootEntity" ).executeUpdate(); + session.createMutationQuery( "delete ContainedEntity" ).executeUpdate(); + } ); + } + + // Arguments for the parameterized test below + List queryWithEntityGraph() { + List args = new ArrayList<>(); + for ( GraphSemantic graphSemantic : GraphSemantic.values() ) { + for ( String propertyName : RootEntity.LAZY_PROPERTY_NAMES ) { + args.add( Arguments.of( graphSemantic, propertyName ) ); + } + for ( String propertyName : RootEntity.EAGER_PROPERTY_NAMES ) { + args.add( Arguments.of( graphSemantic, propertyName ) ); + } + } + // Also test without a graph, for reference + args.add( Arguments.of( null, null ) ); + return args; + } + + @Test + public void testWithFetchGraph(SessionFactoryScope scope) { + String propertySpecifiedInGraph = "eagerOneToOneOwned"; + scope.inTransaction( session -> { + var sqlStatementInspector = scope.getCollectingStatementInspector(); + sqlStatementInspector.clear(); + var query = session.createQuery( "select e from RootEntity e where id in (:ids)", RootEntity.class ) + .setFetchSize( 100 ) + // Selecting multiple entities to make sure we don't have side effects (e.g. some context shared across entity instances) + .setParameter( "ids", List.of( 0L, 100L, 200L ) ); + + var graph = session.createEntityGraph( RootEntity.class ); + graph.addAttributeNode( propertySpecifiedInGraph ); + query.applyGraph( graph, FETCH ); + + var resultList = query.list(); + assertThat( resultList ).isNotEmpty(); + for ( String propertyName : RootEntity.LAZY_PROPERTY_NAMES ) { + var expectInitialized = propertyName.equals( propertySpecifiedInGraph ); + assertAssociationInitialized( resultList, propertyName, expectInitialized, sqlStatementInspector ); + } + for ( String propertyName : RootEntity.EAGER_PROPERTY_NAMES ) { + var expectInitialized = propertyName.equals( propertySpecifiedInGraph ); + assertAssociationInitialized( resultList, propertyName, expectInitialized, sqlStatementInspector ); + } + } ); + } + + @ParameterizedTest + @MethodSource + public void queryWithEntityGraph(GraphSemantic graphSemantic, String propertySpecifiedInGraph, SessionFactoryScope scope) { + scope.inTransaction( session -> { + var sqlStatementInspector = scope.getCollectingStatementInspector(); + sqlStatementInspector.clear(); + var query = session.createQuery( "select e from RootEntity e where id in (:ids)", RootEntity.class ) + .setFetchSize( 100 ) + // Selecting multiple entities to make sure we don't have side effects (e.g. some context shared across entity instances) + .setParameter( "ids", List.of( 0L, 100L, 200L ) ); + + if ( graphSemantic != null ) { + var graph = session.createEntityGraph( RootEntity.class ); + graph.addAttributeNode( propertySpecifiedInGraph ); + query.applyGraph( graph, graphSemantic ); + } // else just run the query without a graph + + var resultList = query.list(); + assertThat( resultList ).isNotEmpty(); + for ( String propertyName : RootEntity.LAZY_PROPERTY_NAMES ) { + var expectInitialized = propertyName.equals( propertySpecifiedInGraph ); + assertAssociationInitialized( resultList, propertyName, expectInitialized, sqlStatementInspector ); + } + for ( String propertyName : RootEntity.EAGER_PROPERTY_NAMES ) { + var expectInitialized = propertyName.equals( propertySpecifiedInGraph ) + // Under LOAD semantics, or when not using graphs, + // eager properties also get loaded (even if not specified in the graph). + || GraphSemantic.LOAD.equals( graphSemantic ) || graphSemantic == null; + assertAssociationInitialized( resultList, propertyName, expectInitialized, sqlStatementInspector ); + } + } ); + } + + private void assertAssociationInitialized( + List resultList, + String propertyName, + boolean expectInitialized, + SQLStatementInspector sqlStatementInspector) { + for ( var rootEntity : resultList ) { + sqlStatementInspector.clear(); + if ( propertyName.endsWith( "Unowned" ) ) { + final Supplier supplier; + switch ( propertyName ) { + case ( "lazyOneToOneUnowned" ): + supplier = () -> rootEntity.getLazyOneToOneUnowned(); + break; + case ( "eagerOneToOneUnowned" ): + supplier = () -> rootEntity.getEagerOneToOneUnowned(); + break; + default: + supplier = null; + fail( "unknown association property name : " + propertyName ); + } + assertUnownedAssociationLazyness( + supplier, + rootEntity, + propertyName, + expectInitialized, + sqlStatementInspector + ); + } + else { + final Supplier supplier; + switch ( propertyName ) { + case "lazyOneToOneOwned": + supplier = () -> rootEntity.getLazyOneToOneOwned(); + break; + case "lazyManyToOneOwned": + supplier = () -> rootEntity.getLazyManyToOneOwned(); + break; + case "eagerOneToOneOwned": + supplier = () -> rootEntity.getEagerOneToOneOwned(); + break; + case "eagerManyToOneOwned": + supplier = () -> rootEntity.getEagerManyToOneOwned(); + break; + default: + supplier = null; + fail( "unknown association property name : " + propertyName ); + } + assertOwnedAssociationLazyness( + supplier, + propertyName, + expectInitialized, + sqlStatementInspector + ); + } + } + } + + private static void assertUnownedAssociationLazyness( + Supplier associationSupplier, + RootEntity rootEntity, + String associationName, + boolean expectInitialized, + SQLStatementInspector sqlStatementInspector) { + // for an unowned lazy association the value is null and accessing the association triggers its initialization + assertThat( Hibernate.isPropertyInitialized( rootEntity, associationName ) ) + .as( associationName + " association expected to be initialized ? expected is :" + expectInitialized + " but it's not " ) + .isEqualTo( expectInitialized ); + if ( !expectInitialized ) { + var containedEntity = associationSupplier.get(); + sqlStatementInspector.assertExecutedCount( 1 ); + assertThat( Hibernate.isInitialized( containedEntity ) ); + sqlStatementInspector.clear(); + + assertThat( containedEntity ).isNotNull(); + associationSupplier.get().getName(); + sqlStatementInspector.assertExecutedCount( 0 ); + } + } + + private static void assertOwnedAssociationLazyness( + Supplier associationSupplier, + String associationName, + boolean expectInitialized, + SQLStatementInspector sqlStatementInspector) { + // for an owned lazy association the value is an enhanced proxy, Hibernate.isPropertyInitialized( rootEntity, "lazyManyToOneOwned" ) returns true. + // accessing the association does not trigger its initialization + assertThat( Hibernate.isInitialized( associationSupplier.get() ) ) + .as( associationName + " association expected to be initialized ? expected is :" + expectInitialized + " but it's not " ) + .isEqualTo( expectInitialized ); + if ( !expectInitialized ) { + var containedEntity = associationSupplier.get(); + sqlStatementInspector.assertExecutedCount( 0 ); + + containedEntity.getName(); + sqlStatementInspector.assertExecutedCount( 1 ); + assertThat( Hibernate.isInitialized( containedEntity ) ).isTrue(); + sqlStatementInspector.clear(); + + assertThat( containedEntity ).isNotNull(); + associationSupplier.get().getName(); + sqlStatementInspector.assertExecutedCount( 0 ); + } + } + + @Entity(name = "RootEntity") + static class RootEntity { + + public static final Set LAZY_PROPERTY_NAMES = Set.of( + "lazyOneToOneOwned", "lazyManyToOneOwned", "lazyOneToOneUnowned" + ); + + public static final Set EAGER_PROPERTY_NAMES = Set.of( + "eagerOneToOneOwned", "eagerManyToOneOwned", "eagerOneToOneUnowned" + + ); + + @Id + private Long id; + + private String name; + + @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST) + private ContainedEntity lazyOneToOneOwned; + + @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST) + private ContainedEntity lazyManyToOneOwned; + + @OneToOne(fetch = FetchType.LAZY, mappedBy = "inverseSideOfLazyOneToOneUnowned") + private ContainedEntity lazyOneToOneUnowned; + + @OneToOne(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST) + private ContainedEntity eagerOneToOneOwned; + + @ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST) + private ContainedEntity eagerManyToOneOwned; + + @OneToOne(fetch = FetchType.EAGER, mappedBy = "inverseSideOfEagerOneToOneUnowned") + private ContainedEntity eagerOneToOneUnowned; + + public RootEntity() { + } + + public RootEntity(Long id) { + this.id = id; + } + + @Override + public String toString() { + return "RootEntity#" + id; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public ContainedEntity getLazyOneToOneOwned() { + return lazyOneToOneOwned; + } + + public void setLazyOneToOneOwned(ContainedEntity lazyOneToOneOwned) { + this.lazyOneToOneOwned = lazyOneToOneOwned; + } + + public ContainedEntity getLazyManyToOneOwned() { + return lazyManyToOneOwned; + } + + public void setLazyManyToOneOwned(ContainedEntity lazyManyToOneOwned) { + this.lazyManyToOneOwned = lazyManyToOneOwned; + } + + public ContainedEntity getEagerOneToOneOwned() { + return eagerOneToOneOwned; + } + + public void setEagerOneToOneOwned(ContainedEntity eagerOneToOneOwned) { + this.eagerOneToOneOwned = eagerOneToOneOwned; + } + + public ContainedEntity getEagerManyToOneOwned() { + return eagerManyToOneOwned; + } + + public void setEagerManyToOneOwned(ContainedEntity eagerManyToOneOwned) { + this.eagerManyToOneOwned = eagerManyToOneOwned; + } + + public ContainedEntity getLazyOneToOneUnowned() { + return lazyOneToOneUnowned; + } + + public void setLazyOneToOneUnowned(ContainedEntity lazyOneToOneUnowned) { + this.lazyOneToOneUnowned = lazyOneToOneUnowned; + } + + public ContainedEntity getEagerOneToOneUnowned() { + return eagerOneToOneUnowned; + } + + public void setEagerOneToOneUnowned(ContainedEntity eagerOneToOneUnowned) { + this.eagerOneToOneUnowned = eagerOneToOneUnowned; + } + } + + @Entity(name = "ContainedEntity") + static class ContainedEntity { + + @Id + private Long id; + + private String name; + + @OneToOne(fetch = FetchType.LAZY) + private RootEntity inverseSideOfLazyOneToOneUnowned; + + @OneToOne(fetch = FetchType.LAZY) + private RootEntity inverseSideOfEagerOneToOneUnowned; + + public ContainedEntity() { + } + + public ContainedEntity(Long id) { + this.id = id; + this.name = "Name #" + id; + } + + @Override + public String toString() { + return "ContainedEntity#" + id; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public RootEntity getInverseSideOfLazyOneToOneUnowned() { + return inverseSideOfLazyOneToOneUnowned; + } + + public void setInverseSideOfLazyOneToOneUnowned(RootEntity inverseSideOfLazyOneToOneUnowned) { + this.inverseSideOfLazyOneToOneUnowned = inverseSideOfLazyOneToOneUnowned; + } + + public RootEntity getInverseSideOfEagerOneToOneUnowned() { + return inverseSideOfEagerOneToOneUnowned; + } + + public void setInverseSideOfEagerOneToOneUnowned(RootEntity inverseSideOfEagerOneToOneUnowned) { + this.inverseSideOfEagerOneToOneUnowned = inverseSideOfEagerOneToOneUnowned; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/graph/LoadAndFetchGraphCollectionsNotExplicitlySpecifiedTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/graph/LoadAndFetchGraphCollectionsNotExplicitlySpecifiedTest.java new file mode 100644 index 000000000000..1c8acb026ab0 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/graph/LoadAndFetchGraphCollectionsNotExplicitlySpecifiedTest.java @@ -0,0 +1,235 @@ +package org.hibernate.orm.test.bytecode.enhancement.graph; + +import java.util.ArrayList; +import java.util.List; + +import org.hibernate.Hibernate; +import org.hibernate.graph.GraphSemantic; + +import org.hibernate.testing.bytecode.enhancement.EnhancementOptions; +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToMany; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel( + annotatedClasses = { + LoadAndFetchGraphCollectionsNotExplicitlySpecifiedTest.RootEntity.class, + LoadAndFetchGraphCollectionsNotExplicitlySpecifiedTest.ContainedEntity.class + } +) +@SessionFactory +@JiraKey("HHH-18489") +@BytecodeEnhanced(runNotEnhancedAsWell = true) +@EnhancementOptions(lazyLoading = true) +public class LoadAndFetchGraphCollectionsNotExplicitlySpecifiedTest { + + @BeforeEach + void init(SessionFactoryScope scope) { + scope.inTransaction( session -> { + for ( long i = 0; i < 3; ++i ) { + var root = new RootEntity( i * 100 ); + long j = i * 100; + session.persist( root ); + + var contained = new ContainedEntity( ++j ); + session.persist( contained ); + + root.addEagerContainedEntity( contained ); + + var contained2 = new ContainedEntity( ++j ); + session.persist( contained2 ); + + root.addLazyContainedEntity( contained2 ); + } + } ); + } + + @AfterEach + void cleanUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.createMutationQuery( "delete ContainedEntity" ).executeUpdate(); + session.createMutationQuery( "delete RootEntity" ).executeUpdate(); + } ); + } + + @Test + void queryWithFetchGraph(SessionFactoryScope scope) { + scope.inTransaction( session -> { + var query = session.createQuery( "select e from RootEntity e where id in (:ids)", RootEntity.class ) + .setFetchSize( 100 ) + // Selecting multiple entities to make sure we don't have side effects (e.g. some context shared across entity instances) + .setParameter( "ids", List.of( 0L, 100L, 200L ) ); + + var graph = session.createEntityGraph( RootEntity.class ); + graph.addAttributeNode( "lazyContainedEntities" ); + query.applyGraph( graph, GraphSemantic.FETCH ); + + var resultList = query.list(); + assertThat( resultList ).isNotEmpty(); + for ( var rootEntity : resultList ) { + // GraphSemantic.FETCH, so eagerContainedEntities is lazy because it's not present in the EntityGraph + assertThat( Hibernate.isInitialized( rootEntity.getEagerContainedEntities() )).isFalse(); + assertThat( Hibernate.isInitialized( rootEntity.getLazyContainedEntities() ) ).isTrue(); + } + } ); + } + + @Test + void queryWithLoadGraph(SessionFactoryScope scope) { + scope.inTransaction( session -> { + var query = session.createQuery( "select e from RootEntity e where id in (:ids)", RootEntity.class ) + .setFetchSize( 100 ) + // Selecting multiple entities to make sure we don't have side effects (e.g. some context shared across entity instances) + .setParameter( "ids", List.of( 0L, 100L, 200L ) ); + + var graph = session.createEntityGraph( RootEntity.class ); + graph.addAttributeNode( "lazyContainedEntities" ); + query.applyGraph( graph, GraphSemantic.LOAD ); + + var resultList = query.list(); + assertThat( resultList ).isNotEmpty(); + for ( var rootEntity : resultList ) { + // GraphSemantic.LOAD, eagerContainedEntities maintains is eagerness + assertThat( Hibernate.isInitialized( rootEntity.getEagerContainedEntities() )).isTrue(); + assertThat( Hibernate.isInitialized( rootEntity.getLazyContainedEntities() ) ).isTrue(); + } + } ); + } + + @Test + void queryWithNoEntityGraph(SessionFactoryScope scope) { + + scope.inTransaction( session -> { + var query = session.createQuery( "select e from RootEntity e where id in (:ids)", RootEntity.class ) + .setFetchSize( 100 ) + // Selecting multiple entities to make sure we don't have side effects (e.g. some context shared across entity instances) + .setParameter( "ids", List.of( 0L, 100L, 200L ) ); + + var resultList = query.list(); + assertThat( resultList ).isNotEmpty(); + for ( var rootEntity : resultList ) { + assertThat( Hibernate.isInitialized( rootEntity.getEagerContainedEntities() ) ).isTrue(); + assertThat( Hibernate.isInitialized( rootEntity.getLazyContainedEntities() ) ).isFalse(); + } + } ); + } + + @Entity(name = "RootEntity") + static class RootEntity { + + @Id + private Long id; + + private String name; + + @OneToMany(fetch = FetchType.EAGER) + @JoinColumn(name = "eager_id") + private List eagerContainedEntities; + + @OneToMany(fetch = FetchType.LAZY) + @JoinColumn(name = "lazy_id") + private List lazyContainedEntities; + + public RootEntity() { + } + + public RootEntity(Long id) { + this.id = id; + this.name = "Name #" + id; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public List getEagerContainedEntities() { + return eagerContainedEntities; + } + + public void setEagerContainedEntities(List eagerContainedEntities) { + this.eagerContainedEntities = eagerContainedEntities; + } + + public List getLazyContainedEntities() { + return lazyContainedEntities; + } + + public void setLazyContainedEntities(List lazyContainedEntities) { + this.lazyContainedEntities = lazyContainedEntities; + } + + public void addEagerContainedEntity(ContainedEntity containedEntity) { + if ( eagerContainedEntities == null ) { + eagerContainedEntities = new ArrayList<>(); + } + eagerContainedEntities.add( containedEntity ); + } + + public void addLazyContainedEntity(ContainedEntity containedEntity) { + if ( lazyContainedEntities == null ) { + lazyContainedEntities = new ArrayList<>(); + } + lazyContainedEntities.add( containedEntity ); + } + + @Override + public String toString() { + return "RootEntity#" + id; + } + } + + @Entity(name = "ContainedEntity") + static class ContainedEntity { + + @Id + private Long id; + + private String name; + + public ContainedEntity() { + } + + public ContainedEntity(Long id) { + this.id = id; + this.name = "Name #" + id; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return "ContainedEntity#" + id; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/LazyLoadingAndParameterizedInheritanceTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/LazyLoadingAndParameterizedInheritanceTest.java index 35523f23cc27..e4b2f01db285 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/LazyLoadingAndParameterizedInheritanceTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/LazyLoadingAndParameterizedInheritanceTest.java @@ -32,6 +32,7 @@ import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; @DomainModel( annotatedClasses = { @@ -103,6 +104,14 @@ public void test(SessionFactoryScope scope) { } ); } + @Test + public void testCollectionWrite() { + Three three = new Three(); + Two two = new Two(); + assertThatNoException().isThrownBy(() -> three.setTwos(Set.of(two))); + assertThat(two.getThree()).isSameAs(three); + } + @Entity(name = "One") public static class One extends AbsOne { } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/LazyLoadingByEnhancerSetterTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/LazyLoadingByEnhancerSetterTest.java index c490d8068c9f..d98dd6c865f1 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/LazyLoadingByEnhancerSetterTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/LazyLoadingByEnhancerSetterTest.java @@ -10,7 +10,6 @@ import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; import org.hibernate.testing.orm.junit.DomainModel; -import org.hibernate.testing.orm.junit.FailureExpected; import org.hibernate.testing.orm.junit.JiraKey; import org.hibernate.testing.orm.junit.ServiceRegistry; import org.hibernate.testing.orm.junit.SessionFactory; @@ -79,7 +78,7 @@ public void testField(SessionFactoryScope scope) { } @Test - @FailureExpected( jiraKey = "HHH-10747" ) + // failure doesn't occur with HHH-16572 change @FailureExpected( jiraKey = "HHH-10747" ) public void testProperty(SessionFactoryScope scope) { scope.inTransaction( s -> { ItemProperty input = new ItemProperty(); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/AncestorEntity.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/AncestorEntity.java new file mode 100644 index 000000000000..ab7cdffb8a4d --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/AncestorEntity.java @@ -0,0 +1,35 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.optimizer; + +import jakarta.persistence.Entity; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; +import org.hibernate.orm.test.bytecode.enhancement.optimizer.parent.Ancestor; + +@Entity(name = "AncestorEntity") +@Inheritance(strategy = InheritanceType.JOINED) +public class AncestorEntity extends Ancestor { + + private Long id; + + private String field; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getField() { + return field; + } + + public void setField(String field) { + this.field = field; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/ChildEntity3.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/ChildEntity3.java new file mode 100644 index 000000000000..100191b43b98 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/ChildEntity3.java @@ -0,0 +1,20 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.optimizer; + +import jakarta.persistence.Entity; + +@Entity(name = "ChildEntity3") +public class ChildEntity3 extends AncestorEntity { + private String childField; + + public String getChildField() { + return childField; + } + + public void setChieldField(String childField) { + this.childField = childField; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/ChildEntity4.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/ChildEntity4.java new file mode 100644 index 000000000000..a8ed90f3ef5b --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/ChildEntity4.java @@ -0,0 +1,20 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.optimizer; + +import jakarta.persistence.Entity; + +@Entity(name = "ChildEntity4") +public class ChildEntity4 extends AncestorEntity { + private String childField; + + public String getChildField() { + return childField; + } + + public void setChieldField(String childField) { + this.childField = childField; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/ChildEntity5.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/ChildEntity5.java new file mode 100644 index 000000000000..ba7540fec1fa --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/ChildEntity5.java @@ -0,0 +1,20 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.optimizer; + +import jakarta.persistence.Entity; + +@Entity(name = "ChildEntity5") +public class ChildEntity5 extends AncestorEntity { + private String childField; + + public String getChildField() { + return childField; + } + + public void setChieldField(String childField) { + this.childField = childField; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/ChildEntity6.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/ChildEntity6.java new file mode 100644 index 000000000000..a996eb9089ed --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/ChildEntity6.java @@ -0,0 +1,20 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.optimizer; + +import jakarta.persistence.Entity; + +@Entity(name = "ChildEntity6") +public class ChildEntity6 extends AncestorEntity { + private String childField; + + public String getChildField() { + return childField; + } + + public void setChieldField(String childField) { + this.childField = childField; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/ChildEntity7.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/ChildEntity7.java new file mode 100644 index 000000000000..9a9775a53cd3 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/ChildEntity7.java @@ -0,0 +1,20 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.optimizer; + +import jakarta.persistence.Entity; + +@Entity(name = "ChildEntity7") +public class ChildEntity7 extends AncestorEntity { + private String childField; + + public String getChildField() { + return childField; + } + + public void setChildField(String childField) { + this.childField = childField; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/ChildEntity8.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/ChildEntity8.java new file mode 100644 index 000000000000..f6bdd29a2a29 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/ChildEntity8.java @@ -0,0 +1,20 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.optimizer; + +import jakarta.persistence.Entity; + +@Entity(name = "ChildEntity8") +public class ChildEntity8 extends AncestorEntity { + private String childField; + + public String getChildField() { + return childField; + } + + public void setChildField(String childField) { + this.childField = childField; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/ChildEntity9.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/ChildEntity9.java new file mode 100644 index 000000000000..b734abdd91ee --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/ChildEntity9.java @@ -0,0 +1,20 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.optimizer; + +import jakarta.persistence.Entity; + +@Entity(name = "ChildEntity9") +public class ChildEntity9 extends AncestorEntity { + private String childField; + + public String getChildField() { + return childField; + } + + public void setChildField(String childField) { + this.childField = childField; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/HierarchyBytecodeOptimizerMethodVisibilityTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/HierarchyBytecodeOptimizerMethodVisibilityTest.java new file mode 100644 index 000000000000..e6e9dd57eed4 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/HierarchyBytecodeOptimizerMethodVisibilityTest.java @@ -0,0 +1,68 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.optimizer; + +import org.hibernate.orm.test.bytecode.enhancement.optimizer.child.ChildEntity10; +import org.hibernate.orm.test.bytecode.enhancement.optimizer.parent.Ancestor; +import org.hibernate.orm.test.bytecode.enhancement.optimizer.parent.ChildEntity2; +import org.hibernate.query.Query; +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel(annotatedClasses = { + Ancestor.class, + AncestorEntity.class, + ChildEntity2.class, + ChildEntity10.class +}) +@SessionFactory +@Jira("https://hibernate.atlassian.net/browse/HHH-19372") +@BytecodeEnhanced +public class HierarchyBytecodeOptimizerMethodVisibilityTest { + + @Test + public void testOptimizerSetPropertyValues(SessionFactoryScope scope) { + ChildEntity2 childEntity2 = new ChildEntity2(); + childEntity2.setId( 1L ); + childEntity2.setField( "field" ); + childEntity2.setChieldField( "childField" ); + + ChildEntity10 childEntity10 = new ChildEntity10(); + childEntity10.setId( 3L ); + childEntity10.setField( "field10" ); + childEntity10.setChieldField( "childField3" ); + + scope.inTransaction( session -> { + session.persist( childEntity2 ); + session.persist( childEntity10 ); + } ); + + scope.inTransaction( session -> { + Query query = session.createQuery( "select c from ChildEntity2 c where c.field = :field", + ChildEntity2.class ); + query.setParameter( "field", "field" ); + assertThat( query.uniqueResult() ).isNotNull(); + } ); + + scope.inTransaction( session -> { + Query query = session.createQuery( "select c from ChildEntity10 c where c.field = :field", + ChildEntity10.class ); + query.setParameter( "field", "field10" ); + assertThat( query.uniqueResult() ).isNotNull(); + } ); + } + + @AfterAll + public void cleanup(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/HierarchyBytecodeOptimizerOrderingTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/HierarchyBytecodeOptimizerOrderingTest.java new file mode 100644 index 000000000000..1493c992c53e --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/HierarchyBytecodeOptimizerOrderingTest.java @@ -0,0 +1,115 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.optimizer; + +import org.hibernate.orm.test.bytecode.enhancement.optimizer.parent.Ancestor; +import org.hibernate.query.Query; +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel(annotatedClasses = { + Ancestor.class, + ParentEntity.class, + ChildEntity3.class, + ChildEntity4.class, + ChildEntity5.class, + ChildEntity6.class, + ChildEntity7.class, + ChildEntity8.class, + ChildEntity9.class +}) +@SessionFactory +@Jira("https://hibernate.atlassian.net/browse/HHH-19369") +@BytecodeEnhanced +public class HierarchyBytecodeOptimizerOrderingTest { + + @Test + public void testOptimizerSetPropertyValues(SessionFactoryScope scope) { + ChildEntity3 childEntity3 = new ChildEntity3(); + childEntity3.setId( 3L ); + childEntity3.setName( "child3" ); + childEntity3.setField( "field3" ); + childEntity3.setChieldField( "childField3" ); + + ChildEntity4 childEntity4 = new ChildEntity4(); + childEntity4.setId( 4L ); + childEntity4.setName( "child4" ); + childEntity4.setField( "field4" ); + childEntity4.setChieldField( "childField4" ); + + ChildEntity5 childEntity5 = new ChildEntity5(); + childEntity5.setId( 5L ); + childEntity5.setName( "child5" ); + childEntity5.setField( "field5" ); + childEntity5.setChieldField( "childField5" ); + + ChildEntity6 childEntity6 = new ChildEntity6(); + childEntity6.setId( 6L ); + childEntity6.setName( "child6" ); + childEntity6.setField( "field6" ); + childEntity6.setChieldField( "childField6" ); + + ChildEntity7 childEntity7 = new ChildEntity7(); + childEntity7.setId( 7L ); + childEntity7.setName( "child7" ); + childEntity7.setField( "field7" ); + childEntity7.setChildField( "childField7" ); + + scope.inTransaction( session -> { + session.persist( childEntity3 ); + session.persist( childEntity4 ); + session.persist( childEntity5 ); + session.persist( childEntity6 ); + session.persist( childEntity7 ); + } ); + + scope.inTransaction( session -> { + Query query = session.createQuery( "select c from ChildEntity3 c where c.field = :field", + ChildEntity3.class ); + query.setParameter( "field", "field3" ); + assertThat( query.uniqueResult() ).isNotNull(); + } ); + + scope.inTransaction( session -> { + Query query = session.createQuery( "select c from ChildEntity4 c where c.field = :field", + ChildEntity4.class ); + query.setParameter( "field", "field4" ); + assertThat( query.uniqueResult() ).isNotNull(); + } ); + + scope.inTransaction( session -> { + Query query = session.createQuery( "select c from ChildEntity5 c where c.field = :field", + ChildEntity5.class ); + query.setParameter( "field", "field5" ); + assertThat( query.uniqueResult() ).isNotNull(); + } ); + + scope.inTransaction( session -> { + Query query = session.createQuery( "select c from ChildEntity6 c where c.field = :field", + ChildEntity6.class ); + query.setParameter( "field", "field6" ); + assertThat( query.uniqueResult() ).isNotNull(); + } ); + + scope.inTransaction( session -> { + Query query = session.createQuery( + "select c from ChildEntity7 c where c.field = :field", ChildEntity7.class ); + query.setParameter( "field", "field7" ); + assertThat( query.uniqueResult() ).isNotNull(); + } ); + } + + @AfterAll + public void cleanup(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/HierarchyBytecodeOptimizerTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/HierarchyBytecodeOptimizerTest.java new file mode 100644 index 000000000000..a90b07190ef3 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/HierarchyBytecodeOptimizerTest.java @@ -0,0 +1,52 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.optimizer; + +import org.hibernate.orm.test.bytecode.enhancement.optimizer.child.ChildEntity; +import org.hibernate.query.Query; +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel(annotatedClasses = { + ParentEntity.class, + ChildEntity.class, +}) +@SessionFactory +@Jira("https://hibernate.atlassian.net/browse/HHH-19372") +@BytecodeEnhanced +public class HierarchyBytecodeOptimizerTest { + + @Test + public void testOptimizerSetPropertyValues(SessionFactoryScope scope) { + ChildEntity childEntity = new ChildEntity(); + childEntity.setId( 1L ); + childEntity.setField( "field" ); + childEntity.setChieldField( "childField" ); + + scope.inTransaction( session -> { + session.persist( childEntity ); + } ); + + scope.inTransaction( session -> { + Query query = session.createQuery( "select c from ChildEntity c where c.field = :field", + ChildEntity.class ); + query.setParameter( "field", "field" ); + assertThat( query.uniqueResult() ).isNotNull(); + } ); + + } + + @AfterAll + public void cleanup(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/ParentEntity.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/ParentEntity.java new file mode 100644 index 000000000000..ee933b160ed0 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/ParentEntity.java @@ -0,0 +1,36 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.optimizer; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; + +@Entity(name = "ParentEntity") +@Inheritance(strategy = InheritanceType.JOINED) +public class ParentEntity { + + @Id + private Long id; + + private String field; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getField() { + return field; + } + + public void setField(String field) { + this.field = field; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/child/ChildEntity.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/child/ChildEntity.java new file mode 100644 index 000000000000..d189fad30c9b --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/child/ChildEntity.java @@ -0,0 +1,22 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.optimizer.child; + +import org.hibernate.orm.test.bytecode.enhancement.optimizer.ParentEntity; + +import jakarta.persistence.Entity; + +@Entity(name = "ChildEntity") +public class ChildEntity extends ParentEntity { + private String childField; + + public String getChildField() { + return childField; + } + + public void setChieldField(String childField) { + this.childField = childField; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/child/ChildEntity10.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/child/ChildEntity10.java new file mode 100644 index 000000000000..f1a6756a8c20 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/child/ChildEntity10.java @@ -0,0 +1,21 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.optimizer.child; + +import jakarta.persistence.Entity; +import org.hibernate.orm.test.bytecode.enhancement.optimizer.AncestorEntity; + +@Entity(name = "ChildEntity10") +public class ChildEntity10 extends AncestorEntity { + private String childField; + + public String getChildField() { + return childField; + } + + public void setChieldField(String childField) { + this.childField = childField; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/parent/Ancestor.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/parent/Ancestor.java new file mode 100644 index 000000000000..15389f9b22fb --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/parent/Ancestor.java @@ -0,0 +1,35 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.optimizer.parent; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; + +@Entity(name = "Parent") +@Inheritance(strategy = InheritanceType.JOINED) +public class Ancestor { + @Id + private Long id; + + private String name; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/parent/ChildEntity2.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/parent/ChildEntity2.java new file mode 100644 index 000000000000..05e4917d8c9e --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/parent/ChildEntity2.java @@ -0,0 +1,21 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.optimizer.parent; + +import jakarta.persistence.Entity; +import org.hibernate.orm.test.bytecode.enhancement.optimizer.AncestorEntity; + +@Entity(name = "ChildEntity2") +public class ChildEntity2 extends AncestorEntity { + private String childField; + + public String getChildField() { + return childField; + } + + public void setChieldField(String childField) { + this.childField = childField; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/refresh/RefreshEntityWithLazyPropertyTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/refresh/RefreshEntityWithLazyPropertyTest.java new file mode 100644 index 000000000000..f0c4caa58508 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/refresh/RefreshEntityWithLazyPropertyTest.java @@ -0,0 +1,346 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.refresh; + +import jakarta.persistence.Basic; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import org.hibernate.annotations.Formula; +import org.hibernate.dialect.MySQLDialect; +import org.hibernate.dialect.SQLServerDialect; +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.SkipForDialect; +import org.hibernate.testing.orm.junit.SkipForDialectGroup; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.HashSet; +import java.util.Set; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + + +@Jira("HHH-13377") +@DomainModel( + annotatedClasses = { + RefreshEntityWithLazyPropertyTest.Person.class, + RefreshEntityWithLazyPropertyTest.Course.class, + RefreshEntityWithLazyPropertyTest.Position.class} +) +@SessionFactory +@BytecodeEnhanced +@SkipForDialectGroup( + { + @SkipForDialect( dialectClass = MySQLDialect.class, matchSubTypes = true, reason = "does not support || as String concatenation"), + @SkipForDialect( dialectClass = SQLServerDialect.class, reason = "does not support || as String concatenation"), + } +) +public class RefreshEntityWithLazyPropertyTest { + + private static final Long PERSON_ID = 1L; + private static final Long ASSISTANT_PROFESSOR_POSITION_ID = 1L; + private static final Long PROFESSOR_POSITION_ID = 2L; + private static final String ASSISTANT_POSITION_DESCRIPTION = "Assistant Professor"; + private static final String POSITION_DESCRIPTION = "Professor"; + private static final String PROFESSOR_FIRST_NAME = "John"; + private static final String PROFESSOR_LAST_NAME = "Doe"; + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + Position professorPosition = new Position( PROFESSOR_POSITION_ID, POSITION_DESCRIPTION ); + session.persist( professorPosition ); + + Position assistantProfessor = new Position( ASSISTANT_PROFESSOR_POSITION_ID, + ASSISTANT_POSITION_DESCRIPTION ); + session.persist( assistantProfessor ); + + Person person = new Person( PERSON_ID, PROFESSOR_FIRST_NAME, PROFESSOR_LAST_NAME, assistantProfessor, + professorPosition ); + session.persist( person ); + } ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.createMutationQuery( "delete from Course" ).executeUpdate(); + session.createMutationQuery( "delete from Person" ).executeUpdate(); + session.createMutationQuery( "delete from Position" ).executeUpdate(); + } ); + } + + @Test + public void testRefreshOfLazyField(SessionFactoryScope scope) { + scope.inTransaction( session -> { + Person p = session.find( Person.class, PERSON_ID ); + assertThat( p.getLastName() ).isEqualTo( PROFESSOR_LAST_NAME ); + + String updatedLastName = "Johnson"; + session.createMutationQuery( "update Person p " + + "set p.lastName = :lastName " + + "where p.id = :id" + ) + .setParameter( "lastName", updatedLastName ) + .setParameter( "id", PERSON_ID ) + .executeUpdate(); + + session.refresh( p ); + assertThat( p.getLastName() ).isEqualTo( updatedLastName ); + } ); + } + + @Test + public void testRefreshOfLazyFormula(SessionFactoryScope scope) { + scope.inTransaction( session -> { + Person p = session.find( Person.class, PERSON_ID ); + assertThat( p.getFullName() ).isEqualTo( "John Doe" ); + + p.setLastName( "Johnson" ); + session.flush(); + session.refresh( p ); + assertThat( p.getFullName() ).isEqualTo( "John Johnson" ); + } ); + } + + @Test + public void testRefreshOfLazyOneToMany(SessionFactoryScope scope) { + scope.inTransaction( session -> { + Person p = session.find( Person.class, PERSON_ID ); + assertThat( p.getCourses().size() ).isEqualTo( 0 ); + + session.createMutationQuery( "insert into Course (id, title, person) values (:id, :title, :person) " ) + .setParameter( "id", 0 ) + .setParameter( "title", "Book Title" ) + .setParameter( "person", p ) + .executeUpdate(); + + session.refresh( p ); + assertThat( p.getCourses().size() ).isEqualTo( 1 ); + } ); + } + + @Test + public void testRefreshOfLazyManyToOne(SessionFactoryScope scope) { + scope.inTransaction( session -> { + Person p = session.find( Person.class, PERSON_ID ); + assertThat( p.getPosition().id ).isEqualTo( ASSISTANT_PROFESSOR_POSITION_ID ); + + Position professorPosition = session.find( Position.class, PROFESSOR_POSITION_ID ); + + session.createMutationQuery( + "update Person p " + + "set p.position = :position " + + "where p.id = :personId " + ) + .setParameter( "position", professorPosition ) + .setParameter( "personId", p.getId() ) + .executeUpdate(); + + session.refresh( p ); + assertThat( p.getPosition().id ).isEqualTo( PROFESSOR_POSITION_ID ); + + } ); + } + + @Test + public void testRefreshOfLazyManyToOneCascadeRefresh(SessionFactoryScope scope) { + scope.inTransaction( session -> { + Person p = session.find( Person.class, PERSON_ID ); + Position position = p.getPosition(); + assertThat( position.getId() ).isEqualTo( ASSISTANT_PROFESSOR_POSITION_ID ); + assertThat( position.getDescription() ).isEqualTo( ASSISTANT_POSITION_DESCRIPTION ); + + String newAssistantProfessorDescription = "Assistant Professor 2"; + session.createMutationQuery( + "update Position " + + "set description = :description " + + "where id = :id " + ) + .setParameter( "description", newAssistantProfessorDescription ) + .setParameter( "id", ASSISTANT_PROFESSOR_POSITION_ID ) + .executeUpdate(); + + session.refresh( p ); + // the association has been refreshed because it's annotated with `cascade = CascadeType.REFRESH` + assertThat( p.getPosition().getDescription() ).isEqualTo( newAssistantProfessorDescription ); + } ); + } + + @Test + public void testRefreshOfLazyManyToOneNoCascadeRefresh(SessionFactoryScope scope) { + scope.inTransaction( session -> { + Person p = session.find( Person.class, PERSON_ID ); + Position position = p.getPreviousPosition(); + assertThat( position.getId() ).isEqualTo( PROFESSOR_POSITION_ID ); + assertThat( position.getDescription() ).isEqualTo( POSITION_DESCRIPTION ); + + String newAssistantProfessorDescription = "Assistant Professor 2"; + session.createMutationQuery( + "update Position " + + "set description = :description " + + "where id = :id " + ) + .setParameter( "description", newAssistantProfessorDescription ) + .setParameter( "id", PROFESSOR_POSITION_ID ) + .executeUpdate(); + + session.refresh( p ); + // the association has not been refreshed because it's not annotated with `cascade = CascadeType.REFRESH` + assertThat( p.getPreviousPosition().getDescription() ).isEqualTo( POSITION_DESCRIPTION ); + } ); + } + + @Entity(name = "Person") + public static class Person { + + @Id + private Long id; + + private String firstName; + + @Basic(fetch = FetchType.LAZY) + private String lastName; + + @Basic(fetch = FetchType.LAZY) + @Formula("firstName || ' ' || lastName") + private String fullName; + + @OneToMany(mappedBy = "person", fetch = FetchType.LAZY, cascade = CascadeType.REFRESH, orphanRemoval = true) + private Set courses = new HashSet<>(); + + @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.REFRESH) + private Position position; + + @ManyToOne(fetch = FetchType.LAZY) + private Position previousPosition; + + protected Person() { + } + + public Person(Long id, String firstName, String lastName, Position position, Position previousPosition) { + this.id = id; + this.firstName = firstName; + this.lastName = lastName; + this.position = position; + this.previousPosition = previousPosition; + } + + public Long getId() { + return id; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getFullName() { + return fullName; + } + + public Set getCourses() { + return courses; + } + + public Position getPosition() { + return position; + } + + public Position getPreviousPosition() { + return previousPosition; + } + } + + @Entity(name = "Course") + public static class Course { + + @Id + private Long id; + + private String title; + + @ManyToOne(fetch = FetchType.LAZY) + private Person person; + + protected Course() { + } + + public Course(Long id, String title, Person person) { + this.id = id; + this.title = title; + this.person = person; + } + + public Long getId() { + return id; + } + + public String getTitle() { + return title; + } + + public Person getPerson() { + return person; + } + } + + @Entity(name = "Position") + @Table(name = "POSITION_TABLE") + public static class Position { + + @Id + private Long id; + + @Basic(fetch = FetchType.LAZY) + private String description; + + public Position() { + } + + public Position(Long id, String description) { + this.id = id; + this.description = description; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/saveupdate/SaveUpdateTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/saveupdate/SaveUpdateTest.java new file mode 100644 index 000000000000..ca76457ee138 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/saveupdate/SaveUpdateTest.java @@ -0,0 +1,242 @@ +package org.hibernate.orm.test.bytecode.enhancement.saveupdate; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Version; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@DomainModel( + annotatedClasses = { + SaveUpdateTest.Parent.class, + SaveUpdateTest.Child.class, + SaveUpdateTest.Owned.class, + SaveUpdateTest.Owner.class, + } +) +@SessionFactory +@BytecodeEnhanced(runNotEnhancedAsWell = true) +public class SaveUpdateTest { + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.createQuery( "delete from Child" ).executeUpdate(); + session.createQuery( "delete from Parent" ).executeUpdate(); + } + ); + } + + @Test + @JiraKey("HHH-18713") + public void testSaveUpdate(SessionFactoryScope scope) { + Parent parent = scope.fromTransaction( + session -> { + Parent p = new Parent( "a" ); + Child child = new Child( p, "b" ); + session.persist( p ); + return p; + + } + ); + + scope.inTransaction( session -> { + Child child2 = new Child( parent, "c" ); + session.saveOrUpdate( parent ); + } ); + + scope.inTransaction( session -> { + Parent saved = session.get( Parent.class, parent.getId() ); + assertThat( saved.getChildren().size() ).isEqualTo( 2 ); + } ); + } + + @Test + @JiraKey("HHH-18614") + public void testUpdate(SessionFactoryScope scope) { + Parent parent = scope.fromTransaction( + session -> { + Parent p = new Parent( "a" ); + Child child = new Child( p, "b" ); + session.persist( p ); + return p; + + } + ); + + scope.inTransaction( session -> { + Child child2 = new Child( parent, "c" ); + session.update( parent ); + } ); + + scope.inTransaction( session -> { + Parent saved = session.get( Parent.class, parent.getId() ); + assertThat( saved.getChildren().size() ).isEqualTo( 2 ); + } ); + } + + @Test + @JiraKey("HHH-18614") + void testUpdate2(SessionFactoryScope scope) { + Long ownerId = scope.fromTransaction( session -> { + Owner owner = new Owner( "a" ); + owner.addOwned( new Owned() ); + session.persist( owner ); + return owner.getId(); + } ); + + scope.inTransaction( session -> { + Owner owner2 = new Owner( ownerId, "a" ); + owner2.addOwned( new Owned() ); + session.update( owner2 ); + session.flush(); + } ); + } + + @Test + @JiraKey("HHH-18614") + void testUpdate3(SessionFactoryScope scope) { + Long ownerId = scope.fromTransaction( session -> { + Owner owner = new Owner( ); + owner.addOwned( new Owned() ); + session.persist( owner ); + return owner.getId(); + } ); + + scope.inTransaction( session -> { + Owner owner2 = new Owner( ); + owner2.id = ownerId; + owner2.owneds.add( new Owned() ); + session.update( owner2 ); + session.flush(); + } ); + } + + @Entity(name = "Parent") + public static class Parent { + + @Id + @GeneratedValue + public Long id; + + public Long getId() { + return id; + } + + private String name; + + @Version + @Column(name = "VERSION_COLUMN") + private long version; + + public Parent() { + } + + public Parent(String name) { + this.name = name; + } + + @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL) + private Set children = new LinkedHashSet<>(); + + public Set getChildren() { + return children; + } + } + + @Entity(name = "Child") + public static class Child { + + @Id + @GeneratedValue + public Long id; + + public Long getId() { + return id; + } + + @ManyToOne + private Parent parent; + + private String name; + + public Child() { + } + + public Child(Parent parent, String name) { + this.parent = parent; + parent.children.add( this ); + this.name = name; + } + } + + @Entity(name = "Owned") + public static class Owned { + + @Id + @GeneratedValue + private Long id; + + private String name; + + public Owned() { + } + + public Owned(String name) { + this.name = name; + } + } + + @Entity(name = "Owner") + public static class Owner { + + @Id + @GeneratedValue + private Long id; + + private String name; + + @OneToMany(cascade = CascadeType.ALL) + private List owneds = new ArrayList<>(); + + public Owner() { + } + + public Owner(String name) { + this.name = name; + } + + public Owner(Long id, String name) { + this.id = id; + this.name = name; + } + + public void addOwned(Owned owned) { + owneds.add( owned ); + } + + public Long getId() { + return id; + } + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/foreignpackage/ConcreteEntity.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/foreignpackage/ConcreteEntity.java new file mode 100644 index 000000000000..316de17c5e2f --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/foreignpackage/ConcreteEntity.java @@ -0,0 +1,29 @@ +package org.hibernate.orm.test.bytecode.foreignpackage; + +import org.hibernate.orm.test.bytecode.SuperclassEntity; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + +@Entity +public class ConcreteEntity extends SuperclassEntity { + @Id + protected long id; + protected String bname; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getBname() { + return bname; + } + + public void setBname(String bname) { + this.bname = bname; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/cache/NaturalIdTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/cache/NaturalIdTest.java new file mode 100644 index 000000000000..ad0a4cf7e353 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/cache/NaturalIdTest.java @@ -0,0 +1,98 @@ +package org.hibernate.orm.test.cache; + +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.NaturalIdCache; +import org.hibernate.cfg.AvailableSettings; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + + +@DomainModel(annotatedClasses = { NaturalIdTest.TestEntity.class }) +@SessionFactory +@ServiceRegistry(settings = { + @Setting(name = AvailableSettings.USE_SECOND_LEVEL_CACHE, value = "true"), + @Setting(name = AvailableSettings.USE_QUERY_CACHE, value = "true"), +}) +@JiraKey("HHH_18511") +public class NaturalIdTest { + + @Test + void testGetNaturalIdentifierSnapshot(SessionFactoryScope scope) { + final var naturalId1 = "id1"; + final var naturalId2 = "id2"; + + scope.inTransaction( session -> { + var testEntity = new TestEntity( 1l, naturalId1, naturalId2, "abc" ); + session.persist( testEntity ); + session.flush(); + + var sessionFactory = session.getFactory(); + var entityPersister = sessionFactory.getRuntimeMetamodels() + .getMappingMetamodel() + .getEntityDescriptor( TestEntity.class ); + + Object[] naturalId = (Object[]) entityPersister.getNaturalIdentifierSnapshot( 1l, session ); + assertThat( naturalId[0] ).isEqualTo( naturalId1 ); + assertThat( naturalId[1] ).isEqualTo( naturalId2 ); + } ); + } + + @Entity(name = "CompositeNaturalIdModel") + @Table(name = "CompositeNaturalIdModel") + @NaturalIdCache + @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + public static class TestEntity { + + @Id + private Long id; + + @NaturalId + private String naturalId1; + + @NaturalId + private String naturalId2; + + private String name; + + public TestEntity() { + } + + public TestEntity(Long id, String naturalId1, String naturalId2, String name) { + this.id = id; + this.naturalId1 = naturalId1; + this.naturalId2 = naturalId2; + this.name = name; + } + + public Long getId() { + return id; + } + + public String getNaturalId1() { + return naturalId1; + } + + + public String getNaturalId2() { + return naturalId2; + } + + public String getName() { + return name; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/cache/QueryCacheIncompleteTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/cache/QueryCacheIncompleteTest.java new file mode 100644 index 000000000000..77575b06d239 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/cache/QueryCacheIncompleteTest.java @@ -0,0 +1,116 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.cache; + +import org.hibernate.CacheMode; +import org.hibernate.Session; +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.hibernate.cfg.AvailableSettings; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel(annotatedClasses = { + QueryCacheIncompleteTest.Admin.class, +}) +@SessionFactory +@ServiceRegistry( + settings = { + @Setting(name = AvailableSettings.USE_SECOND_LEVEL_CACHE, value = "true"), + @Setting(name = AvailableSettings.USE_QUERY_CACHE, value = "true"), + @Setting(name = AvailableSettings.QUERY_CACHE_LAYOUT, value = "FULL") + } +) +@JiraKey(value = "HHH-18689") +public class QueryCacheIncompleteTest { + + private Long adminId; + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + adminId = scope.fromTransaction( + session -> { + Admin admin = new Admin(); + admin.setAge( 42 ); + session.persist( admin ); + return admin.getId(); + } + ); + } + + @Test + void testQueryWithEmbeddableParameter(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + // load uninitialized proxy + session.getReference( Admin.class, adminId ); + // load entity + var multiLoader = session.byMultipleIds( Admin.class ); + multiLoader.with( CacheMode.NORMAL ); + multiLoader.multiLoad( adminId ); + + // store in query cache + Admin admin = queryAdmin( session ); + assertThat( admin.getAge() ).isEqualTo( 42 ); + } + ); + + scope.inTransaction( + session -> { + // use query cache + Admin admin = queryAdmin( session ); + assertThat( admin.getAge() ).isEqualTo( 42 ); + } + ); + } + + private Admin queryAdmin(Session s) { + return s.createQuery( "from Admin", Admin.class ).setCacheable( true ).getSingleResult(); + } + + @Entity(name = "Admin") + @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + public static class Admin { + + @Id + @GeneratedValue + private Long id; + + @Column(nullable = false) + private int age; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/cid/CompositeIdAndMergeTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/cid/CompositeIdAndMergeTest.java new file mode 100644 index 000000000000..9dc2d41e5bd2 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/cid/CompositeIdAndMergeTest.java @@ -0,0 +1,162 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.cid; + +import java.util.ArrayList; +import java.util.List; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.IdClass; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@DomainModel( + annotatedClasses = { + CompositeIdAndMergeTest.Order.class, + CompositeIdAndMergeTest.Invoice.class, + CompositeIdAndMergeTest.LineItem.class + } +) +@SessionFactory +@JiraKey("HHH-18131") +public class CompositeIdAndMergeTest { + + @Test + public void testMerge(SessionFactoryScope scope) { + Integer lineItemIndex = 2; + Order persistedOrder = scope.fromTransaction( + session -> { + Order order = new Order( "order" ); + session.persist( order ); + + Invoice invoice = new Invoice( "invoice" ); + LineItem lineItem = new LineItem( lineItemIndex ); + invoice.addLine( lineItem ); + order.setInvoice( invoice ); + + session.merge( order ); + return order; + } + ); + + scope.inTransaction( + session -> { + Order order = session.find( Order.class, persistedOrder.getId() ); + Invoice invoice = order.getInvoice(); + assertThat( invoice ).isNotNull(); + List lines = invoice.getLines(); + assertThat( lines.size() ).isEqualTo( 1 ); + assertThat( lines.get( 0 ).getIndex() ).isEqualTo( lineItemIndex ); + } + ); + } + + @Entity(name = "Order") + @Table(name = "order_table") + public static class Order { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String description; + + @ManyToOne(cascade = { CascadeType.ALL }) + private Invoice invoice; + + public Order() { + } + + public Order(String description) { + this.description = description; + } + + public Long getId() { + return id; + } + + public Invoice getInvoice() { + return invoice; + } + + public void setInvoice(Invoice invoice) { + this.invoice = invoice; + } + } + + @Entity(name = "Invoice") + public static class Invoice { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "number_column") + private String number; + + @OneToMany(mappedBy = "invoice", cascade = { CascadeType.ALL }, orphanRemoval = true) + private List lines = new ArrayList<>(); + + public Invoice() { + } + + public Invoice(String number) { + this.number = number; + } + + public void addLine(LineItem line) { + lines.add( line ); + line.invoice = this; + } + + public List getLines() { + return lines; + } + } + + @Entity + @Table(name = "invoice_lines") + @IdClass(LineItemId.class) + public static class LineItem { + @Id + @ManyToOne + private Invoice invoice; + + @Id + @Column(name = "index_column") + private Integer index; + + public LineItem() { + } + + public LineItem(Integer index) { + this.index = index; + } + + public Integer getIndex() { + return index; + } + } + + public static class LineItemId { + private Long invoice; + private Integer index; + + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/collection/dereferenced/UnversionedCascadeDereferencedCollectionTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/collection/dereferenced/UnversionedCascadeDereferencedCollectionTest.java index c84dc138a3ba..1e2e07e40129 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/collection/dereferenced/UnversionedCascadeDereferencedCollectionTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/collection/dereferenced/UnversionedCascadeDereferencedCollectionTest.java @@ -36,6 +36,7 @@ import org.hibernate.testing.orm.junit.SessionFactoryScope; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; @@ -76,12 +77,9 @@ public void testMergeNullCollection(SessionFactoryScope scope) { scope.inTransaction( session -> { - UnversionedCascadeOne one = (UnversionedCascadeOne) session.merge( unversionedCascadeOne ); + UnversionedCascadeOne one = session.merge( unversionedCascadeOne ); - // after merging, one.getManies() should still be null; - // the EntityEntry loaded state should contain a PersistentCollection though. - - assertNull( one.getManies() ); + assertThat( one.getManies().size() ).isEqualTo( 0 ); EntityEntry eeOne = getEntityEntry( session, one ); AbstractPersistentCollection maniesEEOneStateOrig = (AbstractPersistentCollection) eeOne.getLoadedValue( "manies" ); @@ -109,27 +107,7 @@ public void testMergeNullCollection(SessionFactoryScope scope) { // Ensure the same EntityEntry is being used. assertSame( eeOne, getEntityEntry( session, one ) ); - // Ensure one.getManies() is still null. - assertNull( one.getManies() ); - - // Ensure CollectionEntry for maniesEEOneStateOrig is no longer in the PersistenceContext. - assertNull( getCollectionEntry( session, maniesEEOneStateOrig ) ); - - // Ensure the original CollectionEntry has role, persister, and key set to null. - assertNull( ceManiesOrig.getRole() ); - assertNull( ceManiesOrig.getLoadedPersister() ); - assertNull( ceManiesOrig.getKey() ); - - // Ensure the PersistentCollection (that was previously returned by eeOne.getLoadedState()) - // has key and role set to null. - assertNull( maniesEEOneStateOrig.getKey() ); - assertNull( maniesEEOneStateOrig.getRole() ); - - // Ensure eeOne.getLoadedState() returns null for collection after flush. - assertNull( eeOne.getLoadedValue( "manies" ) ); - - // Ensure the session in maniesEEOneStateOrig has been unset. - assertNull( maniesEEOneStateOrig.getSession() ); + assertThat( one.getManies().size() ).isEqualTo( 0 ); } ); } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/collection/dereferenced/UnversionedNoCascadeDereferencedCollectionTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/collection/dereferenced/UnversionedNoCascadeDereferencedCollectionTest.java index 04976c981c82..0a4613f42224 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/collection/dereferenced/UnversionedNoCascadeDereferencedCollectionTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/collection/dereferenced/UnversionedNoCascadeDereferencedCollectionTest.java @@ -36,6 +36,7 @@ import org.hibernate.testing.orm.junit.SessionFactoryScope; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; @@ -74,12 +75,10 @@ public void testMergeNullCollection(SessionFactoryScope scope) { scope.inTransaction( session -> { - UnversionedNoCascadeOne one = (UnversionedNoCascadeOne) session.merge( unversionedNoCascadeOne ); + UnversionedNoCascadeOne one = session.merge( unversionedNoCascadeOne ); - // after merging, one.getManies() should still be null; - // the EntityEntry loaded state should contain a PersistentCollection though. + assertThat( one.getManies().size() ).isEqualTo( 0 ); - assertNull( one.getManies() ); EntityEntry eeOne = getEntityEntry( session, one ); AbstractPersistentCollection maniesEEOneStateOrig = (AbstractPersistentCollection) eeOne.getLoadedValue( "manies" ); @@ -107,27 +106,8 @@ public void testMergeNullCollection(SessionFactoryScope scope) { // Ensure the same EntityEntry is being used. assertSame( eeOne, getEntityEntry( session, one ) ); - // Ensure one.getManies() is still null. - assertNull( one.getManies() ); - - // Ensure CollectionEntry for maniesEEOneStateOrig is no longer in the PersistenceContext. - assertNull( getCollectionEntry( session, maniesEEOneStateOrig ) ); - - // Ensure the original CollectionEntry has role, persister, and key set to null. - assertNull( ceManiesOrig.getRole() ); - assertNull( ceManiesOrig.getLoadedPersister() ); - assertNull( ceManiesOrig.getKey() ); + assertThat( one.getManies().size() ).isEqualTo( 0 ); - // Ensure the PersistentCollection (that was previously returned by eeOne.getLoadedState()) - // has key and role set to null. - assertNull( maniesEEOneStateOrig.getKey() ); - assertNull( maniesEEOneStateOrig.getRole() ); - - // Ensure eeOne.getLoadedState() returns null for collection after flush. - assertNull( eeOne.getLoadedValue( "manies" ) ); - - // Ensure the session in maniesEEOneStateOrig has been unset. - assertNull( maniesEEOneStateOrig.getSession() ); } ); } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/collection/dereferenced/VersionedCascadeDereferencedCollectionTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/collection/dereferenced/VersionedCascadeDereferencedCollectionTest.java index afd7c628d444..2bd4742cc590 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/collection/dereferenced/VersionedCascadeDereferencedCollectionTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/collection/dereferenced/VersionedCascadeDereferencedCollectionTest.java @@ -36,6 +36,7 @@ import org.hibernate.testing.orm.junit.SessionFactoryScope; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; @@ -76,12 +77,9 @@ public void testMergeNullCollection(SessionFactoryScope scope) { scope.inTransaction( session -> { - VersionedCascadeOne one = (VersionedCascadeOne) session.merge( versionedCascadeOne ); + VersionedCascadeOne one = session.merge( versionedCascadeOne ); - // after merging, one.getManies() should still be null; - // the EntityEntry loaded state should contain a PersistentCollection though. - - assertNull( one.getManies() ); + assertThat( one.getManies().size() ).isEqualTo( 0 ); EntityEntry eeOne = getEntityEntry( session, one ); AbstractPersistentCollection maniesEEOneStateOrig = (AbstractPersistentCollection) eeOne.getLoadedValue( "manies" ); @@ -109,28 +107,7 @@ public void testMergeNullCollection(SessionFactoryScope scope) { // Ensure the same EntityEntry is being used. assertSame( eeOne, getEntityEntry( session, one ) ); - // Ensure one.getManies() is still null. - assertNull( one.getManies() ); - - // Ensure CollectionEntry for maniesEEOneStateOrig is no longer in the PersistenceContext. - assertNull( getCollectionEntry( session, maniesEEOneStateOrig ) ); - - // Ensure the original CollectionEntry has role, persister, and key set to null. - assertNull( ceManiesOrig.getRole() ); - assertNull( ceManiesOrig.getLoadedPersister() ); - assertNull( ceManiesOrig.getKey() ); - - // Ensure the PersistentCollection (that was previously returned by eeOne.getLoadedState()) - // has key and role set to null. - assertNull( maniesEEOneStateOrig.getKey() ); - assertNull( maniesEEOneStateOrig.getRole() ); - - // Ensure eeOne.getLoadedState() returns null for collection after flush. - assertNull( eeOne.getLoadedValue( "manies" ) ); - - // Ensure the session in maniesEEOneStateOrig has been unset. - assertNull( maniesEEOneStateOrig.getSession() ); - + assertThat( one.getManies().size() ).isEqualTo( 0 ); } ); } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/collection/dereferenced/VersionedNoCascadeDereferencedCollectionTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/collection/dereferenced/VersionedNoCascadeDereferencedCollectionTest.java index 034974ba8e5d..aa9b45bcd999 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/collection/dereferenced/VersionedNoCascadeDereferencedCollectionTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/collection/dereferenced/VersionedNoCascadeDereferencedCollectionTest.java @@ -36,6 +36,7 @@ import org.hibernate.testing.orm.junit.SessionFactoryScope; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; @@ -76,12 +77,9 @@ public void testMergeNullCollection(SessionFactoryScope scope) { scope.inTransaction( session -> { - VersionedNoCascadeOne one = (VersionedNoCascadeOne) session.merge( versionedNoCascadeOne ); + VersionedNoCascadeOne one = session.merge( versionedNoCascadeOne ); - // after merging, one.getManies() should still be null; - // the EntityEntry loaded state should contain a PersistentCollection though. - - assertNull( one.getManies() ); + assertThat( one.getManies().size() ).isEqualTo( 0 ); EntityEntry eeOne = getEntityEntry( session, one ); AbstractPersistentCollection maniesEEOneStateOrig = (AbstractPersistentCollection) eeOne.getLoadedValue( "manies" ); @@ -109,27 +107,7 @@ public void testMergeNullCollection(SessionFactoryScope scope) { // Ensure the same EntityEntry is being used. assertSame( eeOne, getEntityEntry( session, one ) ); - // Ensure one.getManies() is still null. - assertNull( one.getManies() ); - - // Ensure CollectionEntry for maniesEEOneStateOrig is no longer in the PersistenceContext. - assertNull( getCollectionEntry( session, maniesEEOneStateOrig ) ); - - // Ensure the original CollectionEntry has role, persister, and key set to null. - assertNull( ceManiesOrig.getRole() ); - assertNull( ceManiesOrig.getLoadedPersister() ); - assertNull( ceManiesOrig.getKey() ); - - // Ensure the PersistentCollection (that was previously returned by eeOne.getLoadedState()) - // has key and role set to null. - assertNull( maniesEEOneStateOrig.getKey() ); - assertNull( maniesEEOneStateOrig.getRole() ); - - // Ensure eeOne.getLoadedState() returns null for collection after flush. - assertNull( eeOne.getLoadedValue( "manies" ) ); - - // Ensure the session in maniesEEOneStateOrig has been unset. - assertNull( maniesEEOneStateOrig.getSession() ); + assertThat( one.getManies().size() ).isEqualTo( 0 ); } ); } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/collection/multisession/MultipleSessionCollectionTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/collection/multisession/MultipleSessionCollectionTest.java index 0587129d6107..cdf6f14bb2a2 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/collection/multisession/MultipleSessionCollectionTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/collection/multisession/MultipleSessionCollectionTest.java @@ -37,12 +37,15 @@ import org.hibernate.HibernateException; import org.hibernate.collection.spi.AbstractPersistentCollection; import org.hibernate.collection.spi.PersistentCollection; +import org.hibernate.dialect.CockroachDialect; +import org.hibernate.dialect.HSQLDialect; import org.hibernate.engine.spi.CollectionEntry; import org.hibernate.testing.TestForIssue; import org.hibernate.testing.orm.junit.DomainModel; import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.SkipForDialect; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -284,6 +287,8 @@ public void testCopyPersistentCollectionReferenceBeforeFlush(SessionFactoryScope @Test @TestForIssue(jiraKey = "HHH-9518") + @SkipForDialect(dialectClass = HSQLDialect.class, reason = "The select triggered by the merge just hang without any exception") + @SkipForDialect(dialectClass = CockroachDialect.class, reason = "The merge in the second session causes a deadlock") public void testCopyPersistentCollectionReferenceAfterFlush(SessionFactoryScope scope) { Parent p = new Parent(); Child c = new Child(); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/columndiscriminator/Author.java b/hibernate-core/src/test/java/org/hibernate/orm/test/columndiscriminator/Author.java new file mode 100644 index 000000000000..a42591cad3e7 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/columndiscriminator/Author.java @@ -0,0 +1,45 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.columndiscriminator; + +import java.util.*; + +public class Author { + private Long id; + private String name; + private String email; + private List books = new ArrayList<>(); + + public Author(String name, String email) { + this.name = name; + this.email = email; + } + + protected Author() { + // default + } + + public Long id() { + return id; + } + + public String name() { + return name; + } + + public String email() { + return email; + } + + public List books() { + return books; + } + + public void addBook(Book book) { + books.add(book); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/columndiscriminator/Book.java b/hibernate-core/src/test/java/org/hibernate/orm/test/columndiscriminator/Book.java new file mode 100644 index 000000000000..ca16510368e3 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/columndiscriminator/Book.java @@ -0,0 +1,34 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.columndiscriminator; + +public class Book { + private Long id; + private String title; + private BookDetails details; + + public Book(String title, BookDetails details) { + this.title = title; + this.details = details; + } + + protected Book() { + // default + } + + public Long id() { + return id; + } + + public String title() { + return title; + } + + public BookDetails details() { + return details; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/columndiscriminator/BookDetails.java b/hibernate-core/src/test/java/org/hibernate/orm/test/columndiscriminator/BookDetails.java new file mode 100644 index 000000000000..77f8b389c71f --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/columndiscriminator/BookDetails.java @@ -0,0 +1,23 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.columndiscriminator; + +public abstract class BookDetails { + private String information; + + protected BookDetails(String information) { + this.information = information; + } + + protected BookDetails() { + // default + } + + public String information() { + return information; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/columndiscriminator/BoringBookDetails.java b/hibernate-core/src/test/java/org/hibernate/orm/test/columndiscriminator/BoringBookDetails.java new file mode 100644 index 000000000000..235dba94696b --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/columndiscriminator/BoringBookDetails.java @@ -0,0 +1,24 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.columndiscriminator; + +public class BoringBookDetails extends BookDetails { + private String boringInformation; + + public BoringBookDetails(String information, String boringInformation) { + super(information); + this.boringInformation = boringInformation; + } + + public BoringBookDetails() { + // default + } + + public String boringInformation() { + return boringInformation; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/columndiscriminator/ColumnDiscrimnatorWithSchemaTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/columndiscriminator/ColumnDiscrimnatorWithSchemaTest.java new file mode 100644 index 000000000000..d8f2d3384c2b --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/columndiscriminator/ColumnDiscrimnatorWithSchemaTest.java @@ -0,0 +1,47 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.columndiscriminator; + +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.cfg.SchemaToolingSettings; +import org.hibernate.testing.orm.junit.DialectFeatureChecks.SupportSchemaCreation; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.Test; + +@DomainModel(xmlMappings = "org/hibernate/orm/test/columndiscriminator/orm.xml") +@ServiceRegistry(settings = { + @Setting(name = AvailableSettings.DEFAULT_SCHEMA, value = "GREET"), + @Setting(name = SchemaToolingSettings.JAKARTA_HBM2DDL_CREATE_SCHEMAS, value = "true") +}) +@SessionFactory +@RequiresDialectFeature(feature = SupportSchemaCreation.class) +class ColumnDiscrimnatorWithSchemaTest { + + private ColumnDiscrimnatorWithSchemaTest() { + } + + static ColumnDiscrimnatorWithSchemaTest createColumnDiscrimnatorWithSchemaTest() { + return new ColumnDiscrimnatorWithSchemaTest(); + } + + @Test + void testIt(SessionFactoryScope scope) { + scope.inTransaction( entityManager -> { + var book = new Book( "The Art of Computer Programming", + new SpecialBookDetails( "Hardcover", "Computer Science" ) ); + + var author = new Author( "Donald Knuth", "dn@cs.com" ); + author.addBook( book ); + entityManager.persist( author ); + } ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/columndiscriminator/SpecialBookDetails.java b/hibernate-core/src/test/java/org/hibernate/orm/test/columndiscriminator/SpecialBookDetails.java new file mode 100644 index 000000000000..5cc837e63ebf --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/columndiscriminator/SpecialBookDetails.java @@ -0,0 +1,24 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.columndiscriminator; + +public class SpecialBookDetails extends BookDetails { + private String specialInformation; + + public SpecialBookDetails(String information, String specialInformation) { + super(information); + this.specialInformation = specialInformation; + } + + protected SpecialBookDetails() { + // default + } + + public String specialInformation() { + return specialInformation; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/component/StructComponentManyToOneCompositeTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/component/StructComponentManyToOneCompositeTest.java new file mode 100644 index 000000000000..e5e772a2879f --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/component/StructComponentManyToOneCompositeTest.java @@ -0,0 +1,175 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.component; + +import jakarta.persistence.Embeddable; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import org.hibernate.Hibernate; +import org.hibernate.annotations.Struct; +import org.hibernate.testing.jdbc.SharedDriverManagerTypeCacheClearingIntegrator; +import org.hibernate.testing.orm.junit.BootstrapServiceRegistry; +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@BootstrapServiceRegistry( + // Clear the type cache, otherwise we might run into ORA-21700: object does not exist or is marked for delete + integrators = SharedDriverManagerTypeCacheClearingIntegrator.class +) +@DomainModel( + annotatedClasses = { + StructComponentManyToOneCompositeTest.Book.class + } +) +@SessionFactory +@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsStructAggregate.class) +public class StructComponentManyToOneCompositeTest { + + @BeforeEach + public void setUp(SessionFactoryScope scope){ + scope.inTransaction( + session -> { + Book book1 = new Book(); + book1.id = 1L; + book1.id2 = 1L; + book1.title = "Hibernate 3"; + book1.author = new Author( "Gavin", null ); + + session.persist( book1 ); + + Book book2 = new Book(); + book2.id = 2L; + book2.id2 = 2L; + book2.title = "Hibernate 6"; + book2.author = new Author( "Steve", book1 ); + + session.persist( book2 ); + } + ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope){ + scope.inTransaction( + session -> + session.createQuery( "delete from Book" ).executeUpdate() + ); + } + + @Test + public void testGet(SessionFactoryScope scope){ + scope.inTransaction( + session -> { + Book book = session.createQuery( "from Book b where b.id = 2", Book.class ).getSingleResult(); + assertFalse( Hibernate.isInitialized( book.author.getFavoriteBook() ) ); + assertEquals( "Gavin", book.author.getFavoriteBook().getAuthor().getName() ); + } + ); + } + + @Test + public void testJoin(SessionFactoryScope scope){ + scope.inTransaction( + session -> { + Book book = session.createQuery( + "from Book b join fetch b.author.favoriteBook where b.id = 2", + Book.class + ).getSingleResult(); + assertTrue( Hibernate.isInitialized( book.author.getFavoriteBook() ) ); + assertEquals( "Gavin", book.author.getFavoriteBook().getAuthor().getName() ); + } + ); + } + + @Entity(name = "Book") + public static class Book { + @Id + private Long id; + @Id + private Long id2; + private String title; + private Author author; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getId2() { + return id2; + } + + public void setId2(Long id2) { + this.id2 = id2; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public Author getAuthor() { + return author; + } + + public void setAuthor(Author author) { + this.author = author; + } + } + + @Embeddable + @Struct( name = "author_type") + public static class Author { + + private String name; + @ManyToOne(fetch = FetchType.LAZY) + private Book favoriteBook; + + public Author() { + } + + public Author(String name, Book favoriteBook) { + this.name = name; + this.favoriteBook = favoriteBook; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Book getFavoriteBook() { + return favoriteBook; + } + + public void setFavoriteBook(Book favoriteBook) { + this.favoriteBook = favoriteBook; + } + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/dynamicmap/CustomEntityNameResolverTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/dynamicmap/CustomEntityNameResolverTest.java new file mode 100644 index 000000000000..744674c5d945 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/dynamicmap/CustomEntityNameResolverTest.java @@ -0,0 +1,58 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.dynamicmap; + +import java.util.HashMap; +import java.util.Map; + +import org.hibernate.EntityNameResolver; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.cfg.Configuration; +import org.hibernate.engine.spi.SessionFactoryImplementor; + +import org.hibernate.testing.orm.junit.Jira; +import org.junit.jupiter.api.Test; + +import static org.hibernate.testing.transaction.TransactionUtil2.inTransaction; +import static org.hibernate.tool.schema.Action.ACTION_CREATE_THEN_DROP; + +/** + * @author Marco Belladelli + */ +@Jira( "https://hibernate.atlassian.net/browse/HHH-18486" ) +public class CustomEntityNameResolverTest { + @Test + public void test() { + final Configuration configuration = new Configuration(); + configuration.setProperty( AvailableSettings.HBM2DDL_AUTO, ACTION_CREATE_THEN_DROP ); + configuration.setProperty( AvailableSettings.SHOW_SQL, Boolean.TRUE.toString() ); + configuration.setProperty( AvailableSettings.FORMAT_SQL, Boolean.TRUE.toString() ); + configuration.addResource( "org/hibernate/orm/test/dynamicmap/artist.hbm.xml" ); + configuration.addEntityNameResolver( new HibernateEntityNameResolver() ); + try (final SessionFactoryImplementor sf = (SessionFactoryImplementor) configuration.buildSessionFactory()) { + inTransaction( sf, session -> { + final Map artistEntity = new HashMap<>(); + artistEntity.put( "id", 1 ); + artistEntity.put( "firstname", "John" ); + artistEntity.put( "lastname", "Doe" ); + // Persist the dynamic map entity + session.persist( artistEntity ); + } ); + sf.getSchemaManager().truncateMappedObjects(); + } + } + + static class HibernateEntityNameResolver implements EntityNameResolver { + @Override + public String resolveEntityName(Object entity) { + if ( entity instanceof Map ) { + return "artist"; + } + return null; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/engine/spi/LoadQueryInfluencersTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/engine/spi/LoadQueryInfluencersTest.java new file mode 100644 index 000000000000..d5379c597b3e --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/engine/spi/LoadQueryInfluencersTest.java @@ -0,0 +1,159 @@ +package org.hibernate.orm.test.engine.spi; + +import java.util.Set; + +import org.hibernate.annotations.BatchSize; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.engine.spi.LoadQueryInfluencers; +import org.hibernate.metamodel.model.domain.NavigableRole; +import org.hibernate.persister.collection.CollectionPersister; +import org.hibernate.persister.entity.EntityPersister; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@DomainModel(annotatedClasses = { + LoadQueryInfluencersTest.EntityWithBatchSize1.class, + LoadQueryInfluencersTest.ChildEntity.class +}) +@SessionFactory +@ServiceRegistry( + settings = { + @Setting(name = AvailableSettings.DEFAULT_BATCH_FETCH_SIZE, value = "100") + } +) +public class LoadQueryInfluencersTest { + + @Test + public void usesCustomEntityBatchSizeForEffectivelyBatchLoadable(SessionFactoryScope scope) { + scope.inSession( + (nonTransactedSession) -> { + LoadQueryInfluencers influencers = new LoadQueryInfluencers( nonTransactedSession.getSessionFactory() ); + assertTrue( influencers.getBatchSize() > 1, "Expecting default batch size > 1" ); + + EntityPersister persister = nonTransactedSession.getSessionFactory() + .getRuntimeMetamodels() + .getMappingMetamodel() + .getEntityDescriptor( EntityWithBatchSize1.class ); + + assertFalse( persister.isBatchLoadable(), "Incorrect persister batch loadable." ); + assertEquals( 1, influencers.effectiveBatchSize( persister ), "Incorrect effective batch size." ); + assertFalse( + influencers.effectivelyBatchLoadable( persister ), + "Incorrect effective batch loadable." + ); + } + ); + } + + @Test + public void usesCustomCollectionBatchSizeForEffectivelyBatchLoadable(SessionFactoryScope scope) { + scope.inSession( + (nonTransactedSession) -> { + LoadQueryInfluencers influencers = new LoadQueryInfluencers( nonTransactedSession.getSessionFactory() ); + assertTrue( influencers.getBatchSize() > 1, "Expecting default batch size > 1" ); + + EntityPersister entityPersister = nonTransactedSession.getSessionFactory() + .getRuntimeMetamodels() + .getMappingMetamodel() + .getEntityDescriptor( EntityWithBatchSize1.class ); + + NavigableRole collectionRole = entityPersister.getNavigableRole() + .append( "childrenWithBatchSize1" ); + + CollectionPersister persister = nonTransactedSession.getSessionFactory() + .getRuntimeMetamodels() + .getMappingMetamodel() + .findCollectionDescriptor( collectionRole.getFullPath() ); + + assertFalse( persister.isBatchLoadable(), "Incorrect persister batch loadable." ); + assertEquals( 1, influencers.effectiveBatchSize( persister ), "Incorrect effective batch size." ); + assertFalse( + influencers.effectivelyBatchLoadable( persister ), + "Incorrect effective batch loadable." + ); + } + ); + } + + + @Entity + @Table(name = "EntityWithBatchSize1") + @BatchSize(size = 1) + public class EntityWithBatchSize1 { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + + @OneToMany + @BatchSize(size = 1) + private Set childrenWithBatchSize1; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Set getChildrenWithBatchSize1() { + return childrenWithBatchSize1; + } + + public void setChildrenWithBatchSize1(Set childrenWithBatchSize1) { + this.childrenWithBatchSize1 = childrenWithBatchSize1; + } + } + + @Entity + @Table(name = "ChildEntity") + public class ChildEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/EntityGraphAndJoinFetchTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/EntityGraphAndJoinFetchTest.java new file mode 100644 index 000000000000..3ab3e03204f8 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/EntityGraphAndJoinFetchTest.java @@ -0,0 +1,209 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.entitygraph; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.IdClass; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.NamedAttributeNode; +import jakarta.persistence.NamedEntityGraph; +import jakarta.persistence.NamedSubgraph; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import org.hibernate.Hibernate; +import org.hibernate.jpa.SpecHints; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@DomainModel( + annotatedClasses = { + EntityGraphAndJoinFetchTest.OrderItem.class, + EntityGraphAndJoinFetchTest.Order.class, + EntityGraphAndJoinFetchTest.Product.class + } +) +@SessionFactory +@JiraKey( value = "HHH-19246") +public class EntityGraphAndJoinFetchTest { + + private static final Long ORDER_ID = 1l; + private static final Long PRODUCT_ID = 2l; + + private static final String NAMED_GRAPH_NAME = "Order.fetchAll"; + private static final String NAMED_SUBGRAPH_NAME = "OrderItem.fetchAll"; + + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Order order = new Order( ORDER_ID, new BigDecimal( 1000 ) ); + Product product = new Product( PRODUCT_ID, "laptop" ); + OrderItem orderItem = new OrderItem( product, order ); + order.getItems().add( orderItem ); + + session.persist( order ); + session.persist( product ); + session.persist( orderItem ); + } + ); + } + + @Test + public void testJoinFetchBeingSubsetOfGraph(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Order order = session.createQuery( "FROM Order e LEFT JOIN FETCH e.items", Order.class ) + .setHint( + SpecHints.HINT_SPEC_LOAD_GRAPH, + scope.getSessionFactory().createEntityManager().getEntityGraph( NAMED_GRAPH_NAME ) + ) + .getSingleResult(); + assertTrue( Hibernate.isInitialized( order.getItems() ), "OrderItems have not been fetched" ); + assertEquals( 1, order.getItems().size(), "OrderItems have not been fetched" ); + assertTrue( Hibernate.isInitialized( order.getItems().iterator().next().getProduct() ), "Product has not been fetched" ); + } + ); + } + + @Entity(name = "OrderItem") + @IdClass(OrderItem.PK.class) + public static class OrderItem { + + @Id + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "product_id") + private Product product; + + @Id + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "order_id") + private Order order; + + public OrderItem() { + } + + public OrderItem(Product product, Order order) { + this.product = product; + this.order = order; + } + + public Product getProduct() { + return product; + } + + public Order getOrder() { + return order; + } + + public static class PK implements Serializable { + private Long product; + private Long order; + + @Override + public boolean equals(Object o) { + if ( this == o ) { + return true; + } + if ( o == null || getClass() != o.getClass() ) { + return false; + } + PK pk = (PK) o; + return Objects.equals( product, pk.product ) && Objects.equals( order, pk.order ); + } + + @Override + public int hashCode() { + return Objects.hash( product, order ); + } + } + } + + + @Entity(name = "Order") + @Table(name = "ORDER_TABLE") + @NamedEntityGraph( + name = NAMED_GRAPH_NAME, + attributeNodes = { + @NamedAttributeNode(value = "items", subgraph = NAMED_SUBGRAPH_NAME) + }, + subgraphs = { + @NamedSubgraph(name = NAMED_SUBGRAPH_NAME, attributeNodes = {@NamedAttributeNode("product")}) + } + ) + public static class Order { + + @Id + private Long id; + + private BigDecimal total; + @OneToMany(mappedBy = "order") + private Set items = new HashSet<>(); + + public Order() { + } + + public Order(Long id, BigDecimal total) { + this.id = id; + this.total = total; + } + + public Long getId() { + return id; + } + + public BigDecimal getTotal() { + return total; + } + + public Set getItems() { + return items; + } + } + + @Entity(name = "Product") + public static class Product { + + @Id + private Long id; + + private String name; + + public Product() { + } + + public Product(Long id, String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/FetchGraphCollectionOrderByAndCriteriaJoinTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/FetchGraphCollectionOrderByAndCriteriaJoinTest.java new file mode 100644 index 000000000000..cf69cbaf2e64 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/FetchGraphCollectionOrderByAndCriteriaJoinTest.java @@ -0,0 +1,331 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.entitygraph; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.NamedAttributeNode; +import jakarta.persistence.NamedEntityGraph; +import jakarta.persistence.NamedEntityGraphs; +import jakarta.persistence.NamedSubgraph; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OrderBy; +import jakarta.persistence.criteria.JoinType; +import jakarta.persistence.criteria.Predicate; +import org.hibernate.Hibernate; +import org.hibernate.graph.spi.RootGraphImplementor; +import org.hibernate.query.Query; +import org.hibernate.query.criteria.HibernateCriteriaBuilder; +import org.hibernate.query.criteria.JpaCriteriaQuery; +import org.hibernate.query.criteria.JpaJoin; +import org.hibernate.query.criteria.JpaRoot; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Random; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author baranyit@gmail.com + */ +@DomainModel(annotatedClasses = { + FetchGraphCollectionOrderByAndCriteriaJoinTest.Level1.class, + FetchGraphCollectionOrderByAndCriteriaJoinTest.Level2.class, + FetchGraphCollectionOrderByAndCriteriaJoinTest.Level3.class, +}) +@SessionFactory +@Jira( "https://hibernate.atlassian.net/browse/HHH-19207" ) +public class FetchGraphCollectionOrderByAndCriteriaJoinTest { + + @Test + public void testJoinAndFilter(SessionFactoryScope scope) { + executeTest( scope, true, true ); + } + + @Test + public void testNotJoinAndNotFilter(SessionFactoryScope scope) { + executeTest( scope, false, false ); + } + + /** + * This case describes the problem of using a fetch graph with a collection that has an @OrderBy clause + * and a criteria join without any usage. + *

    + * This test case is expected to fail because the @OrderBy is not applied to the collection in the + * generated SQL query. + *

    + * The issue can also be solved by optimizing the criteria definition like in the test case + * testJoinAndFilter or testNotJoinAndNotFilter, but there are some program code + * structures where it is not possible to do it, or makes the source code more complex and less readable. + *

    + * The required and the logical behaviour should be that the @OrderBy clause is applied to the + * collection as in the other test cases. If this problem occurs very difficult to find out the reason because + * this behaviour is not documented and the source code looks correct. + */ + @Test + public void testJoinAndNotFilter(SessionFactoryScope scope) { + executeTest( scope, true, false ); + } + + + private void executeTest(SessionFactoryScope scope, boolean directJoin, boolean filterOnJoin) { + scope.inTransaction( session -> { + HibernateCriteriaBuilder builder = session.getCriteriaBuilder(); + JpaCriteriaQuery criteriaQuery = builder.createQuery( Level1.class ); + JpaRoot root = criteriaQuery.from( Level1.class ); + + List predicates = new ArrayList<>(); + predicates.add( + builder.equal( root.get( "id" ), 1L ) + ); + + if ( directJoin || filterOnJoin ) { + // Directly add the join to the level2 and level3 entities + JpaJoin join = root.join( "children", JoinType.INNER ) + .join( "children", JoinType.LEFT ); + + if ( filterOnJoin ) { + predicates.add( + builder.gt( join.get( "id" ), 1L ) + ); + } + } + + // Add all defined predicates to the criteria query + criteriaQuery.where( builder.and( predicates.toArray(new Predicate[0]) ) ); + + // Set some default root ordering (not required for the test case) + criteriaQuery.orderBy( builder.asc( root.get( "id" ) ) ); + + // Create the TypedQuery with entity graph + RootGraphImplementor graph = session.getEntityGraph( "level1_loadAll" ); + Query query = session + .createQuery( criteriaQuery ) + .setHint( org.hibernate.jpa.SpecHints.HINT_SPEC_FETCH_GRAPH, graph ); + + // Parse the result as stream, but the problem occurs also with getResultList() + query.getResultStream().forEach( level1 -> { + + // Check ordering of Level2 entities + Long ordinalLevel2 = 0L; + assertThat( level1.getChildren() ).matches( Hibernate::isInitialized ); + for ( Level2 level2 : level1.getChildren() ) { + System.out.println( "Level2: " + level2.getOrdinal() ); + assertThat( level2.getOrdinal() ).isGreaterThan( ordinalLevel2 ); + ordinalLevel2 = level2.getOrdinal(); + + // Check ordering of Level3 entities + Long ordinalLevel3 = 0L; + assertThat( level2.getChildren() ).matches( Hibernate::isInitialized ); + for ( Level3 level3 : level2.getChildren() ) { + System.out.println( "Level3: " + level3.getOrdinal() ); + assertThat( level3.getOrdinal() ).isGreaterThan( ordinalLevel3 ); + ordinalLevel3 = level3.getOrdinal(); + } + } + } ); + } ); + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + + final Iterator randomOrdinals = new Random().longs( 100, 999 ) + .distinct().limit( 200 ).boxed().iterator(); + + for ( long l1 = 1; l1 <= 5; l1++ ) { + final Level1 root = new Level1( l1 ); + + for ( long l2 = 1; l2 <= 5; l2++ ) { + final long l2Id = (l1 * 10) + l2; + final Level2 child2 = new Level2( root, l2Id, randomOrdinals.next() ); + + for ( long l3 = 1; l3 <= 5; l3++ ) { + final long l3Id = (l2Id * 10) + l3; + new Level3( child2, l3Id, randomOrdinals.next() ); + } + } + session.persist( root ); + } + } ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Entity(name = "Level1") + @NamedEntityGraphs({ + @NamedEntityGraph( + name = "level1_loadAll", + attributeNodes = { + @NamedAttributeNode(value = "children", subgraph = "subgraph.children") + }, + subgraphs = { + @NamedSubgraph( + name = "subgraph.children", + attributeNodes = { + @NamedAttributeNode(value = "children") + } + ) + } + ) + }) + static class Level1 { + @Id + private Long id; + + @OneToMany(fetch = FetchType.LAZY, + mappedBy = "parent", + cascade = CascadeType.PERSIST) + @OrderBy("ordinal") + private Set children = new LinkedHashSet<>(); + + public Level1() { + } + + public Level1(Long id) { + this.id = id; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Set getChildren() { + return children; + } + + @Override + public String toString() { + return "Level1 #" + id; + } + } + + @Entity(name = "Level2") + static class Level2 { + @Id + Long id; + + Long ordinal; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "parent_id") + private Level1 parent; + + @OneToMany(fetch = FetchType.LAZY, + mappedBy = "parent", + cascade = CascadeType.PERSIST) + @OrderBy("ordinal") + private Set children = new LinkedHashSet<>(); + + public Level2() { + } + + public Level2(Level1 parent, Long id, Long ordinal) { + this.parent = parent; + this.id = id; + this.ordinal = ordinal; + parent.getChildren().add( this ); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Level1 getParent() { + return parent; + } + + public void setParent(Level1 parent) { + this.parent = parent; + } + + public Set getChildren() { + return children; + } + + public Long getOrdinal() { + return ordinal; + } + + @Override + public String toString() { + return "Level1 #" + id + " $" + ordinal; + } + } + + @Entity(name = "Level3") + static class Level3 { + @Id + Long id; + + Long ordinal; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "parent_id") + private Level2 parent; + + public Level3() { + } + + public Level3(Level2 parent, Long id, Long ordinal) { + this.parent = parent; + this.id = id; + this.ordinal = ordinal; + parent.getChildren().add( this ); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Level2 getParent() { + return parent; + } + + public void setParent(Level2 parent) { + this.parent = parent; + } + + public Long getOrdinal() { + return ordinal; + } + + @Override + public String toString() { + return "Level3 #" + id + " $" + ordinal; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/FindGraphCollectionOrderByTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/FindGraphCollectionOrderByTest.java new file mode 100644 index 000000000000..837f365223fd --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/FindGraphCollectionOrderByTest.java @@ -0,0 +1,235 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.entitygraph; + +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import org.hibernate.Hibernate; +import org.hibernate.graph.spi.RootGraphImplementor; +import org.hibernate.jpa.AvailableHints; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.NamedAttributeNode; +import jakarta.persistence.NamedEntityGraph; +import jakarta.persistence.NamedEntityGraphs; +import jakarta.persistence.NamedSubgraph; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OrderBy; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Marco Belladelli + */ +@DomainModel( annotatedClasses = { + FindGraphCollectionOrderByTest.Level1.class, + FindGraphCollectionOrderByTest.Level2.class, + FindGraphCollectionOrderByTest.Level3.class, +} ) +@SessionFactory +@Jira( "https://hibernate.atlassian.net/browse/HHH-18436" ) +public class FindGraphCollectionOrderByTest { + @Test + public void testLoadGraphFind(SessionFactoryScope scope) { + executeTest( scope, AvailableHints.HINT_SPEC_LOAD_GRAPH, true ); + } + + @Test + public void testLoadGraphQuery(SessionFactoryScope scope) { + executeTest( scope, AvailableHints.HINT_SPEC_LOAD_GRAPH, false ); + } + + @Test + public void testFetchGraphFind(SessionFactoryScope scope) { + executeTest( scope, AvailableHints.HINT_SPEC_FETCH_GRAPH, true ); + } + + @Test + public void testFetchGraphQuery(SessionFactoryScope scope) { + executeTest( scope, AvailableHints.HINT_SPEC_FETCH_GRAPH, false ); + } + + + private void executeTest(SessionFactoryScope scope, String hint, boolean find) { + scope.inTransaction( session -> { + final RootGraphImplementor graph = session.getEntityGraph( "level1_loadAll" ); + final Level1 root = find ? session.find( Level1.class, 1L, Map.of( hint, graph ) ) : + session.createQuery( "from Level1 where id = :id", Level1.class ) + .setParameter( "id", 1L ) + .setHint( hint, graph ) + .getSingleResult(); + + assertThat( root.getChildren() ).matches( Hibernate::isInitialized ).hasSize( 3 ); + long i = 1; + for ( final Level2 child : root.getChildren() ) { + if ( i == 2 ) { + assertThat( child.getChildren() ).matches( Hibernate::isInitialized ).hasSize( 1 ); + } + assertThat( child.getId() ).as( "Children not in expected order" ).isEqualTo( i++ ); + } + } ); + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final Level1 root = new Level1( 1L ); + new Level2( root, 1L ); + final Level2 child2 = new Level2( root, 2L ); + new Level2( root, 3L ); + new Level3( child2, 1L ); + session.persist( root ); + } ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Entity( name = "Level1" ) + @NamedEntityGraphs( { + @NamedEntityGraph( + name = "level1_loadAll", + attributeNodes = { + @NamedAttributeNode( value = "children", subgraph = "subgraph.children" ) + }, + subgraphs = { + @NamedSubgraph( + name = "subgraph.children", + attributeNodes = { + @NamedAttributeNode( value = "children" ) + } + ) + } + ) + } ) + static class Level1 { + @Id + private Long id; + + @OneToMany( fetch = FetchType.LAZY, + mappedBy = "parent", + cascade = CascadeType.PERSIST ) + @OrderBy( "id" ) + private Set children = new LinkedHashSet<>(); + + public Level1() { + } + + public Level1(Long id) { + this.id = id; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Set getChildren() { + return children; + } + } + + @Entity( name = "Level2" ) + static class Level2 { + @Id + Long id; + + @ManyToOne( fetch = FetchType.LAZY ) + @JoinColumn( name = "parent_id" ) + private Level1 parent; + + @OneToMany( fetch = FetchType.LAZY, + mappedBy = "parent", + cascade = CascadeType.PERSIST ) + @OrderBy( "id" ) + private Set children = new LinkedHashSet<>(); + + public Level2() { + } + + public Level2(Level1 parent, Long id) { + this.parent = parent; + this.id = id; + parent.getChildren().add( this ); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Level1 getParent() { + return parent; + } + + public void setParent(Level1 parent) { + this.parent = parent; + } + + public Set getChildren() { + return children; + } + } + + @Entity( name = "Level3" ) + static class Level3 { + @Id + Long id; + + @ManyToOne( fetch = FetchType.LAZY ) + @JoinColumn( name = "parent_id" ) + private Level2 parent; + + public Level3() { + } + + public Level3(Level2 parent, Long id) { + this.parent = parent; + this.id = id; + parent.getChildren().add( this ); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Level2 getParent() { + return parent; + } + + public void setParent(Level2 parent) { + this.parent = parent; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/filter/FilterDefinitionOrderTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/filter/FilterDefinitionOrderTest.java new file mode 100644 index 000000000000..9a3b2631fae9 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/filter/FilterDefinitionOrderTest.java @@ -0,0 +1,130 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.filter; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import org.hibernate.SharedSessionContract; +import org.hibernate.annotations.Filter; +import org.hibernate.annotations.FilterDef; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel( + annotatedClasses = { + FilterDefinitionOrderTest.AMyEntity.class, + FilterDefinitionOrderTest.XEntity.class + } +) +@JiraKey("HHH-19036") +// Real test is if it actually starts, as that's where the filter binding happens, +// but let's also check that it was actually processed as well: +public class FilterDefinitionOrderTest extends AbstractStatefulStatelessFilterTest { + + @BeforeEach + void setUp() { + scope.inTransaction(session -> { + AMyEntity entity1 = new AMyEntity(); + entity1.setField("test"); + AMyEntity entity2 = new AMyEntity(); + entity2.setField("Hello"); + session.persist(entity1); + session.persist(entity2); + }); + } + + @AfterEach + void tearDown() { + scope.inTransaction(session -> { + session.createMutationQuery("delete from AMyEntity").executeUpdate(); + }); + } + + @ParameterizedTest + @MethodSource("transactionKind") + void smokeTest(BiConsumer> inTransaction) { + inTransaction.accept(scope, session -> { + session.enableFilter("x_filter"); + List entities = session.createQuery("FROM AMyEntity", AMyEntity.class).getResultList(); + assertThat(entities).hasSize(1) + .allSatisfy(entity -> assertThat(entity.getField()).isEqualTo("Hello")); + + session.disableFilter("x_filter"); + entities = session.createQuery("FROM AMyEntity", AMyEntity.class).getResultList(); + assertThat(entities).hasSize(2); + }); + } + + @Entity(name = "AMyEntity") + @Filter(name = "x_filter") + public static class AMyEntity { + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + @Column(nullable = false) + private Long id; + + public String field; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getField() { + return field; + } + + public void setField(String field) { + this.field = field; + } + } + + @Entity(name = "XEntity") + @FilterDef(name = "x_filter", defaultCondition = "field = 'Hello'") + public static class XEntity { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + @Column(nullable = false) + private Long id; + + public String field; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getField() { + return field; + } + + public void setField(String field) { + this.field = field; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayConstructorInSelectClauseTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayConstructorInSelectClauseTest.java new file mode 100644 index 000000000000..1c36651739d7 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayConstructorInSelectClauseTest.java @@ -0,0 +1,144 @@ +package org.hibernate.orm.test.function.array; + +import java.util.Arrays; + +import org.hibernate.dialect.OracleDialect; + +import org.hibernate.testing.jdbc.SharedDriverManagerTypeCacheClearingIntegrator; +import org.hibernate.testing.orm.junit.BootstrapServiceRegistry; +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@DomainModel( + annotatedClasses = { + ArrayConstructorInSelectClauseTest.Book.class, + ArrayConstructorInSelectClauseTest.Author.class + }) +@SessionFactory +@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsStructuralArrays.class) +// Clear the type cache, otherwise we might run into ORA-21700: object does not exist or is marked for delete +@BootstrapServiceRegistry(integrators = SharedDriverManagerTypeCacheClearingIntegrator.class) +public class ArrayConstructorInSelectClauseTest { + private static final Long AUTHOR_ID = 1l; + private static final Long CO_AUTHOR_ID = 2l; + private static final Long BOOK_ID = 3l; + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + var dialect = session.getDialect(); + if ( dialect instanceof OracleDialect ) { + session.createNativeQuery( + "create or replace type LongArray as varying array(127) of number(19,0)" ) + .executeUpdate(); + } + var author = new Author( AUTHOR_ID, "D Thomas" ); + var coAuthor = new Author( CO_AUTHOR_ID, "A Hunt" ); + session.persist( author ); + session.persist( coAuthor ); + + var book = new Book( BOOK_ID, "The Pragmatic Programmer", author, coAuthor ); + session.persist( book ); + } + ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.createMutationQuery( "delete from Book" ).executeUpdate(); + session.createMutationQuery( "delete from Author" ).executeUpdate(); + var dialect = session.getDialect(); + if ( dialect instanceof OracleDialect ) { + session.createNativeQuery( "drop type LongArray" ).executeUpdate(); + } + } + ); + } + + @Test + @Jira("HHH-18353") + public void testArraySelectionNoCast(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + var query = session.createQuery( + "SELECT ARRAY(b.author.id, b.coAuthor.id) FROM Book b WHERE b.id = :bookId", + Long[].class + ).setParameter( "bookId", BOOK_ID ); + + assertThat( query.getSingleResult() ).containsAll( Arrays.asList( AUTHOR_ID, CO_AUTHOR_ID ) ); + } + ); + } + + @Test + public void testArraySelectionCast(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + var query = session.createQuery( + "SELECT ARRAY(CAST(b.author.id as Long), CAST(b.coAuthor.id as Long)) FROM Book b WHERE b.id = :bookId", + Long[].class + ).setParameter( "bookId", BOOK_ID ); + + assertThat( query.getSingleResult() ).containsAll( Arrays.asList( AUTHOR_ID, CO_AUTHOR_ID ) ); + } + ); + } + + @Entity(name = "Author") + public static class Author { + + @Id + private Long id; + + private String name; + + public Author() { + } + + public Author(Long id, String name) { + this.id = id; + this.name = name; + } + } + + @Entity(name = "Book") + public static class Book { + + @Id + private Long id; + + private String title; + + @ManyToOne + private Author author; + + @ManyToOne + private Author coAuthor; + + public Book() { + } + + public Book(long id, String title, Author author, Author coAuthor) { + this.id = id; + this.title = title; + this.author = author; + this.coAuthor = coAuthor; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayContainsTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayContainsTest.java index 2167a8ed5ad7..9bfdf9680d72 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayContainsTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayContainsTest.java @@ -17,6 +17,7 @@ import org.hibernate.testing.orm.junit.BootstrapServiceRegistry; import org.hibernate.testing.orm.junit.DialectFeatureChecks; import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; import org.hibernate.testing.orm.junit.RequiresDialectFeature; import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactoryScope; @@ -157,4 +158,22 @@ public void testInSyntax(SessionFactoryScope scope) { } ); } + @Test + @JiraKey( "HHH-18851" ) + public void testInArray(SessionFactoryScope scope) { + scope.inSession( em -> { + List results = em.createQuery( + "select e.id " + + "from EntityWithArrays e " + + "where :p in e.theArray", + Tuple.class + ) + .setParameter( "p", "abc" ) + .getResultList(); + + assertEquals( 1, results.size() ); + assertEquals( 2L, results.get( 0 ).get( 0 ) ); + } ); + } + } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/hql/ASTParserLoadingTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/hql/ASTParserLoadingTest.java index 62e8bb02c126..2ac157068bf9 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/hql/ASTParserLoadingTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/hql/ASTParserLoadingTest.java @@ -26,12 +26,13 @@ import org.hibernate.Session; import org.hibernate.Transaction; import org.hibernate.TypeMismatchException; -import org.hibernate.cfg.Configuration; import org.hibernate.cfg.Environment; +import org.hibernate.community.dialect.InformixDialect; import org.hibernate.dialect.AbstractHANADialect; import org.hibernate.dialect.CockroachDialect; import org.hibernate.dialect.DB2Dialect; import org.hibernate.dialect.DerbyDialect; +import org.hibernate.dialect.Dialect; import org.hibernate.dialect.H2Dialect; import org.hibernate.dialect.HSQLDialect; import org.hibernate.dialect.MySQLDialect; @@ -49,7 +50,6 @@ import org.hibernate.query.Query; import org.hibernate.query.SyntaxException; import org.hibernate.query.spi.QueryImplementor; -import org.hibernate.query.sqm.ParsingException; import org.hibernate.query.sqm.SqmExpressible; import org.hibernate.query.sqm.internal.QuerySqmImpl; import org.hibernate.query.sqm.tree.domain.SqmPath; @@ -60,13 +60,17 @@ import org.hibernate.transform.ResultTransformer; import org.hibernate.transform.Transformers; -import org.hibernate.testing.DialectChecks; -import org.hibernate.testing.FailureExpected; -import org.hibernate.testing.RequiresDialect; -import org.hibernate.testing.RequiresDialectFeature; -import org.hibernate.testing.SkipForDialect; -import org.hibernate.testing.TestForIssue; -import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase; +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.FailureExpected; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.RequiresDialect; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.hibernate.testing.orm.junit.SkipForDialect; import org.hibernate.orm.test.cid.Customer; import org.hibernate.orm.test.cid.LineItem; @@ -74,8 +78,10 @@ import org.hibernate.orm.test.cid.Order; import org.hibernate.orm.test.cid.Product; -import org.junit.After; -import org.junit.Test; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import org.jboss.logging.Logger; import org.hamcrest.CoreMatchers; @@ -86,7 +92,6 @@ import static org.hamcrest.Matchers.notNullValue; import static org.hibernate.testing.junit4.ExtraAssertions.assertClassAssignability; import static org.hibernate.testing.junit4.ExtraAssertions.assertTyping; -import static org.hibernate.testing.transaction.TransactionUtil.doInHibernate; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -106,15 +111,40 @@ * * @author Steve */ -@RequiresDialectFeature(DialectChecks.SupportsTemporaryTable.class) -public class ASTParserLoadingTest extends BaseCoreFunctionalTestCase { +@RequiresDialectFeature( feature = DialectFeatureChecks.SupportsTemporaryTable.class) +@DomainModel( annotatedClasses = {Department.class, Employee.class, Title.class}, + xmlMappings = {"/org/hibernate/orm/test/hql/Animal.hbm.xml", + "/org/hibernate/orm/test/hql/FooBarCopy.hbm.xml", + "/org/hibernate/orm/test/hql/SimpleEntityWithAssociation.hbm.xml", + "/org/hibernate/orm/test/hql/CrazyIdFieldNames.hbm.xml", + "/org/hibernate/orm/test/hql/Image.hbm.xml", + "/org/hibernate/orm/test/hql/ComponentContainer.hbm.xml", + "/org/hibernate/orm/test/hql/VariousKeywordPropertyEntity.hbm.xml", + "/org/hibernate/orm/test/hql/Constructor.hbm.xml", + "/org/hibernate/orm/test/batchfetch/ProductLine.hbm.xml", + "/org/hibernate/orm/test/cid/Customer.hbm.xml", + "/org/hibernate/orm/test/cid/Order.hbm.xml", + "/org/hibernate/orm/test/cid/LineItem.hbm.xml", + "/org/hibernate/orm/test/cid/Product.hbm.xml", + "/org/hibernate/orm/test/any/hbm/Properties.hbm.xml", + "/org/hibernate/orm/test/legacy/Commento.hbm.xml", + "/org/hibernate/orm/test/legacy/Marelo.hbm.xml"}) +@SessionFactory +@ServiceRegistry( + settings = { + @Setting(name = Environment.USE_QUERY_CACHE, value = "true"), + @Setting(name = Environment.GENERATE_STATISTICS, value = "true") + } +) +@SuppressWarnings("JUnitMalformedDeclaration") +public class ASTParserLoadingTest { + private static final Logger log = Logger.getLogger(ASTParserLoadingTest.class); private final List createdAnimalIds = new ArrayList<>(); - @After - public void cleanUpTestData() { - inTransaction( - (session) -> { + @AfterEach + public void cleanUpTestData(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { session.createQuery( "from Animal" ).list().forEach( (animal) -> session.delete( animal ) ); @@ -193,51 +223,10 @@ public void cleanUpTestData() { } ); } - @Override - protected boolean isCleanupTestDataRequired() { - return false; - } - - @Override - public String[] getMappings() { - return new String[] { - "/org/hibernate/orm/test/hql/Animal.hbm.xml", - "/org/hibernate/orm/test/hql/FooBarCopy.hbm.xml", - "/org/hibernate/orm/test/hql/SimpleEntityWithAssociation.hbm.xml", - "/org/hibernate/orm/test/hql/CrazyIdFieldNames.hbm.xml", - "/org/hibernate/orm/test/hql/Image.hbm.xml", - "/org/hibernate/orm/test/hql/ComponentContainer.hbm.xml", - "/org/hibernate/orm/test/hql/VariousKeywordPropertyEntity.hbm.xml", - "/org/hibernate/orm/test/hql/Constructor.hbm.xml", - "/org/hibernate/orm/test/batchfetch/ProductLine.hbm.xml", - "/org/hibernate/orm/test/cid/Customer.hbm.xml", - "/org/hibernate/orm/test/cid/Order.hbm.xml", - "/org/hibernate/orm/test/cid/LineItem.hbm.xml", - "/org/hibernate/orm/test/cid/Product.hbm.xml", - "/org/hibernate/orm/test/any/hbm/Properties.hbm.xml", - "/org/hibernate/orm/test/legacy/Commento.hbm.xml", - "/org/hibernate/orm/test/legacy/Marelo.hbm.xml" - }; - } - @Override - protected Class[] getAnnotatedClasses() { - return new Class[] { - Department.class, - Employee.class, - Title.class - }; - } - - @Override - public void configure(Configuration cfg) { - super.configure( cfg ); - cfg.setProperty( Environment.USE_QUERY_CACHE, true ); - cfg.setProperty( Environment.GENERATE_STATISTICS, true ); - } @Test - public void testSubSelectAsArithmeticOperand() { - inTransaction( + public void testSubSelectAsArithmeticOperand(SessionFactoryScope scope) { + scope.inTransaction( (s) -> { s.createQuery( "from Zoo z where ( select count(*) from Zoo ) = 0" ).list(); @@ -252,8 +241,8 @@ public void testSubSelectAsArithmeticOperand() { } @Test - @TestForIssue( jiraKey = "HHH-8432" ) - public void testExpandListParameter() { + @JiraKey( "HHH-8432" ) + public void testExpandListParameter(SessionFactoryScope scope) { final Object[] namesArray = new Object[] { "ZOO 1", "ZOO 2", "ZOO 3", "ZOO 4", "ZOO 5", "ZOO 6", "ZOO 7", "ZOO 8", "ZOO 9", "ZOO 10", "ZOO 11", "ZOO 12" @@ -263,7 +252,7 @@ public void testExpandListParameter() { "City 8", "City 9", "City 10", "City 11", "City 12" }; - inTransaction( + scope.inTransaction( (session) -> { Address address = new Address(); Zoo zoo = new Zoo( "ZOO 1", address ); @@ -272,7 +261,7 @@ public void testExpandListParameter() { } ); - inTransaction( + scope.inTransaction( (session) -> { List result = session.createQuery( "FROM Zoo z WHERE z.name IN (?1) and z.address.city IN (?2)" ) .setParameterList( 1, namesArray ) @@ -284,9 +273,9 @@ public void testExpandListParameter() { } @Test - @TestForIssue(jiraKey = "HHH-8699") - public void testBooleanPredicate() { - final Constructor created = fromTransaction( + @JiraKey( "HHH-8699") + public void testBooleanPredicate(SessionFactoryScope scope) { + final Constructor created = scope.fromTransaction( (session) -> { final Constructor constructor = new Constructor(); session.save( constructor ); @@ -296,7 +285,7 @@ public void testBooleanPredicate() { Constructor.resetConstructorExecutionCount(); - inTransaction( + scope.inTransaction( (session) -> { final String qry = "select new Constructor( c.id, c.id is not null, c.id = c.id, c.id + 1, concat( str(c.id), 'foo' ) ) from Constructor c where c.id = :id"; final Constructor result = session.createQuery(qry, Constructor.class).setParameter( "id", created.getId() ).uniqueResult(); @@ -314,8 +303,8 @@ public void testBooleanPredicate() { } @Test - public void testJpaTypeOperator() { - inTransaction( + public void testJpaTypeOperator(SessionFactoryScope scope) { + scope.inTransaction( session -> { // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // where clause @@ -352,8 +341,8 @@ public void testJpaTypeOperator() { } @Test - public void testComponentJoins() { - inTransaction( + public void testComponentJoins(SessionFactoryScope scope) { + scope.inTransaction( (s) -> { ComponentContainer root = new ComponentContainer( new ComponentContainer.Address( @@ -367,7 +356,7 @@ public void testComponentJoins() { } ); - inTransaction( + scope.inTransaction( (s) -> { List result = s.createQuery( "select a from ComponentContainer c join c.address a" ).list(); assertEquals( 1, result.size() ); @@ -389,9 +378,9 @@ public void testComponentJoins() { } @Test - @TestForIssue( jiraKey = "HHH-9642") - public void testLazyAssociationInComponent() { - inTransaction( + @JiraKey( "HHH-9642") + public void testLazyAssociationInComponent(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { Address address = new Address(); Zoo zoo = new Zoo( "ZOO 1", address ); @@ -404,7 +393,7 @@ public void testLazyAssociationInComponent() { } ); - inTransaction( + scope.inTransaction( (session) -> { final Zoo zoo = (Zoo) session.createQuery( "from Zoo z" ).uniqueResult(); assertNotNull( zoo ); @@ -417,7 +406,7 @@ public void testLazyAssociationInComponent() { ); - inTransaction( + scope.inTransaction( (session) -> { final Zoo zoo = (Zoo) session.createQuery( "from Zoo z join fetch z.address.stateProvince" ).uniqueResult(); assertNotNull( zoo ); @@ -428,7 +417,7 @@ public void testLazyAssociationInComponent() { } ); - inTransaction( + scope.inTransaction( (session) -> { final Zoo zoo = (Zoo) session.createQuery( "from Zoo z join fetch z.address a join fetch a.stateProvince" ).uniqueResult(); assertNotNull( zoo ); @@ -441,9 +430,9 @@ public void testLazyAssociationInComponent() { } @Test - public void testJPAQLQualifiedIdentificationVariablesControl() { + public void testJPAQLQualifiedIdentificationVariablesControl(SessionFactoryScope scope) { // just checking syntax here... - Session s = openSession(); + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); s.createQuery( "from VariousKeywordPropertyEntity where type = 'something'" ).list(); s.createQuery( "from VariousKeywordPropertyEntity where value = 'something'" ).list(); @@ -460,8 +449,8 @@ public void testJPAQLQualifiedIdentificationVariablesControl() { @Test @SuppressWarnings( {"unchecked"}) - public void testJPAQLMapKeyQualifier() { - Session s = openSession(); + public void testJPAQLMapKeyQualifier(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); Human me = new Human(); me.setName( new Name( "Steve", null, "Ebersole" ) ); @@ -477,7 +466,7 @@ public void testJPAQLMapKeyQualifier() { // in SELECT clause { // hibernate-only form - s = openSession(); + s = scope.getSessionFactory().openSession(); s.beginTransaction(); List results = s.createQuery( "select distinct key(h.family) from Human h" ).list(); assertEquals( 1, results.size() ); @@ -489,7 +478,7 @@ public void testJPAQLMapKeyQualifier() { { // jpa form - s = openSession(); + s = scope.getSessionFactory().openSession(); s.beginTransaction(); List results = s.createQuery( "select distinct KEY(f) from Human h join h.family f" ).list(); assertEquals( 1, results.size() ); @@ -502,7 +491,7 @@ public void testJPAQLMapKeyQualifier() { // in WHERE clause { // hibernate-only form - s = openSession(); + s = scope.getSessionFactory().openSession(); s.beginTransaction(); Long count = (Long) s.createQuery( "select count(*) from Human h where KEY(h.family) = 'son'" ).uniqueResult(); assertEquals( (Long)1L, count ); @@ -512,7 +501,7 @@ public void testJPAQLMapKeyQualifier() { { // jpa form - s = openSession(); + s = scope.getSessionFactory().openSession(); s.beginTransaction(); Long count = (Long) s.createQuery( "select count(*) from Human h join h.family f where key(f) = 'son'" ).uniqueResult(); assertEquals( (Long)1L, count ); @@ -520,7 +509,7 @@ public void testJPAQLMapKeyQualifier() { s.close(); } - s = openSession(); + s = scope.getSessionFactory().openSession(); s.beginTransaction(); s.delete( me ); s.delete( joe ); @@ -530,8 +519,8 @@ public void testJPAQLMapKeyQualifier() { @Test @SuppressWarnings( {"unchecked"}) - public void testJPAQLMapEntryQualifier() { - Session s = openSession(); + public void testJPAQLMapEntryQualifier(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); Human me = new Human(); me.setName( new Name( "Steve", null, "Ebersole" ) ); @@ -547,7 +536,7 @@ public void testJPAQLMapEntryQualifier() { // in SELECT clause { // hibernate-only form - s = openSession(); + s = scope.getSessionFactory().openSession(); s.beginTransaction(); List results = s.createQuery( "select entry(h.family) from Human h" ).list(); assertEquals( 1, results.size() ); @@ -562,7 +551,7 @@ public void testJPAQLMapEntryQualifier() { { // jpa form - s = openSession(); + s = scope.getSessionFactory().openSession(); s.beginTransaction(); List results = s.createQuery( "select ENTRY(f) from Human h join h.family f" ).list(); assertEquals( 1, results.size() ); @@ -578,7 +567,7 @@ public void testJPAQLMapEntryQualifier() { // not exactly sure of the syntax of ENTRY in the WHERE clause... - s = openSession(); + s = scope.getSessionFactory().openSession(); s.beginTransaction(); s.delete( me ); s.delete( joe ); @@ -588,8 +577,8 @@ public void testJPAQLMapEntryQualifier() { @Test @SuppressWarnings( {"unchecked"}) - public void testJPAQLMapValueQualifier() { - Session s = openSession(); + public void testJPAQLMapValueQualifier(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); Human me = new Human(); me.setName( new Name( "Steve", null, "Ebersole" ) ); @@ -605,7 +594,7 @@ public void testJPAQLMapValueQualifier() { // in SELECT clause { // hibernate-only form - s = openSession(); + s = scope.getSessionFactory().openSession(); s.beginTransaction(); List results = s.createQuery( "select value(h.family) from Human h" ).list(); assertEquals( 1, results.size() ); @@ -617,7 +606,7 @@ public void testJPAQLMapValueQualifier() { { // jpa form - s = openSession(); + s = scope.getSessionFactory().openSession(); s.beginTransaction(); List results = s.createQuery( "select VALUE(f) from Human h join h.family f" ).list(); assertEquals( 1, results.size() ); @@ -630,7 +619,7 @@ public void testJPAQLMapValueQualifier() { // in WHERE clause { // hibernate-only form - s = openSession(); + s = scope.getSessionFactory().openSession(); s.beginTransaction(); Long count = (Long) s.createQuery( "select count(*) from Human h where VALUE(h.family) = :joe" ).setParameter( "joe", joe ).uniqueResult(); // ACTUALLY EXACTLY THE SAME AS: @@ -642,7 +631,7 @@ public void testJPAQLMapValueQualifier() { { // jpa form - s = openSession(); + s = scope.getSessionFactory().openSession(); s.beginTransaction(); Long count = (Long) s.createQuery( "select count(*) from Human h join h.family f where value(f) = :joe" ).setParameter( "joe", joe ).uniqueResult(); // ACTUALLY EXACTLY THE SAME AS: @@ -652,7 +641,7 @@ public void testJPAQLMapValueQualifier() { s.close(); } - s = openSession(); + s = scope.getSessionFactory().openSession(); s.beginTransaction(); s.delete( me ); s.delete( joe ); @@ -661,9 +650,9 @@ public void testJPAQLMapValueQualifier() { } @Test - @RequiresDialectFeature( DialectChecks.SupportsSubqueryInSelect.class ) - public void testPaginationWithPolymorphicQuery() { - Session s = openSession(); + @RequiresDialectFeature( feature = DialectFeatureChecks.SupportsSubqueryInSelect.class ) + public void testPaginationWithPolymorphicQuery(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); Human h = new Human(); h.setName( new Name( "Steve", null, "Ebersole" ) ); @@ -671,14 +660,14 @@ public void testPaginationWithPolymorphicQuery() { s.getTransaction().commit(); s.close(); - s = openSession(); + s = scope.getSessionFactory().openSession(); s.beginTransaction(); List results = s.createQuery( "from java.lang.Object" ).setMaxResults( 2 ).list(); assertEquals( 1, results.size() ); s.getTransaction().commit(); s.close(); - s = openSession(); + s = scope.getSessionFactory().openSession(); s.beginTransaction(); s.delete( h ); s.getTransaction().commit(); @@ -686,10 +675,10 @@ public void testPaginationWithPolymorphicQuery() { } @Test - @TestForIssue( jiraKey = "HHH-2045" ) + @JiraKey( "HHH-2045" ) @RequiresDialect( H2Dialect.class ) - public void testEmptyInList() { - Session session = openSession(); + public void testEmptyInList(SessionFactoryScope scope) { + Session session = scope.getSessionFactory().openSession(); session.beginTransaction(); Human human = new Human(); human.setName( new Name( "Lukasz", null, "Antoniak" ) ); @@ -698,14 +687,14 @@ public void testEmptyInList() { session.getTransaction().commit(); session.close(); - session = openSession(); + session = scope.getSessionFactory().openSession(); session.beginTransaction(); List results = session.createQuery( "from Human h where h.nickName in ()" ).list(); assertEquals( 0, results.size() ); session.getTransaction().commit(); session.close(); - session = openSession(); + session = scope.getSessionFactory().openSession(); session.beginTransaction(); session.delete( human ); session.getTransaction().commit(); @@ -713,9 +702,9 @@ public void testEmptyInList() { } @Test - @TestForIssue( jiraKey = "HHH-8901" ) - public void testEmptyInListForDialectsNotSupportsEmptyInList() { - Session session = openSession(); + @JiraKey( "HHH-8901" ) + public void testEmptyInListForDialectsNotSupportsEmptyInList(SessionFactoryScope scope) { + Session session = scope.getSessionFactory().openSession(); session.beginTransaction(); Human human = new Human(); human.setName( new Name( "Lukasz", null, "Antoniak" ) ); @@ -724,7 +713,7 @@ public void testEmptyInListForDialectsNotSupportsEmptyInList() { session.getTransaction().commit(); session.close(); - session = openSession(); + session = scope.getSessionFactory().openSession(); session.beginTransaction(); List results = session.createQuery( "from Human h where h.nickName in (:nickNames)" ) .setParameter( "nickNames", Collections.emptySet() ) @@ -733,7 +722,7 @@ public void testEmptyInListForDialectsNotSupportsEmptyInList() { session.getTransaction().commit(); session.close(); - session = openSession(); + session = scope.getSessionFactory().openSession(); session.beginTransaction(); session.delete( human ); session.getTransaction().commit(); @@ -741,9 +730,9 @@ public void testEmptyInListForDialectsNotSupportsEmptyInList() { } @Test - @TestForIssue( jiraKey = "HHH-2851") - public void testMultipleRefsToSameParam() { - Session s = openSession(); + @JiraKey( "HHH-2851") + public void testMultipleRefsToSameParam(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); Human h = new Human(); h.setName( new Name( "Johnny", 'B', "Goode" ) ); @@ -764,7 +753,7 @@ public void testMultipleRefsToSameParam() { s.getTransaction().commit(); s.close(); - s = openSession(); + s = scope.getSessionFactory().openSession(); s.beginTransaction(); List results = s.createQuery( "from Human where name.first = :name or name.last=:name" ) .setParameter( "name", "Johnny" ) @@ -813,7 +802,7 @@ public void testMultipleRefsToSameParam() { s.getTransaction().commit(); s.close(); - s = openSession(); + s = scope.getSessionFactory().openSession(); s.beginTransaction(); s.createQuery( "delete Human" ).executeUpdate(); s.getTransaction().commit(); @@ -821,8 +810,8 @@ public void testMultipleRefsToSameParam() { } @Test - public void testComponentNullnessChecks() { - Session s = openSession(); + public void testComponentNullnessChecks(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); Human h = new Human(); h.setName( new Name( "Johnny", 'B', "Goode" ) ); @@ -839,18 +828,19 @@ public void testComponentNullnessChecks() { s.getTransaction().commit(); s.close(); - s = openSession(); + s = scope.getSessionFactory().openSession(); s.beginTransaction(); List results = s.createQuery( "from Human where name is null" ).list(); assertEquals( 1, results.size() ); results = s.createQuery( "from Human where name is not null" ).list(); assertEquals( 3, results.size() ); + Dialect dialect = scope.getSessionFactory().getJdbcServices().getDialect(); String query = - ( getDialect() instanceof DB2Dialect || getDialect() instanceof HSQLDialect ) ? + ( dialect instanceof DB2Dialect || dialect instanceof HSQLDialect ) ? "from Human where cast(?1 as string) is null" : "from Human where ?1 is null" ; - if ( getDialect() instanceof DerbyDialect ) { + if ( dialect instanceof DerbyDialect ) { s.createQuery( query ).setParameter( 1, "null" ).list(); } else { @@ -860,7 +850,7 @@ public void testComponentNullnessChecks() { s.getTransaction().commit(); s.close(); - s = openSession(); + s = scope.getSessionFactory().openSession(); s.beginTransaction(); s.createQuery( "delete Human" ).executeUpdate(); s.getTransaction().commit(); @@ -868,9 +858,9 @@ public void testComponentNullnessChecks() { } @Test - @TestForIssue( jiraKey = "HHH-4150" ) - public void testSelectClauseCaseWithSum() { - Session s = openSession(); + @JiraKey( "HHH-4150" ) + public void testSelectClauseCaseWithSum(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); Transaction t = s.beginTransaction(); Human h1 = new Human(); @@ -900,9 +890,10 @@ public void testSelectClauseCaseWithSum() { } @Test - @TestForIssue( jiraKey = "HHH-4150" ) - public void testSelectClauseCaseWithCountDistinct() { - Session s = openSession(); + @JiraKey( "HHH-4150" ) + @SkipForDialect( dialectClass = InformixDialect.class, majorVersion = 11, minorVersion = 70, reason = "Informix does not support case with count distinct") + public void testSelectClauseCaseWithCountDistinct(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); Transaction t = s.beginTransaction(); Human h1 = new Human(); @@ -937,12 +928,12 @@ public void testSelectClauseCaseWithCountDistinct() { } @Test - public void testInvalidCollectionDereferencesFail() { + public void testInvalidCollectionDereferencesFail(SessionFactoryScope scope) { - try ( final SessionImplementor s = (SessionImplementor) openSession() ) { + try ( final SessionImplementor s = (SessionImplementor) scope.getSessionFactory().openSession() ) { // control group... - inTransaction( + scope.inTransaction( s, session -> { s.createQuery( "from Animal a join a.offspring o where o.description = 'xyz'" ).list(); @@ -952,7 +943,7 @@ public void testInvalidCollectionDereferencesFail() { } ); - inTransaction( + scope.inTransaction( s, session -> { try { @@ -968,7 +959,7 @@ public void testInvalidCollectionDereferencesFail() { } ); - inTransaction( + scope.inTransaction( s, session -> { try { @@ -984,7 +975,7 @@ public void testInvalidCollectionDereferencesFail() { } ); - inTransaction( + scope.inTransaction( s, session -> { try { @@ -1000,7 +991,7 @@ public void testInvalidCollectionDereferencesFail() { } ); - inTransaction( + scope.inTransaction( s, session -> { try { @@ -1019,9 +1010,9 @@ public void testInvalidCollectionDereferencesFail() { } @Test - public void testConcatenation() { + public void testConcatenation(SessionFactoryScope scope) { // simple syntax checking... - Session s = openSession(); + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); s.createQuery( "from Human h where h.nickName = '1' || 'ov' || 'tha' || 'few'" ).list(); s.getTransaction().commit(); @@ -1029,13 +1020,14 @@ public void testConcatenation() { } @Test - @SkipForDialect(value = CockroachDialect.class, comment = "https://github.com/cockroachdb/cockroach/issues/41943") - public void testExpressionWithParamInFunction() { - Session s = openSession(); + @SkipForDialect(dialectClass = CockroachDialect.class, matchSubTypes = true ,reason = "https://github.com/cockroachdb/cockroach/issues/41943") + public void testExpressionWithParamInFunction(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); s.createQuery( "from Animal a where abs(a.bodyWeight-:param) < 2.0" ).setParameter( "param", 1 ).list(); s.createQuery( "from Animal a where abs(:param - a.bodyWeight) < 2.0" ).setParameter( "param", 1 ).list(); - if ( getDialect() instanceof HSQLDialect || getDialect() instanceof DB2Dialect || getDialect() instanceof DerbyDialect ) { + Dialect dialect = scope.getSessionFactory().getJdbcServices().getDialect(); + if ( dialect instanceof HSQLDialect || dialect instanceof DB2Dialect || dialect instanceof DerbyDialect ) { // HSQLDB and DB2 don't like the abs(? - ?) syntax. bit work if at least one parameter is typed... s.createQuery( "from Animal where abs(cast(:x as long) - :y) < 2.0" ).setParameter( "x", 1 ).setParameter( "y", 1 ).list(); s.createQuery( "from Animal where abs(:x - cast(:y as long)) < 2.0" ).setParameter( "x", 1 ).setParameter( "y", 1 ).list(); @@ -1045,7 +1037,7 @@ public void testExpressionWithParamInFunction() { s.createQuery( "from Animal where abs(:x - :y) < 2.0" ).setParameter( "x", 1 ).setParameter( "y", 1 ).list(); } - if ( getDialect() instanceof DB2Dialect ) { + if ( dialect instanceof DB2Dialect ) { s.createQuery( "from Animal where lower(upper(cast(:foo as string))) like 'f%'" ).setParameter( "foo", "foo" ).list(); } else { @@ -1054,17 +1046,17 @@ public void testExpressionWithParamInFunction() { s.createQuery( "from Animal a where abs(abs(a.bodyWeight - 1.0 + :param) * abs(length('ffobar')-3)) = 3.0" ).setParameter( "param", 1 ).list(); - if ( getDialect() instanceof DB2Dialect ) { + if ( dialect instanceof DB2Dialect ) { s.createQuery( "from Animal where lower(upper('foo') || upper(cast(:bar as string))) like 'f%'" ).setParameter( "bar", "xyz" ).list(); } else { s.createQuery( "from Animal where lower(upper('foo') || upper(:bar)) like 'f%'" ).setParameter( "bar", "xyz" ).list(); } - if ( getDialect() instanceof AbstractHANADialect ) { + if ( dialect instanceof AbstractHANADialect ) { s.createQuery( "from Animal where abs(cast(1 as double) - cast(:param as double)) = 1.0" ).setParameter( "param", 1 ).list(); } - else if ( !( getDialect() instanceof PostgreSQLDialect || getDialect() instanceof MySQLDialect ) ) { + else if ( !( dialect instanceof PostgreSQLDialect || dialect instanceof MySQLDialect ) ) { s.createQuery( "from Animal where abs(cast(1 as float) - cast(:param as float)) = 1.0" ).setParameter( "param", 1 ).list(); } @@ -1073,12 +1065,12 @@ else if ( !( getDialect() instanceof PostgreSQLDialect || getDialect() instanceo } @Test - public void testCrazyIdFieldNames() { + public void testCrazyIdFieldNames(SessionFactoryScope scope) { MoreCrazyIdFieldNameStuffEntity top = new MoreCrazyIdFieldNameStuffEntity( "top" ); HeresAnotherCrazyIdFieldName next = new HeresAnotherCrazyIdFieldName( "next" ); top.setHeresAnotherCrazyIdFieldName( next ); MoreCrazyIdFieldNameStuffEntity other = new MoreCrazyIdFieldNameStuffEntity( "other" ); - Session s = openSession(); + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); s.save( next ); s.save( top ); @@ -1108,15 +1100,15 @@ public void testCrazyIdFieldNames() { } @Test - @TestForIssue( jiraKey = "HHH-2257" ) - public void testImplicitJoinsInDifferentClauses() { + @JiraKey( "HHH-2257" ) + public void testImplicitJoinsInDifferentClauses(SessionFactoryScope scope) { // both the classic and ast translators output the same syntactically valid sql // for all of these cases; the issue is that shallow (iterate) and // non-shallow (list/scroll) queries return different results because the // shallow skips the inner join which "weeds out" results from the non-shallow queries. // The results were initially different depending upon the clause(s) in which the // implicit join occurred - Session s = openSession(); + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); SimpleEntityWithAssociation owner = new SimpleEntityWithAssociation( "owner" ); SimpleAssociatedEntity e1 = new SimpleAssociatedEntity( "thing one", owner ); @@ -1127,19 +1119,19 @@ public void testImplicitJoinsInDifferentClauses() { s.getTransaction().commit(); s.close(); - checkCounts( "select e.owner from SimpleAssociatedEntity e", 1, "implicit-join in select clause" ); - checkCounts( "select e.id, e.owner from SimpleAssociatedEntity e", 1, "implicit-join in select clause" ); + checkCounts( scope, "select e.owner from SimpleAssociatedEntity e", 1, "implicit-join in select clause" ); + checkCounts( scope, "select e.id, e.owner from SimpleAssociatedEntity e", 1, "implicit-join in select clause" ); // resolved to a "id short cut" when part of the order by clause -> no inner join = no weeding out... - checkCounts( "from SimpleAssociatedEntity e order by e.owner", 2, "implicit-join in order-by clause" ); + checkCounts( scope, "from SimpleAssociatedEntity e order by e.owner", 2, "implicit-join in order-by clause" ); // resolved to a "id short cut" when part of the group by clause -> no inner join = no weeding out... - checkCounts( + checkCounts( scope, "select e.owner.id, count(*) from SimpleAssociatedEntity e group by e.owner", 2, "implicit-join in select and group-by clauses" ); - s = openSession(); + s = scope.getSessionFactory().openSession(); s.beginTransaction(); s.delete( e1 ); s.delete( e2 ); @@ -1149,8 +1141,8 @@ public void testImplicitJoinsInDifferentClauses() { } @Test - public void testRowValueConstructorSyntaxInInList() { - Session s = openSession(); + public void testRowValueConstructorSyntaxInInList(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); Product product = new Product(); product.setDescription( "My Product" ); @@ -1197,8 +1189,8 @@ public void testRowValueConstructorSyntaxInInList() { } - private void checkCounts(String hql, int expected, String testCondition) { - inTransaction( + private void checkCounts(SessionFactoryScope scope, String hql, int expected, String testCondition) { + scope.inTransaction( session -> { int count = determineCount( session.createQuery( hql ).list().iterator() ); assertEquals( "list() [" + testCondition + "]", expected, count ); @@ -1207,13 +1199,13 @@ private void checkCounts(String hql, int expected, String testCondition) { } @Test - @TestForIssue( jiraKey = "HHH-2257" ) - public void testImplicitSelectEntityAssociationInShallowQuery() { + @JiraKey( "HHH-2257" ) + public void testImplicitSelectEntityAssociationInShallowQuery(SessionFactoryScope scope) { // both the classic and ast translators output the same syntactically valid sql. // the issue is that shallow and non-shallow queries return different // results because the shallow skips the inner join which "weeds out" results // from the non-shallow queries... - Session s = openSession(); + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); SimpleEntityWithAssociation owner = new SimpleEntityWithAssociation( "owner" ); SimpleAssociatedEntity e1 = new SimpleAssociatedEntity( "thing one", owner ); @@ -1224,7 +1216,7 @@ public void testImplicitSelectEntityAssociationInShallowQuery() { s.getTransaction().commit(); s.close(); - s = openSession(); + s = scope.getSessionFactory().openSession(); s.beginTransaction(); int count = determineCount( s.createQuery( "select e.id, e.owner from SimpleAssociatedEntity e" ).list().iterator() ); // thing two would be removed from the result due to the inner join @@ -1232,7 +1224,7 @@ public void testImplicitSelectEntityAssociationInShallowQuery() { s.getTransaction().commit(); s.close(); - s = openSession(); + s = scope.getSessionFactory().openSession(); s.beginTransaction(); s.delete( e1 ); s.delete( e2 ); @@ -1250,29 +1242,29 @@ private int determineCount(Iterator iterator) { return count; } - @Test - @TestForIssue( jiraKey = "HHH-6714" ) - public void testUnaryMinus(){ - Session s = openSession(); - s.beginTransaction(); - Human stliu = new Human(); - stliu.setIntValue( 26 ); + @Test + @JiraKey( "HHH-6714" ) + public void testUnaryMinus(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); + s.beginTransaction(); + Human stliu = new Human(); + stliu.setIntValue( 26 ); - s.persist( stliu ); - s.getTransaction().commit(); - s.clear(); - s.beginTransaction(); - List list =s.createQuery( "from Human h where -(h.intValue - 100)=74" ).list(); - assertEquals( 1, list.size() ); - s.getTransaction().commit(); - s.close(); + s.persist( stliu ); + s.getTransaction().commit(); + s.clear(); + s.beginTransaction(); + List list = s.createQuery( "from Human h where -(h.intValue - 100)=74" ).list(); + assertEquals( 1, list.size() ); + s.getTransaction().commit(); + s.close(); - } + } @Test - public void testEntityAndOneToOneReturnedByQuery() { - Session s = openSession(); + public void testEntityAndOneToOneReturnedByQuery(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); Human h = new Human(); h.setName( new Name( "Gail", null, "Badner" ) ); @@ -1284,7 +1276,7 @@ public void testEntityAndOneToOneReturnedByQuery() { s.getTransaction().commit(); s.close(); - s = openSession(); + s = scope.getSessionFactory().openSession(); s.beginTransaction(); Object [] result = ( Object [] ) s.createQuery( "from User u, Human h where u.human = h" ).uniqueResult(); assertNotNull( result ); @@ -1298,8 +1290,8 @@ public void testEntityAndOneToOneReturnedByQuery() { } @Test - @TestForIssue( jiraKey = "HHH-9305") - public void testExplicitToOneInnerJoin() { + @JiraKey( "HHH-9305") + public void testExplicitToOneInnerJoin(SessionFactoryScope scope) { final Employee employee1 = new Employee(); employee1.setFirstName( "Jane" ); employee1.setLastName( "Doe" ); @@ -1317,7 +1309,7 @@ public void testExplicitToOneInnerJoin() { title2.setDescription( "John's title" ); employee2.setTitle( title2 ); - Session s = openSession(); + Session s = scope.getSessionFactory().openSession(); s.getTransaction().begin(); s.persist( title1 ); s.persist( dept1 ); @@ -1327,7 +1319,7 @@ public void testExplicitToOneInnerJoin() { s.getTransaction().commit(); s.close(); - s = openSession(); + s = scope.getSessionFactory().openSession(); s.getTransaction().begin(); Department department = (Department) s.createQuery( "select e.department from Employee e inner join e.department" ).uniqueResult(); assertEquals( employee1.getDepartment().getDeptName(), department.getDeptName() ); @@ -1341,7 +1333,7 @@ public void testExplicitToOneInnerJoin() { } @Test - public void testExplicitToOneOuterJoin() { + public void testExplicitToOneOuterJoin(SessionFactoryScope scope) { final Employee employee1 = new Employee(); employee1.setFirstName( "Jane" ); employee1.setLastName( "Doe" ); @@ -1359,7 +1351,7 @@ public void testExplicitToOneOuterJoin() { title2.setDescription( "John's title" ); employee2.setTitle( title2 ); - Session s = openSession(); + Session s = scope.getSessionFactory().openSession(); s.getTransaction().begin(); s.persist( title1 ); s.persist( dept1 ); @@ -1368,7 +1360,7 @@ public void testExplicitToOneOuterJoin() { s.persist( employee2 ); s.getTransaction().commit(); s.close(); - s = openSession(); + s = scope.getSessionFactory().openSession(); s.getTransaction().begin(); List list = s.createQuery( "select e.department from Employee e left join e.department" ).list(); assertEquals( 2, list.size() ); @@ -1391,7 +1383,7 @@ public void testExplicitToOneOuterJoin() { } @Test - public void testExplicitToOneInnerJoinAndImplicitToOne() { + public void testExplicitToOneInnerJoinAndImplicitToOne(SessionFactoryScope scope) { final Employee employee1 = new Employee(); employee1.setFirstName( "Jane" ); employee1.setLastName( "Doe" ); @@ -1409,7 +1401,7 @@ public void testExplicitToOneInnerJoinAndImplicitToOne() { title2.setDescription( "John's title" ); employee2.setTitle( title2 ); - Session s = openSession(); + Session s = scope.getSessionFactory().openSession(); s.getTransaction().begin(); s.persist( title1 ); s.persist( dept1 ); @@ -1418,7 +1410,7 @@ public void testExplicitToOneInnerJoinAndImplicitToOne() { s.persist( employee2 ); s.getTransaction().commit(); s.close(); - s = openSession(); + s = scope.getSessionFactory().openSession(); s.getTransaction().begin(); Object[] result = (Object[]) s.createQuery( "select e.firstName, e.lastName, e.title.description, e.department from Employee e inner join e.department" @@ -1437,7 +1429,7 @@ public void testExplicitToOneInnerJoinAndImplicitToOne() { } @Test - public void testNestedComponentIsNull() { + public void testNestedComponentIsNull(SessionFactoryScope scope) { // (1) From MapTest originally... // (2) Was then moved into HQLTest... // (3) However, a bug fix to EntityType#getIdentifierOrUniqueKeyType (HHH-2138) @@ -1447,57 +1439,57 @@ public void testNestedComponentIsNull() { // // fyi... found and fixed the problem in the classic parser; still // leaving here for syntax checking - new SyntaxChecker( "from Commento c where c.marelo.commento.mcompr is null" ).checkAll(); + new SyntaxChecker( scope, "from Commento c where c.marelo.commento.mcompr is null" ).checkAll(); } @Test - @TestForIssue( jiraKey = "HHH-939" ) - public void testSpecialClassPropertyReference() { + @JiraKey( "HHH-939" ) + public void testSpecialClassPropertyReference(SessionFactoryScope scope) { // this is a long standing bug in Hibernate when applied to joined-subclasses; // see HHH-939 for details and history - new SyntaxChecker( "from Zoo zoo where zoo.class = PettingZoo" ).checkAll(); - new SyntaxChecker( "select a.description from Animal a where a.class = Mammal" ).checkAll(); - new SyntaxChecker( "select a.class from Animal a" ).checkAll(); - new SyntaxChecker( "from DomesticAnimal an where an.class = Dog" ).checkAll(); - new SyntaxChecker( "from Animal an where an.class = Dog" ).checkAll(); + new SyntaxChecker( scope, "from Zoo zoo where zoo.class = PettingZoo" ).checkAll(); + new SyntaxChecker( scope, "select a.description from Animal a where a.class = Mammal" ).checkAll(); + new SyntaxChecker( scope, "select a.class from Animal a" ).checkAll(); + new SyntaxChecker( scope, "from DomesticAnimal an where an.class = Dog" ).checkAll(); + new SyntaxChecker( scope, "from Animal an where an.class = Dog" ).checkAll(); } @Test - @TestForIssue( jiraKey = "HHH-2376" ) - public void testSpecialClassPropertyReferenceFQN() { - new SyntaxChecker( "from Zoo zoo where zoo.class = org.hibernate.orm.test.hql.PettingZoo" ).checkAll(); - new SyntaxChecker( "select a.description from Animal a where a.class = org.hibernate.orm.test.hql.Mammal" ).checkAll(); - new SyntaxChecker( "from DomesticAnimal an where an.class = org.hibernate.orm.test.hql.Dog" ).checkAll(); - new SyntaxChecker( "from Animal an where an.class = org.hibernate.orm.test.hql.Dog" ).checkAll(); + @JiraKey( "HHH-2376" ) + public void testSpecialClassPropertyReferenceFQN(SessionFactoryScope scope) { + new SyntaxChecker( scope, "from Zoo zoo where zoo.class = org.hibernate.orm.test.hql.PettingZoo" ).checkAll(); + new SyntaxChecker( scope, "select a.description from Animal a where a.class = org.hibernate.orm.test.hql.Mammal" ).checkAll(); + new SyntaxChecker( scope, "from DomesticAnimal an where an.class = org.hibernate.orm.test.hql.Dog" ).checkAll(); + new SyntaxChecker( scope, "from Animal an where an.class = org.hibernate.orm.test.hql.Dog" ).checkAll(); } @Test - @TestForIssue( jiraKey = "HHH-1631" ) - public void testSubclassOrSuperclassPropertyReferenceInJoinedSubclass() { + @JiraKey( "HHH-1631" ) + public void testSubclassOrSuperclassPropertyReferenceInJoinedSubclass(SessionFactoryScope scope) { // this is a long standing bug in Hibernate; see HHH-1631 for details and history // // (1) pregnant is defined as a property of the class (Mammal) itself // (2) description is defined as a property of the superclass (Animal) // (3) name is defined as a property of a particular subclass (Human) - new SyntaxChecker( "from Zoo z join z.mammals as m where m.name.first = 'John'" ).checkAll(); + new SyntaxChecker( scope, "from Zoo z join z.mammals as m where m.name.first = 'John'" ).checkAll(); - new SyntaxChecker( "from Zoo z join z.mammals as m where m.pregnant = false" ).checkAll(); - new SyntaxChecker( "select m.pregnant from Zoo z join z.mammals as m where m.pregnant = false" ).checkAll(); + new SyntaxChecker( scope, "from Zoo z join z.mammals as m where m.pregnant = false" ).checkAll(); + new SyntaxChecker( scope, "select m.pregnant from Zoo z join z.mammals as m where m.pregnant = false" ).checkAll(); - new SyntaxChecker( "from Zoo z join z.mammals as m where m.description = 'tabby'" ).checkAll(); - new SyntaxChecker( "select m.description from Zoo z join z.mammals as m where m.description = 'tabby'" ).checkAll(); + new SyntaxChecker( scope, "from Zoo z join z.mammals as m where m.description = 'tabby'" ).checkAll(); + new SyntaxChecker( scope, "select m.description from Zoo z join z.mammals as m where m.description = 'tabby'" ).checkAll(); - new SyntaxChecker( "from Zoo z join z.mammals as m where m.name.first = 'John'" ).checkAll(); - new SyntaxChecker( "select m.name from Zoo z join z.mammals as m where m.name.first = 'John'" ).checkAll(); + new SyntaxChecker( scope, "from Zoo z join z.mammals as m where m.name.first = 'John'" ).checkAll(); + new SyntaxChecker( scope, "select m.name from Zoo z join z.mammals as m where m.name.first = 'John'" ).checkAll(); - new SyntaxChecker( "select m.pregnant from Zoo z join z.mammals as m" ).checkAll(); - new SyntaxChecker( "select m.description from Zoo z join z.mammals as m" ).checkAll(); - new SyntaxChecker( "select m.name from Zoo z join z.mammals as m" ).checkAll(); + new SyntaxChecker( scope, "select m.pregnant from Zoo z join z.mammals as m" ).checkAll(); + new SyntaxChecker( scope, "select m.description from Zoo z join z.mammals as m" ).checkAll(); + new SyntaxChecker( scope, "select m.name from Zoo z join z.mammals as m" ).checkAll(); - new SyntaxChecker( "from DomesticAnimal da join da.owner as o where o.nickName = 'Gavin'" ).checkAll(); - new SyntaxChecker( "select da.father from DomesticAnimal da join da.owner as o where o.nickName = 'Gavin'" ).checkAll(); - new SyntaxChecker( "select da.father from DomesticAnimal da where da.owner.nickName = 'Gavin'" ).checkAll(); + new SyntaxChecker( scope, "from DomesticAnimal da join da.owner as o where o.nickName = 'Gavin'" ).checkAll(); + new SyntaxChecker( scope, "select da.father from DomesticAnimal da join da.owner as o where o.nickName = 'Gavin'" ).checkAll(); + new SyntaxChecker( scope, "select da.father from DomesticAnimal da where da.owner.nickName = 'Gavin'" ).checkAll(); } /** @@ -1506,19 +1498,19 @@ public void testSubclassOrSuperclassPropertyReferenceInJoinedSubclass() { * keyword. */ @Test - public void testExplicitEntityCasting() { - new SyntaxChecker( "from Zoo z join treat(z.mammals as Human) as m where m.name.first = 'John'" ).checkAll(); - new SyntaxChecker( "from Zoo z join z.mammals as m where treat(m as Human).name.first = 'John'" ).checkAll(); + public void testExplicitEntityCasting(SessionFactoryScope scope) { + new SyntaxChecker( scope, "from Zoo z join treat(z.mammals as Human) as m where m.name.first = 'John'" ).checkAll(); + new SyntaxChecker( scope, "from Zoo z join z.mammals as m where treat(m as Human).name.first = 'John'" ).checkAll(); } @Test @RequiresDialectFeature( - value = DialectChecks.SupportLimitAndOffsetCheck.class, + feature = DialectFeatureChecks.SupportLimitAndOffsetCheck.class, comment = "dialect does not support offset and limit combo" ) - public void testSimpleSelectWithLimitAndOffset() throws Exception { + public void testSimpleSelectWithLimitAndOffset(SessionFactoryScope scope) throws Exception { // just checking correctness of param binding code... - Session session = openSession(); + Session session = scope.getSessionFactory().openSession(); Transaction t = session.beginTransaction(); session.createQuery( "from Animal" ) .setFirstResult( 2 ) @@ -1529,8 +1521,8 @@ public void testSimpleSelectWithLimitAndOffset() throws Exception { } @Test - public void testJPAPositionalParameterList() { - Session s = openSession(); + public void testJPAPositionalParameterList(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); ArrayList params = new ArrayList(); params.add( "Doe" ); @@ -1572,8 +1564,8 @@ public void testJPAPositionalParameterList() { } @Test - public void testComponentQueries() { - inTransaction( + public void testComponentQueries(SessionFactoryScope scope) { + scope.inTransaction( session -> { final QueryImplementor query = session.createQuery( "select h.name from Human h" ); final SqmSelectStatement sqmStatement = (SqmSelectStatement) query.unwrap( QuerySqmImpl.class ).getSqmStatement(); @@ -1602,10 +1594,10 @@ public void testComponentQueries() { } @Test - @TestForIssue( jiraKey = "HHH-1774" ) - @RequiresDialectFeature( DialectChecks.SupportsSubqueryInSelect.class ) - public void testComponentParameterBinding() { - Session s = openSession(); + @JiraKey( "HHH-1774" ) + @RequiresDialectFeature( feature = DialectFeatureChecks.SupportsSubqueryInSelect.class ) + public void testComponentParameterBinding(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); Order.Id oId = new Order.Id( "1234", 1 ); @@ -1628,8 +1620,8 @@ public void testComponentParameterBinding() { @SuppressWarnings( {"unchecked"}) @Test - public void testAnyMappingReference() { - Session s = openSession(); + public void testAnyMappingReference(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); PropertyValue redValue = new StringPropertyValue( "red" ); @@ -1664,8 +1656,8 @@ public void testAnyMappingReference() { } @Test - public void testJdkEnumStyleEnumConstant() throws Exception { - Session s = openSession(); + public void testJdkEnumStyleEnumConstant(SessionFactoryScope scope) throws Exception { + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); s.createQuery( "from Zoo z where z.classification = org.hibernate.orm.test.hql.Classification.LAME" ).list(); @@ -1676,9 +1668,9 @@ public void testJdkEnumStyleEnumConstant() throws Exception { @Test @FailureExpected( jiraKey = "unknown" ) - public void testParameterTypeMismatch() { - try ( final SessionImplementor s = (SessionImplementor) openSession() ) { - inTransaction( + public void testParameterTypeMismatch(SessionFactoryScope scope) { + try ( final SessionImplementor s = (SessionImplementor) scope.getSessionFactory().openSession() ) { + scope.inTransaction( s, session -> { try { @@ -1699,9 +1691,9 @@ public void testParameterTypeMismatch() { } @Test - public void testMultipleBagFetchesFail() { - try ( final SessionImplementor s = (SessionImplementor) openSession() ) { - inTransaction( + public void testMultipleBagFetchesFail(SessionFactoryScope scope) { + try ( final SessionImplementor s = (SessionImplementor) scope.getSessionFactory().openSession() ) { + scope.inTransaction( s, session-> { try { @@ -1720,8 +1712,8 @@ public void testMultipleBagFetchesFail() { } @Test - @TestForIssue( jiraKey = "HHH-1248" ) - public void testCollectionJoinsInSubselect() { + @JiraKey( "HHH-1248" ) + public void testCollectionJoinsInSubselect(SessionFactoryScope scope) { // HHH-1248 : initially FromElementFactory treated any explicit join // as an implied join so that theta-style joins would always be used. // This was because correlated subqueries cannot use ANSI-style joins @@ -1729,7 +1721,7 @@ public void testCollectionJoinsInSubselect() { // to only correlated subqueries; it was applied to any subqueries -> // which in-and-of-itself is not necessarily bad. But somewhere later // the choices made there caused joins to be dropped. - Session s = openSession(); + Session s = scope.getSessionFactory().openSession(); Transaction t = s.beginTransaction(); String qryString = "select a.id, a.description" + @@ -1739,11 +1731,11 @@ public void testCollectionJoinsInSubselect() { " select a1 from Animal a1" + " left join a1.offspring o" + " where a1.id=1" + - ")"; + ")"; s.createQuery( qryString ).list(); qryString = "select h.id, h.description" + - " from Human h" + + " from Human h" + " left join h.friends" + " where h in (" + " select h1" + @@ -1754,7 +1746,7 @@ public void testCollectionJoinsInSubselect() { s.createQuery( qryString ).list(); qryString = "select h.id, h.description" + - " from Human h" + + " from Human h" + " left join h.friends f" + " where f in (" + " select h1" + @@ -1768,9 +1760,9 @@ public void testCollectionJoinsInSubselect() { } @Test - public void testCollectionFetchWithDistinctionAndLimit() { + public void testCollectionFetchWithDistinctionAndLimit(SessionFactoryScope scope) { // create some test data... - Session s = openSession(); + Session s = scope.getSessionFactory().openSession(); Transaction t = s.beginTransaction(); int parentCount = 30; for ( int i = 0; i < parentCount; i++ ) { @@ -1790,7 +1782,7 @@ public void testCollectionFetchWithDistinctionAndLimit() { t.commit(); s.close(); - s = openSession(); + s = scope.getSessionFactory().openSession(); t = s.beginTransaction(); // Test simple distinction List results; @@ -1807,7 +1799,7 @@ public void testCollectionFetchWithDistinctionAndLimit() { t.commit(); s.close(); - s = openSession(); + s = scope.getSessionFactory().openSession(); t = s.beginTransaction(); s.createQuery( "delete Animal where mother is not null" ).executeUpdate(); s.createQuery( "delete Animal" ).executeUpdate(); @@ -1816,8 +1808,8 @@ public void testCollectionFetchWithDistinctionAndLimit() { } @Test - public void testFetchInSubqueryFails() { - Session s = openSession(); + public void testFetchInSubqueryFails(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); try { s.createQuery( "from Animal a where a.mother in (select m from Animal a1 inner join a1.mother as m join fetch m.mother)" ).list(); fail( "fetch join allowed in subquery" ); @@ -1832,10 +1824,10 @@ public void testFetchInSubqueryFails() { } @Test - @TestForIssue(jiraKey = "HHH-1830") - @SkipForDialect(value = DerbyDialect.class, comment = "Derby doesn't see that the subquery is functionally dependent") - public void testAggregatedJoinAlias() { - Session s = openSession(); + @JiraKey( "HHH-1830") + @SkipForDialect(dialectClass = DerbyDialect.class, matchSubTypes = true, reason = "Derby doesn't see that the subquery is functionally dependent") + public void testAggregatedJoinAlias(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); s.getTransaction().begin(); s.createQuery( "select p.id, size( descendants ) " + @@ -1848,11 +1840,11 @@ public void testAggregatedJoinAlias() { } @Test - @TestForIssue( jiraKey = "HHH-1464" ) - public void testQueryMetadataRetrievalWithFetching() { + @JiraKey( "HHH-1464" ) + public void testQueryMetadataRetrievalWithFetching(SessionFactoryScope scope) { // HHH-1464 : there was a problem due to the fact they we polled // the shallow version of the query plan to get the metadata. - inSession( + scope.inSession( session -> { final Query query = session.createQuery( "from Animal a inner join fetch a.mother" ); final SqmSelectStatement sqmStatement = (SqmSelectStatement) query.unwrap( QuerySqmImpl.class ).getSqmStatement(); @@ -1863,16 +1855,16 @@ public void testQueryMetadataRetrievalWithFetching() { assertThat( selectionType.getExpressibleJavaType().getJavaTypeClass(), equalTo( Animal.class ) ); } ); - Session s = openSession(); + Session s = scope.getSessionFactory().openSession(); s.close(); } @Test - @TestForIssue( jiraKey = "HHH-429" ) + @JiraKey( "HHH-429" ) @SuppressWarnings( {"unchecked"}) - public void testSuperclassPropertyReferenceAfterCollectionIndexedAccess() { + public void testSuperclassPropertyReferenceAfterCollectionIndexedAccess(SessionFactoryScope scope) { // note: simply performing syntax checking in the db - Session s = openSession(); + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); Mammal tiger = new Mammal(); tiger.setDescription( "Tiger" ); @@ -1890,14 +1882,14 @@ public void testSuperclassPropertyReferenceAfterCollectionIndexedAccess() { s.getTransaction().commit(); s.close(); - s = openSession(); + s = scope.getSessionFactory().openSession(); s.beginTransaction(); List results = s.createQuery( "from Zoo zoo where zoo.mammals['tiger'].mother.bodyWeight > 3.0f" ).list(); assertEquals( 1, results.size() ); s.getTransaction().commit(); s.close(); - s = openSession(); + s = scope.getSessionFactory().openSession(); s.beginTransaction(); s.delete( tiger ); s.delete( mother ); @@ -1907,9 +1899,9 @@ public void testSuperclassPropertyReferenceAfterCollectionIndexedAccess() { } @Test - public void testJoinFetchCollectionOfValues() { + public void testJoinFetchCollectionOfValues(SessionFactoryScope scope) { // note: simply performing syntax checking in the db - Session s = openSession(); + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); s.createQuery( "select h from Human as h join fetch h.nickNames" ).list(); s.getTransaction().commit(); @@ -1917,9 +1909,9 @@ public void testJoinFetchCollectionOfValues() { } @Test - public void testIntegerLiterals() { + public void testIntegerLiterals(SessionFactoryScope scope) { // note: simply performing syntax checking in the db - Session s = openSession(); + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); s.createQuery( "from Foo where long = 1" ).list(); s.createQuery( "from Foo where long = " + Integer.MIN_VALUE ).list(); @@ -1933,9 +1925,9 @@ public void testIntegerLiterals() { } @Test - public void testDecimalLiterals() { + public void testDecimalLiterals(SessionFactoryScope scope) { // note: simply performing syntax checking in the db - Session s = openSession(); + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); s.createQuery( "from Animal where bodyWeight > 100.0e-10" ).list(); s.createQuery( "from Animal where bodyWeight > 100.0E-10" ).list(); @@ -1952,9 +1944,9 @@ public void testDecimalLiterals() { } @Test - public void testNakedPropertyRef() { + public void testNakedPropertyRef(SessionFactoryScope scope) { // note: simply performing syntax and column/table resolution checking in the db - Session s = openSession(); + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); s.createQuery( "from Animal where bodyWeight = bodyWeight" ).list(); s.createQuery( "select bodyWeight from Animal" ).list(); @@ -1964,9 +1956,9 @@ public void testNakedPropertyRef() { } @Test - public void testNakedComponentPropertyRef() { + public void testNakedComponentPropertyRef(SessionFactoryScope scope) { // note: simply performing syntax and column/table resolution checking in the db - Session s = openSession(); + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); s.createQuery( "from Human where name.first = 'Gavin'" ).list(); s.createQuery( "select name from Human" ).list(); @@ -1977,9 +1969,9 @@ public void testNakedComponentPropertyRef() { } @Test - public void testNakedImplicitJoins() { + public void testNakedImplicitJoins(SessionFactoryScope scope) { // note: simply performing syntax and column/table resolution checking in the db - Session s = openSession(); + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); s.createQuery( "from Animal where mother.father.id = 1" ).list(); s.getTransaction().commit(); @@ -1987,11 +1979,11 @@ public void testNakedImplicitJoins() { } @Test - public void testNakedEntityAssociationReference() { + public void testNakedEntityAssociationReference(SessionFactoryScope scope) { // note: simply performing syntax and column/table resolution checking in the db - Session s = openSession(); + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); - if ( getDialect() instanceof AbstractHANADialect ) { + if ( scope.getSessionFactory().getJdbcServices().getDialect() instanceof AbstractHANADialect ) { s.createQuery( "from Animal where mother is null" ).list(); } else { @@ -2003,9 +1995,9 @@ public void testNakedEntityAssociationReference() { } @Test - public void testNakedMapIndex() throws Exception { + public void testNakedMapIndex(SessionFactoryScope scope) throws Exception { // note: simply performing syntax and column/table resolution checking in the db - Session s = openSession(); + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); s.createQuery( "from Zoo where mammals['dog'].description like '%black%'" ).list(); s.getTransaction().commit(); @@ -2013,10 +2005,10 @@ public void testNakedMapIndex() throws Exception { } @Test - public void testInvalidFetchSemantics() { - try ( final SessionImplementor s = (SessionImplementor) openSession()) { + public void testInvalidFetchSemantics(SessionFactoryScope scope) { + try ( final SessionImplementor s = (SessionImplementor) scope.getSessionFactory().openSession()) { - inTransaction( + scope.inTransaction( s, session -> { try { @@ -2031,7 +2023,7 @@ public void testInvalidFetchSemantics() { } ); - inTransaction( + scope.inTransaction( s, session-> { try { @@ -2050,8 +2042,8 @@ public void testInvalidFetchSemantics() { } @Test - public void testArithmetic() { - Session s = openSession(); + public void testArithmetic(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); Transaction t = s.beginTransaction(); Zoo zoo = new Zoo(); zoo.setName("Melbourne Zoo"); @@ -2078,8 +2070,8 @@ public void testArithmetic() { } @Test - public void testNestedCollectionFetch() { - Session s = openSession(); + public void testNestedCollectionFetch(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); Transaction t = s.beginTransaction(); s.createQuery("from Animal a left join fetch a.offspring o left join fetch o.offspring where a.mother.id = 1 order by a.description").list(); s.createQuery("from Zoo z left join fetch z.animals a left join fetch a.offspring where z.name ='MZ' order by a.description").list(); @@ -2089,10 +2081,10 @@ public void testNestedCollectionFetch() { } @Test - @RequiresDialectFeature( DialectChecks.SupportsSubqueryInSelect.class ) + @RequiresDialectFeature( feature = DialectFeatureChecks.SupportsSubqueryInSelect.class ) @SuppressWarnings( {"unchecked"}) - public void testSelectClauseSubselect() { - Session s = openSession(); + public void testSelectClauseSubselect(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); Transaction t = s.beginTransaction(); Zoo zoo = new Zoo(); zoo.setName("Melbourne Zoo"); @@ -2119,8 +2111,8 @@ public void testSelectClauseSubselect() { } @Test - public void testInitProxy() { - Session s = openSession(); + public void testInitProxy(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); Transaction t = s.beginTransaction(); Mammal plat = new Mammal(); plat.setBodyWeight( 11f ); @@ -2140,8 +2132,8 @@ public void testInitProxy() { @Test @SuppressWarnings( {"unchecked"}) - public void testSelectClauseImplicitJoin() { - Session s = openSession(); + public void testSelectClauseImplicitJoin(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); Transaction t = s.beginTransaction(); Zoo zoo = new Zoo(); zoo.setName("The Zoo"); @@ -2187,10 +2179,10 @@ private static void verifyAnimalZooSelection(Query q) { } @Test - @TestForIssue( jiraKey = "HHH-9305") + @JiraKey( "HHH-9305") @SuppressWarnings( {"unchecked"}) - public void testSelectClauseImplicitJoinOrderByJoinedProperty() { - Session s = openSession(); + public void testSelectClauseImplicitJoinOrderByJoinedProperty(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); Transaction t = s.beginTransaction(); Zoo zoo = new Zoo(); zoo.setName("The Zoo"); @@ -2253,8 +2245,8 @@ public void testSelectClauseImplicitJoinOrderByJoinedProperty() { @Test @SuppressWarnings( {"unchecked"}) - public void testSelectClauseDistinctImplicitJoinOrderByJoinedProperty() { - Session s = openSession(); + public void testSelectClauseDistinctImplicitJoinOrderByJoinedProperty(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); Transaction t = s.beginTransaction(); Zoo zoo = new Zoo(); zoo.setName("The Zoo"); @@ -2317,8 +2309,8 @@ public void testSelectClauseDistinctImplicitJoinOrderByJoinedProperty() { @Test @SuppressWarnings( {"unchecked"}) - public void testSelectClauseImplicitJoinWithIterate() { - Session s = openSession(); + public void testSelectClauseImplicitJoinWithIterate(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); Transaction t = s.beginTransaction(); Zoo zoo = new Zoo(); zoo.setName("The Zoo"); @@ -2351,8 +2343,8 @@ public void testSelectClauseImplicitJoinWithIterate() { } @Test - public void testComponentOrderBy() { - Session s = openSession(); + public void testComponentOrderBy(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); Transaction t = s.beginTransaction(); Long id1 = ( Long ) s.save( genSimpleHuman( "John", "Jacob" ) ); @@ -2378,8 +2370,8 @@ public void testComponentOrderBy() { } @Test - public void testOrderedWithCustomColumnReadAndWrite() { - Session s = openSession(); + public void testOrderedWithCustomColumnReadAndWrite(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); Transaction t = s.beginTransaction(); SimpleEntityWithAssociation first = new SimpleEntityWithAssociation(); first.setNegatedNumber( 1 ); @@ -2408,8 +2400,8 @@ public void testOrderedWithCustomColumnReadAndWrite() { } @Test - public void testHavingWithCustomColumnReadAndWrite() { - Session s = openSession(); + public void testHavingWithCustomColumnReadAndWrite(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); Transaction t = s.beginTransaction(); SimpleEntityWithAssociation first = new SimpleEntityWithAssociation(); first.setNegatedNumber(5); @@ -2439,9 +2431,9 @@ public void testHavingWithCustomColumnReadAndWrite() { } @Test - public void testLoadSnapshotWithCustomColumnReadAndWrite() { + public void testLoadSnapshotWithCustomColumnReadAndWrite(SessionFactoryScope scope) { // Exercises entity snapshot load when select-before-update is true. - Session s = openSession(); + Session s = scope.getSessionFactory().openSession(); Transaction t = s.beginTransaction(); final double SIZE_IN_KB = 1536d; final double SIZE_IN_MB = SIZE_IN_KB / 1024d; @@ -2458,7 +2450,7 @@ public void testLoadSnapshotWithCustomColumnReadAndWrite() { t.commit(); s.close(); - s = openSession(); + s = scope.getSessionFactory().openSession(); t = s.beginTransaction(); final double NEW_SIZE_IN_KB = 2048d; final double NEW_SIZE_IN_MB = NEW_SIZE_IN_KB / 1024d; @@ -2482,8 +2474,8 @@ private Human genSimpleHuman(String fName, String lName) { } @Test - public void testCastInSelect() { - Session s = openSession(); + public void testCastInSelect(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); Transaction t = s.beginTransaction(); Animal a = new Animal(); a.setBodyWeight(12.4f); @@ -2506,8 +2498,8 @@ public void testCastInSelect() { } @Test - public void testNumericExpressionReturnTypes() { - Session s = openSession(); + public void testNumericExpressionReturnTypes(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); Transaction t = s.beginTransaction(); Animal a = new Animal(); a.setBodyWeight(12.4f); @@ -2613,8 +2605,8 @@ public void testNumericExpressionReturnTypes() { } @Test - public void testAliases() { - Session s = openSession(); + public void testAliases(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); Transaction t = s.beginTransaction(); Animal a = new Animal(); a.setBodyWeight(12.4f); @@ -2650,9 +2642,9 @@ public void testAliases() { } @Test - @RequiresDialectFeature(DialectChecks.SupportsTemporaryTable.class) - public void testParameterMixing() { - Session s = openSession(); + @RequiresDialectFeature( feature = DialectFeatureChecks.SupportsTemporaryTable.class) + public void testParameterMixing(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); Transaction t = s.beginTransaction(); s.createQuery( "from Animal a where a.description = ?1 and a.bodyWeight = ?2 or a.bodyWeight = :bw" ) .setParameter( 1, "something" ) @@ -2664,8 +2656,8 @@ public void testParameterMixing() { } @Test - public void testOrdinalParameters() { - Session s = openSession(); + public void testOrdinalParameters(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); Transaction t = s.beginTransaction(); s.createQuery( "from Animal a where a.description = ?1 and a.bodyWeight = ?2" ) .setParameter( 1, "something" ) @@ -2680,8 +2672,8 @@ public void testOrdinalParameters() { } @Test - public void testIndexParams() { - Session s = openSession(); + public void testIndexParams(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); Transaction t = s.beginTransaction(); s.createQuery( "from Zoo zoo where zoo.mammals[:name].id = :id" ) .setParameter( "name", "Walrus" ) @@ -2711,8 +2703,8 @@ public void testIndexParams() { } @Test - public void testAggregation() { - Session s = openSession(); + public void testAggregation(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); Human h = new Human(); h.setBodyWeight( (float) 74.0 ); @@ -2731,7 +2723,7 @@ public void testAggregation() { s.getTransaction().commit(); s.close(); - s = openSession(); + s = scope.getSessionFactory().openSession(); s.beginTransaction(); h = new Human(); h.setFloatValue( 2.5F ); @@ -2762,8 +2754,8 @@ public void testAggregation() { } @Test - public void testSelectClauseCase() { - Session s = openSession(); + public void testSelectClauseCase(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); Transaction t = s.beginTransaction(); Human h = new Human(); h.setBodyWeight( (float) 74.0 ); @@ -2782,9 +2774,9 @@ public void testSelectClauseCase() { } @Test - @RequiresDialectFeature( DialectChecks.SupportsSubqueryInSelect.class ) - public void testImplicitPolymorphism() { - Session s = openSession(); + @RequiresDialectFeature( feature = DialectFeatureChecks.SupportsSubqueryInSelect.class ) + public void testImplicitPolymorphism(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); Transaction t = s.beginTransaction(); Product product = new Product(); @@ -2807,8 +2799,8 @@ public void testImplicitPolymorphism() { } @Test - public void testCoalesce() { - Session session = openSession(); + public void testCoalesce(SessionFactoryScope scope) { + Session session = scope.getSessionFactory().openSession(); Transaction txn = session.beginTransaction(); session.createQuery("from Human h where coalesce(h.nickName, h.name.first, h.name.last) = 'max'").list(); session.createQuery("select nullif(nickName, '1e1') from Human").list(); @@ -2817,14 +2809,14 @@ public void testCoalesce() { } @Test - public void testStr() { - Session session = openSession(); + public void testStr(SessionFactoryScope scope) { + Session session = scope.getSessionFactory().openSession(); Transaction txn = session.beginTransaction(); Animal an = new Animal(); an.setBodyWeight(123.45f); session.persist( an ); String str = (String) session.createQuery("select str(an.bodyWeight) from Animal an where str(an.bodyWeight) like '%1%'").uniqueResult(); - if ( getDialect() instanceof DB2Dialect ) { + if ( scope.getSessionFactory().getJdbcServices().getDialect() instanceof DB2Dialect ) { assertTrue( str.startsWith( "1.234" ) ); } else { @@ -2847,10 +2839,10 @@ public void testStr() { } @Test - @SkipForDialect( MySQLDialect.class ) - @SkipForDialect( DB2Dialect.class ) - public void testCast() { - Session session = openSession(); + @SkipForDialect( dialectClass = MySQLDialect.class, matchSubTypes = true ) + @SkipForDialect( dialectClass = DB2Dialect.class, matchSubTypes = true ) + public void testCast(SessionFactoryScope scope) { + Session session = scope.getSessionFactory().openSession(); Transaction txn = session.beginTransaction(); session.createQuery("from Human h where h.nickName like 'G%'").list(); session.createQuery("from Animal a where cast(a.bodyWeight as string) like '1.%'").list(); @@ -2860,12 +2852,12 @@ public void testCast() { } @Test - public void testExtract() { - Session session = openSession(); + public void testExtract(SessionFactoryScope scope) { + Session session = scope.getSessionFactory().openSession(); Transaction txn = session.beginTransaction(); session.createQuery("select second(current_timestamp()), minute(current_timestamp()), hour(current_timestamp()) from Mammal m").list(); session.createQuery("select day(m.birthdate), month(m.birthdate), year(m.birthdate) from Mammal m").list(); - if ( !(getDialect() instanceof DB2Dialect) ) { //no ANSI extract + if ( !(scope.getSessionFactory().getJdbcServices().getDialect() instanceof DB2Dialect) ) { //no ANSI extract session.createQuery("select extract(second from current_timestamp()), extract(minute from current_timestamp()), extract(hour from current_timestamp()) from Mammal m").list(); session.createQuery("select extract(day from m.birthdate), extract(month from m.birthdate), extract(year from m.birthdate) from Mammal m").list(); } @@ -2874,11 +2866,11 @@ public void testExtract() { } @Test - @SkipForDialect(value = CockroachDialect.class, comment = "https://github.com/cockroachdb/cockroach/issues/41943") + @SkipForDialect(dialectClass = CockroachDialect.class, matchSubTypes = true, reason = "https://github.com/cockroachdb/cockroach/issues/41943") @SuppressWarnings( {"UnusedAssignment", "UnusedDeclaration"}) - public void testSelectExpressions() { - createTestBaseData(); - Session session = openSession(); + public void testSelectExpressions(SessionFactoryScope scope) { + createTestBaseData( scope ); + Session session = scope.getSessionFactory().openSession(); Transaction txn = session.beginTransaction(); Human h = new Human(); h.setName( new Name( "Gavin", 'A', "King" ) ); @@ -2906,11 +2898,11 @@ public void testSelectExpressions() { session.delete(h); txn.commit(); session.close(); - destroyTestBaseData(); + destroyTestBaseData( scope ); } - private void createTestBaseData() { - Session session = openSession(); + private void createTestBaseData(SessionFactoryScope scope) { + Session session = scope.getSessionFactory().openSession(); Transaction txn = session.beginTransaction(); Mammal m1 = new Mammal(); @@ -2933,8 +2925,8 @@ private void createTestBaseData() { createdAnimalIds.add( m2.getId() ); } - private void destroyTestBaseData() { - Session session = openSession(); + private void destroyTestBaseData(SessionFactoryScope scope) { + Session session = scope.getSessionFactory().openSession(); Transaction txn = session.beginTransaction(); for ( Long createdAnimalId : createdAnimalIds ) { @@ -2949,8 +2941,8 @@ private void destroyTestBaseData() { } @Test - public void testImplicitJoin() throws Exception { - Session session = openSession(); + public void testImplicitJoin(SessionFactoryScope scope) throws Exception { + Session session = scope.getSessionFactory().openSession(); Transaction t = session.beginTransaction(); Animal a = new Animal(); a.setBodyWeight(0.5f); @@ -2975,48 +2967,48 @@ public void testImplicitJoin() throws Exception { } @Test - public void testFromOnly() throws Exception { - createTestBaseData(); - Session session = openSession(); + public void testFromOnly(SessionFactoryScope scope) throws Exception { + createTestBaseData( scope ); + Session session = scope.getSessionFactory().openSession(); Transaction t = session.beginTransaction(); List results = session.createQuery( "from Animal" ).list(); assertEquals( "Incorrect result size", 2, results.size() ); assertTrue( "Incorrect result return type", results.get( 0 ) instanceof Animal ); t.commit(); session.close(); - destroyTestBaseData(); + destroyTestBaseData( scope ); } @Test - public void testSimpleSelect() throws Exception { - createTestBaseData(); - Session session = openSession(); + public void testSimpleSelect(SessionFactoryScope scope) throws Exception { + createTestBaseData( scope ); + Session session = scope.getSessionFactory().openSession(); Transaction t = session.beginTransaction(); List results = session.createQuery( "select a from Animal as a" ).list(); assertEquals( "Incorrect result size", 2, results.size() ); assertTrue( "Incorrect result return type", results.get( 0 ) instanceof Animal ); t.commit(); session.close(); - destroyTestBaseData(); + destroyTestBaseData( scope ); } @Test - public void testEntityPropertySelect() throws Exception { - createTestBaseData(); - Session session = openSession(); + public void testEntityPropertySelect(SessionFactoryScope scope) throws Exception { + createTestBaseData( scope ); + Session session = scope.getSessionFactory().openSession(); Transaction t = session.beginTransaction(); List results = session.createQuery( "select a.mother from Animal as a" ).list(); assertTrue( "Incorrect result return type", results.get( 0 ) instanceof Animal ); t.commit(); session.close(); - destroyTestBaseData(); + destroyTestBaseData( scope ); } @Test - public void testWhere() throws Exception { - createTestBaseData(); + public void testWhere(SessionFactoryScope scope) throws Exception { + createTestBaseData( scope ); - Session session = openSession(); + Session session = scope.getSessionFactory().openSession(); Transaction t = session.beginTransaction(); List results = session.createQuery( "from Animal an where an.bodyWeight > 10" ).list(); assertEquals( "Incorrect result size", 1, results.size() ); @@ -3039,14 +3031,14 @@ public void testWhere() throws Exception { t.commit(); session.close(); - destroyTestBaseData(); + destroyTestBaseData( scope ); } @Test - public void testEntityFetching() throws Exception { - createTestBaseData(); + public void testEntityFetching(SessionFactoryScope scope) throws Exception { + createTestBaseData( scope ); - Session session = openSession(); + Session session = scope.getSessionFactory().openSession(); Transaction t = session.beginTransaction(); List results = session.createQuery( "from Animal an join fetch an.mother" ).list(); @@ -3064,14 +3056,14 @@ public void testEntityFetching() throws Exception { t.commit(); session.close(); - destroyTestBaseData(); + destroyTestBaseData( scope ); } @Test - public void testCollectionFetching() throws Exception { - createTestBaseData(); + public void testCollectionFetching(SessionFactoryScope scope) throws Exception { + createTestBaseData( scope ); - Session session = openSession(); + Session session = scope.getSessionFactory().openSession(); Transaction t = session.beginTransaction(); List results = session.createQuery( "from Animal an join fetch an.offspring" ).list(); assertEquals( "Incorrect result size", 1, results.size() ); @@ -3088,12 +3080,12 @@ public void testCollectionFetching() throws Exception { t.commit(); session.close(); - destroyTestBaseData(); + destroyTestBaseData( scope ); } @Test @SuppressWarnings( {"unchecked"}) - public void testJoinFetchedCollectionOfJoinedSubclass() throws Exception { + public void testJoinFetchedCollectionOfJoinedSubclass(SessionFactoryScope scope) throws Exception { Mammal mammal = new Mammal(); mammal.setDescription( "A Zebra" ); Zoo zoo = new Zoo(); @@ -3101,14 +3093,14 @@ public void testJoinFetchedCollectionOfJoinedSubclass() throws Exception { zoo.getMammals().put( "zebra", mammal ); mammal.setZoo( zoo ); - Session session = openSession(); + Session session = scope.getSessionFactory().openSession(); Transaction txn = session.beginTransaction(); session.save( mammal ); session.save( zoo ); txn.commit(); session.close(); - session = openSession(); + session = scope.getSessionFactory().openSession(); txn = session.beginTransaction(); List results = session.createQuery( "from Zoo z join fetch z.mammals" ).list(); assertEquals( "Incorrect result size", 1, results.size() ); @@ -3126,7 +3118,7 @@ public void testJoinFetchedCollectionOfJoinedSubclass() throws Exception { @Test @SuppressWarnings( {"unchecked"}) - public void testJoinedCollectionOfJoinedSubclass() throws Exception { + public void testJoinedCollectionOfJoinedSubclass(SessionFactoryScope scope) throws Exception { Mammal mammal = new Mammal(); mammal.setDescription( "A Zebra" ); Zoo zoo = new Zoo(); @@ -3134,14 +3126,14 @@ public void testJoinedCollectionOfJoinedSubclass() throws Exception { zoo.getMammals().put( "zebra", mammal ); mammal.setZoo( zoo ); - Session session = openSession(); + Session session = scope.getSessionFactory().openSession(); Transaction txn = session.beginTransaction(); session.save( mammal ); session.save( zoo ); txn.commit(); session.close(); - session = openSession(); + session = scope.getSessionFactory().openSession(); txn = session.beginTransaction(); List results = session.createQuery( "select z, m from Zoo z join z.mammals m" ).list(); assertEquals( "Incorrect result size", 1, results.size() ); @@ -3159,7 +3151,7 @@ public void testJoinedCollectionOfJoinedSubclass() throws Exception { @Test @SuppressWarnings( {"unchecked"}) - public void testJoinedCollectionOfJoinedSubclassProjection() throws Exception { + public void testJoinedCollectionOfJoinedSubclassProjection(SessionFactoryScope scope) throws Exception { Mammal mammal = new Mammal(); mammal.setDescription( "A Zebra" ); Zoo zoo = new Zoo(); @@ -3167,14 +3159,14 @@ public void testJoinedCollectionOfJoinedSubclassProjection() throws Exception { zoo.getMammals().put( "zebra", mammal ); mammal.setZoo( zoo ); - Session session = openSession(); + Session session = scope.getSessionFactory().openSession(); Transaction txn = session.beginTransaction(); session.save( mammal ); session.save( zoo ); txn.commit(); session.close(); - session = openSession(); + session = scope.getSessionFactory().openSession(); txn = session.beginTransaction(); List results = session.createQuery( "select z, m from Zoo z join z.mammals m" ).list(); assertEquals( "Incorrect result size", 1, results.size() ); @@ -3191,9 +3183,9 @@ public void testJoinedCollectionOfJoinedSubclassProjection() throws Exception { } @Test - public void testProjectionQueries() throws Exception { - createTestBaseData(); - Session session = openSession(); + public void testProjectionQueries(SessionFactoryScope scope) throws Exception { + createTestBaseData( scope ); + Session session = scope.getSessionFactory().openSession(); Transaction t = session.beginTransaction(); List results = session.createQuery( "select an.mother.id, max(an.bodyWeight) from Animal an group by an.mother.id" ).list(); @@ -3204,13 +3196,13 @@ public void testProjectionQueries() throws Exception { t.commit(); session.close(); - destroyTestBaseData(); + destroyTestBaseData( scope ); } @Test - @SkipForDialect(value = CockroachDialect.class, strictMatching = true) - public void testStandardFunctions() { - Session session = openSession(); + @SkipForDialect(dialectClass = CockroachDialect.class) + public void testStandardFunctions(SessionFactoryScope scope) { + Session session = scope.getSessionFactory().openSession(); Transaction t = session.beginTransaction(); Product p = new Product(); p.setDescription( "a product" ); @@ -3232,10 +3224,10 @@ public void testStandardFunctions() { } @Test - public void testDynamicInstantiationQueries() throws Exception { - createTestBaseData(); + public void testDynamicInstantiationQueries(SessionFactoryScope scope) throws Exception { + createTestBaseData( scope ); - Session session = openSession(); + Session session = scope.getSessionFactory().openSession(); Transaction t = session.beginTransaction(); List results = session.createQuery( "select new Animal(an.description, an.bodyWeight) from Animal an" ).list(); @@ -3281,7 +3273,7 @@ public void testDynamicInstantiationQueries() throws Exception { } // caching... - QueryStatistics stats = sessionFactory().getStatistics().getQueryStatistics( "select new Animal(an.description, an.bodyWeight) from Animal an" ); + QueryStatistics stats = scope.getSessionFactory().getStatistics().getQueryStatistics( "select new Animal(an.description, an.bodyWeight) from Animal an" ); results = session.createQuery( "select new Animal(an.description, an.bodyWeight) from Animal an" ) .setCacheable( true ) .list(); @@ -3298,12 +3290,12 @@ public void testDynamicInstantiationQueries() throws Exception { t.commit(); session.close(); - destroyTestBaseData(); + destroyTestBaseData( scope ); } @Test - @TestForIssue( jiraKey = "HHH-9305") - public void testDynamicInstantiationWithToOneQueries() throws Exception { + @JiraKey( "HHH-9305") + public void testDynamicInstantiationWithToOneQueries(SessionFactoryScope scope) throws Exception { final Employee employee1 = new Employee(); employee1.setFirstName( "Jane" ); employee1.setLastName( "Doe" ); @@ -3321,7 +3313,7 @@ public void testDynamicInstantiationWithToOneQueries() throws Exception { title2.setDescription( "John's title" ); employee2.setTitle( title2 ); - Session s = openSession(); + Session s = scope.getSessionFactory().openSession(); s.getTransaction().begin(); s.persist( title1 ); s.persist( dept1 ); @@ -3336,64 +3328,64 @@ public void testDynamicInstantiationWithToOneQueries() throws Exception { // at the beginning of the FROM clause, avoiding failures on DBs that cannot handle cross joins // interleaved with ANSI joins (e.g., PostgreSql). - s = openSession(); + s = scope.getSessionFactory().openSession(); s.getTransaction().begin(); - List results = session.createQuery( + List results = s.createQuery( "select new Employee(e.id, e.lastName, e.title.id, e.title.description, e.department, e.firstName) from Employee e inner join e.title" ).list(); assertEquals( "Incorrect result size", 1, results.size() ); assertClassAssignability( results.get( 0 ).getClass(), Employee.class ); - results = session.createQuery( + results = s.createQuery( "select new Employee(e.id, e.lastName, t.id, t.description, e.department, e.firstName) from Employee e inner join e.title t" ).list(); assertEquals( "Incorrect result size", 1, results.size() ); assertClassAssignability( results.get( 0 ).getClass(), Employee.class ); - results = session.createQuery( + results = s.createQuery( "select new Employee(e.id, e.lastName, e.title.id, e.title.description, e.department, e.firstName) from Employee e inner join e.department" ).list(); assertEquals( "Incorrect result size", 1, results.size() ); assertClassAssignability( results.get( 0 ).getClass(), Employee.class ); - results = session.createQuery( + results = s.createQuery( "select new Employee(e.id, e.lastName, e.title.id, e.title.description, d, e.firstName) from Employee e inner join e.department d" ).list(); assertEquals( "Incorrect result size", 1, results.size() ); assertClassAssignability( results.get( 0 ).getClass(), Employee.class ); - results = session.createQuery( + results = s.createQuery( "select new Employee(e.id, e.lastName, e.title.id, e.title.description, e.department, e.firstName) from Employee e left outer join e.department" ).list(); assertEquals( "Incorrect result size", 2, results.size() ); assertClassAssignability( results.get( 0 ).getClass(), Employee.class ); - results = session.createQuery( + results = s.createQuery( "select new Employee(e.id, e.lastName, e.title.id, e.title.description, d, e.firstName) from Employee e left outer join e.department d" ).list(); assertEquals( "Incorrect result size", 2, results.size() ); assertClassAssignability( results.get( 0 ).getClass(), Employee.class ); - results = session.createQuery( + results = s.createQuery( "select new Employee(e.id, e.lastName, e.title.id, e.title.description, e.department, e.firstName) from Employee e left outer join e.department inner join e.title" ).list(); assertEquals( "Incorrect result size", 2, results.size() ); assertClassAssignability( results.get( 0 ).getClass(), Employee.class ); - results = session.createQuery( + results = s.createQuery( "select new Employee(e.id, e.lastName, t.id, t.description, d, e.firstName) from Employee e left outer join e.department d inner join e.title t" ).list(); assertEquals( "Incorrect result size", 2, results.size() ); assertClassAssignability( results.get( 0 ).getClass(), Employee.class ); - results = session.createQuery( + results = s.createQuery( "select new Employee(e.id, e.lastName, e.title.id, e.title.description, e.department, e.firstName) from Employee e left outer join e.department left outer join e.title" ).list(); assertEquals( "Incorrect result size", 2, results.size() ); assertClassAssignability( results.get( 0 ).getClass(), Employee.class ); - results = session.createQuery( + results = s.createQuery( "select new Employee(e.id, e.lastName, t.id, t.description, d, e.firstName) from Employee e left outer join e.department d left outer join e.title t" ).list(); assertEquals( "Incorrect result size", 2, results.size() ); assertClassAssignability( results.get( 0 ).getClass(), Employee.class ); - results = session.createQuery( + results = s.createQuery( "select new Employee(e.id, e.lastName, e.title.id, e.title.description, e.department, e.firstName) from Employee e left outer join e.department order by e.title.description" ).list(); assertEquals( "Incorrect result size", 2, results.size() ); assertClassAssignability( results.get( 0 ).getClass(), Employee.class ); - results = session.createQuery( + results = s.createQuery( "select new Employee(e.id, e.lastName, e.title.id, e.title.description, e.department, e.firstName) from Employee e left outer join e.department d order by e.title.description" ).list(); assertEquals( "Incorrect result size", 2, results.size() ); @@ -3402,7 +3394,7 @@ public void testDynamicInstantiationWithToOneQueries() throws Exception { s.close(); - s = openSession(); + s = scope.getSessionFactory().openSession(); s.getTransaction().begin(); s.delete( employee1 ); s.delete( title1 ); @@ -3415,7 +3407,7 @@ public void testDynamicInstantiationWithToOneQueries() throws Exception { @Test @SuppressWarnings( {"UnusedAssignment"}) - public void testCachedJoinedAndJoinFetchedManyToOne() throws Exception { + public void testCachedJoinedAndJoinFetchedManyToOne(SessionFactoryScope scope) throws Exception { Animal a = new Animal(); a.setDescription( "an animal" ); @@ -3434,7 +3426,7 @@ public void testCachedJoinedAndJoinFetchedManyToOne() throws Exception { a.addOffspring( offspring2 ); offspring2.setMother( a ); - Session s = openSession(); + Session s = scope.getSessionFactory().openSession(); Transaction t = s.beginTransaction(); s.save( mother ); s.save( a ); @@ -3443,20 +3435,20 @@ public void testCachedJoinedAndJoinFetchedManyToOne() throws Exception { t.commit(); s.close(); - sessionFactory().getCache().evictQueryRegions(); - sessionFactory().getStatistics().clear(); + scope.getSessionFactory().getCache().evictQueryRegions(); + scope.getSessionFactory().getStatistics().clear(); - s = openSession(); + s = scope.getSessionFactory().openSession(); t = s.beginTransaction(); List list = s.createQuery( "from Animal a left join fetch a.mother" ).setCacheable( true ).list(); - assertEquals( 0, sessionFactory().getStatistics().getQueryCacheHitCount() ); - assertEquals( 1, sessionFactory().getStatistics().getQueryCachePutCount() ); + assertEquals( 0, scope.getSessionFactory().getStatistics().getQueryCacheHitCount() ); + assertEquals( 1, scope.getSessionFactory().getStatistics().getQueryCachePutCount() ); list = s.createQuery( "select a from Animal a left join fetch a.mother" ).setCacheable( true ).list(); - assertEquals( 1, sessionFactory().getStatistics().getQueryCacheHitCount() ); - assertEquals( 1, sessionFactory().getStatistics().getQueryCachePutCount() ); + assertEquals( 1, scope.getSessionFactory().getStatistics().getQueryCacheHitCount() ); + assertEquals( 1, scope.getSessionFactory().getStatistics().getQueryCachePutCount() ); list = s.createQuery( "select a, m from Animal a left join a.mother m" ).setCacheable( true ).list(); - assertEquals( 1, sessionFactory().getStatistics().getQueryCacheHitCount() ); - assertEquals( 2, sessionFactory().getStatistics().getQueryCachePutCount() ); + assertEquals( 1, scope.getSessionFactory().getStatistics().getQueryCacheHitCount() ); + assertEquals( 2, scope.getSessionFactory().getStatistics().getQueryCachePutCount() ); list = s.createQuery( "from Animal" ).list(); for(Object obj : list){ s.delete( obj ); @@ -3467,7 +3459,7 @@ public void testCachedJoinedAndJoinFetchedManyToOne() throws Exception { @Test @SuppressWarnings( {"UnusedAssignment", "UnusedDeclaration"}) - public void testCachedJoinedAndJoinFetchedOneToMany() throws Exception { + public void testCachedJoinedAndJoinFetchedOneToMany(SessionFactoryScope scope) throws Exception { Animal a = new Animal(); a.setDescription( "an animal" ); Animal mother = new Animal(); @@ -3483,10 +3475,10 @@ public void testCachedJoinedAndJoinFetchedOneToMany() throws Exception { a.addOffspring( offspring2 ); offspring2.setMother( a ); - sessionFactory().getCache().evictQueryRegions(); - sessionFactory().getStatistics().clear(); + scope.getSessionFactory().getCache().evictQueryRegions(); + scope.getSessionFactory().getStatistics().clear(); - Session s = openSession(); + Session s = scope.getSessionFactory().openSession(); Transaction t = s.beginTransaction(); s.save( mother ); s.save( a ); @@ -3495,17 +3487,17 @@ public void testCachedJoinedAndJoinFetchedOneToMany() throws Exception { t.commit(); s.close(); - s = openSession(); + s = scope.getSessionFactory().openSession(); t = s.beginTransaction(); List list = s.createQuery( "from Animal a left join fetch a.offspring" ).setCacheable( true ).list(); - assertEquals( 0, sessionFactory().getStatistics().getQueryCacheHitCount() ); - assertEquals( 1, sessionFactory().getStatistics().getQueryCachePutCount() ); + assertEquals( 0, scope.getSessionFactory().getStatistics().getQueryCacheHitCount() ); + assertEquals( 1, scope.getSessionFactory().getStatistics().getQueryCachePutCount() ); list = s.createQuery( "select a from Animal a left join fetch a.offspring" ).setCacheable( true ).list(); - assertEquals( 1, sessionFactory().getStatistics().getQueryCacheHitCount() ); - assertEquals( 1, sessionFactory().getStatistics().getQueryCachePutCount() ); + assertEquals( 1, scope.getSessionFactory().getStatistics().getQueryCacheHitCount() ); + assertEquals( 1, scope.getSessionFactory().getStatistics().getQueryCachePutCount() ); list = s.createQuery( "select a, o from Animal a left join a.offspring o" ).setCacheable( true ).list(); - assertEquals( 1, sessionFactory().getStatistics().getQueryCacheHitCount() ); - assertEquals( 2, sessionFactory().getStatistics().getQueryCachePutCount() ); + assertEquals( 1, scope.getSessionFactory().getStatistics().getQueryCacheHitCount() ); + assertEquals( 2, scope.getSessionFactory().getStatistics().getQueryCachePutCount() ); list = s.createQuery( "from Animal" ).list(); for ( Object obj : list ) { s.delete( obj ); @@ -3515,9 +3507,9 @@ public void testCachedJoinedAndJoinFetchedOneToMany() throws Exception { } @Test - public void testSelectNewTransformerQueries() { - createTestBaseData(); - Session session = openSession(); + public void testSelectNewTransformerQueries(SessionFactoryScope scope) { + createTestBaseData( scope ); + Session session = scope.getSessionFactory().openSession(); Transaction t = session.beginTransaction(); List list = session.createQuery( "select new Animal(an.description, an.bodyWeight) as animal from Animal an order by an.description" ) .setResultTransformer( Transformers.ALIAS_TO_ENTITY_MAP ) @@ -3531,16 +3523,16 @@ public void testSelectNewTransformerQueries() { assertEquals( "Mammal #2", m2.get( "animal" ).getDescription() ); t.commit(); session.close(); - destroyTestBaseData(); + destroyTestBaseData( scope ); } @Test - public void testResultTransformerScalarQueries() throws Exception { - createTestBaseData(); + public void testResultTransformerScalarQueries(SessionFactoryScope scope) throws Exception { + createTestBaseData( scope ); String query = "select an.description as description, an.bodyWeight as bodyWeight from Animal an order by bodyWeight desc"; - Session session = openSession(); + Session session = scope.getSessionFactory().openSession(); Transaction t = session.beginTransaction(); List results = session.createQuery( query ) @@ -3556,11 +3548,11 @@ public void testResultTransformerScalarQueries() throws Exception { t.commit(); session.close(); - session = openSession(); + session = scope.getSessionFactory().openSession(); t = session.beginTransaction(); try (ScrollableResults sr = session.createQuery( query ) - .setResultTransformer(Transformers.aliasToBean(Animal.class)).scroll()) { + .setResultTransformer( Transformers.aliasToBean( Animal.class ) ).scroll()) { assertTrue( "Incorrect result size", sr.next() ); assertTrue( "Incorrect return type", sr.get() instanceof Animal ); assertFalse( session.contains( sr.get() ) ); @@ -3569,7 +3561,7 @@ public void testResultTransformerScalarQueries() throws Exception { t.commit(); session.close(); - session = openSession(); + session = scope.getSessionFactory().openSession(); t = session.beginTransaction(); results = session.createQuery( "select a from Animal a, Animal b order by a.id" ) @@ -3590,16 +3582,16 @@ public Object transformTuple(Object[] tuple, String[] aliases) { t.commit(); session.close(); - destroyTestBaseData(); + destroyTestBaseData( scope ); } @Test - public void testResultTransformerEntityQueries() throws Exception { - createTestBaseData(); + public void testResultTransformerEntityQueries(SessionFactoryScope scope) throws Exception { + createTestBaseData( scope ); String query = "select an as an from Animal an order by bodyWeight desc"; - Session session = openSession(); + Session session = scope.getSessionFactory().openSession(); Transaction t = session.beginTransaction(); List results = session.createQuery( query ) @@ -3618,7 +3610,7 @@ public void testResultTransformerEntityQueries() throws Exception { t.commit(); session.close(); - session = openSession(); + session = scope.getSessionFactory().openSession(); t = session.beginTransaction(); try (ScrollableResults sr = session.createQuery( query ) @@ -3630,12 +3622,12 @@ public void testResultTransformerEntityQueries() throws Exception { t.commit(); session.close(); - destroyTestBaseData(); + destroyTestBaseData( scope ); } @Test - public void testEJBQLFunctions() throws Exception { - Session session = openSession(); + public void testEJBQLFunctions(SessionFactoryScope scope) throws Exception { + Session session = scope.getSessionFactory().openSession(); Transaction t = session.beginTransaction(); String hql = "from Animal a where a.description = concat('1', concat('2','3'), '4'||'5')||'0'"; @@ -3665,17 +3657,21 @@ public void testEJBQLFunctions() throws Exception { hql = "select length(a.description) from Animal a"; session.createQuery(hql).list(); - //note: postgres and db2 don't have a 3-arg form, it gets transformed to 2-args - hql = "from Animal a where locate('abc', a.description, 2) = 2"; - session.createQuery(hql).list(); + Dialect dialect = scope.getSessionFactory().getJdbcServices().getDialect(); + // Informix before version 12 didn't support finding the index of substrings + if ( !( dialect instanceof InformixDialect && dialect.getVersion().isBefore( 12 ) ) ) { + //note: postgres and db2 don't have a 3-arg form, it gets transformed to 2-args + hql = "from Animal a where locate('abc', a.description, 2) = 2"; + session.createQuery( hql ).list(); - hql = "from Animal a where locate('abc', a.description) = 2"; - session.createQuery(hql).list(); + hql = "from Animal a where locate('abc', a.description) = 2"; + session.createQuery( hql ).list(); - hql = "select locate('cat', a.description, 2) from Animal a"; - session.createQuery(hql).list(); + hql = "select locate('cat', a.description, 2) from Animal a"; + session.createQuery( hql ).list(); + } - if ( !( getDialect() instanceof DB2Dialect ) ) { + if ( !( dialect instanceof DB2Dialect ) ) { hql = "from Animal a where trim(trailing '_' from a.description) = 'cat'"; session.createQuery(hql).list(); @@ -3689,7 +3685,7 @@ public void testEJBQLFunctions() throws Exception { session.createQuery(hql).list(); } - if ( !(getDialect() instanceof HSQLDialect) ) { //HSQL doesn't like trim() without specification + if ( !(dialect instanceof HSQLDialect) ) { //HSQL doesn't like trim() without specification hql = "from Animal a where trim(a.description) = 'cat'"; session.createQuery(hql).list(); } @@ -3728,10 +3724,10 @@ public void testEJBQLFunctions() throws Exception { } @Test - @TestForIssue( jiraKey = "HHH-11942" ) - public void testOrderByExtraParenthesis() throws Exception { + @JiraKey( "HHH-11942" ) + public void testOrderByExtraParenthesis(SessionFactoryScope scope) throws Exception { try { - doInHibernate( this::sessionFactory, session -> { + scope.inTransaction( session -> { session.createQuery( "select a from Product a " + "where " + @@ -3750,22 +3746,22 @@ public void testOrderByExtraParenthesis() throws Exception { } @RequiresDialectFeature( - value = DialectChecks.SupportSubqueryAsLeftHandSideInPredicate.class, + feature = DialectFeatureChecks.SupportsSubqueryAsLeftHandSideInPredicate.class, comment = "Database does not support using subquery as singular value expression" ) - public void testSubqueryAsSingularValueExpression() { - assertResultSize( "from Animal x where (select max(a.bodyWeight) from Animal a) in (1,2,3)", 0 ); - assertResultSize( "from Animal x where (select max(a.bodyWeight) from Animal a) between 0 and 100", 0 ); - assertResultSize( "from Animal x where (select max(a.description) from Animal a) like 'big%'", 0 ); - assertResultSize( "from Animal x where (select max(a.bodyWeight) from Animal a) is not null", 0 ); + public void testSubqueryAsSingularValueExpression(SessionFactoryScope scope) { + assertResultSize( scope, "from Animal x where (select max(a.bodyWeight) from Animal a) in (1,2,3)", 0 ); + assertResultSize( scope,"from Animal x where (select max(a.bodyWeight) from Animal a) between 0 and 100", 0 ); + assertResultSize( scope,"from Animal x where (select max(a.description) from Animal a) like 'big%'", 0 ); + assertResultSize( scope,"from Animal x where (select max(a.bodyWeight) from Animal a) is not null", 0 ); } - public void testExistsSubquery() { - assertResultSize( "from Animal x where exists (select max(a.bodyWeight) from Animal a)", 0 ); + public void testExistsSubquery(SessionFactoryScope scope) { + assertResultSize( scope, "from Animal x where exists (select max(a.bodyWeight) from Animal a)", 0 ); } - private void assertResultSize(String hql, int size) { - Session session = openSession(); + private void assertResultSize(SessionFactoryScope scope, String hql, int size) { + Session session = scope.getSessionFactory().openSession(); Transaction txn = session.beginTransaction(); assertEquals( size, session.createQuery(hql).list().size() ); txn.commit(); @@ -3782,14 +3778,16 @@ public void prepare(Query query) { }; private class SyntaxChecker { + private final SessionFactoryScope scope; private final String hql; private final QueryPreparer preparer; - public SyntaxChecker(String hql) { - this( hql, DEFAULT_PREPARER ); + public SyntaxChecker(SessionFactoryScope scope, String hql) { + this( scope, hql, DEFAULT_PREPARER ); } - public SyntaxChecker(String hql, QueryPreparer preparer) { + public SyntaxChecker(SessionFactoryScope scope, String hql, QueryPreparer preparer) { + this.scope = scope; this.hql = hql; this.preparer = preparer; } @@ -3800,7 +3798,7 @@ public void checkAll() { } public SyntaxChecker checkList() { - Session s = openSession(); + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); Query query = s.createQuery( hql ); preparer.prepare( query ); @@ -3811,7 +3809,7 @@ public SyntaxChecker checkList() { } public SyntaxChecker checkScroll() { - Session s = openSession(); + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); Query query = s.createQuery( hql ); preparer.prepare( query ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/hql/HQLTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/hql/HQLTest.java index aa71355e8678..752f6118f586 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/hql/HQLTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/hql/HQLTest.java @@ -25,6 +25,7 @@ import org.hibernate.ScrollableResults; import org.hibernate.Session; import org.hibernate.community.dialect.FirebirdDialect; +import org.hibernate.community.dialect.InformixDialect; import org.hibernate.dialect.CockroachDialect; import org.hibernate.dialect.DerbyDialect; import org.hibernate.dialect.H2Dialect; @@ -1523,6 +1524,7 @@ public void test_hql_aggregate_functions_filter_example() { @SkipForDialect(dialectClass = DerbyDialect.class) @SkipForDialect(dialectClass = SybaseASEDialect.class) @SkipForDialect(dialectClass = FirebirdDialect.class, reason = "order by not supported in list") + @SkipForDialect(dialectClass = InformixDialect.class) public void test_hql_aggregate_functions_within_group_example() { doInJPA(this::entityManagerFactory, entityManager -> { //tag::hql-aggregate-functions-within-group-example[] diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/hql/ManyToOneJoinReuseTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/hql/ManyToOneJoinReuseTest.java index 4722974bc7e9..b62d05aea280 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/hql/ManyToOneJoinReuseTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/hql/ManyToOneJoinReuseTest.java @@ -78,7 +78,7 @@ public void joinAndImplicitPath(SessionFactoryScope scope) { query.where( cb.and( root.get( "book" ).isNotNull(), - join.isNotNull() + cb.fk( root.get( "book" ) ).isNotNull() ) ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/hql/SingleSelectionArrayResultTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/hql/SingleSelectionArrayResultTest.java new file mode 100644 index 000000000000..fc03650e1fc4 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/hql/SingleSelectionArrayResultTest.java @@ -0,0 +1,157 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.hql; + +import java.util.stream.Stream; + +import org.hibernate.dialect.H2Dialect; +import org.hibernate.dialect.PostgreSQLDialect; +import org.hibernate.query.NativeQuery; +import org.hibernate.query.Query; +import org.hibernate.query.SelectionQuery; + +import org.hibernate.testing.orm.domain.gambit.BasicEntity; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.RequiresDialect; +import org.hibernate.testing.orm.junit.RequiresDialects; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Marco Belladelli + */ +@DomainModel(annotatedClasses = BasicEntity.class) +@SessionFactory +@Jira("https://hibernate.atlassian.net/browse/HHH-18450") +@Jira("https://hibernate.atlassian.net/browse/HHH-19472") +@RequiresDialects({@RequiresDialect(H2Dialect.class), @RequiresDialect(PostgreSQLDialect.class)}) +public class SingleSelectionArrayResultTest { + + static class TestArguments implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext extensionContext) { + return Stream.of( + Arguments.of( "select 1", null, null ), + Arguments.of( "select cast(1 as integer)", null, null ), + Arguments.of( "select id from BasicEntity", null, null ), + Arguments.of( "select cast(id as integer) from BasicEntity", null, null ), + Arguments.of( "select ?1", 1, 1 ), + Arguments.of( "select :p1", "p1", 1 ), + Arguments.of( "select cast(?1 as integer)", 1, 1 ), + Arguments.of( "select cast(:p1 as integer)", "p1", 1 ) + ); + } + } + + @ParameterizedTest + @ArgumentsSource(TestArguments.class) + public void testQueryObjectResult(String ql, Object arg1, Object arg2, SessionFactoryScope scope) { + scope.inTransaction( session -> { + Query query = session.createQuery( ql, Object.class ); + if ( arg1 instanceof Integer ) { + query.setParameter( (Integer) arg1, arg2 ); + } + if ( arg1 instanceof String ) { + query.setParameter( (String) arg1, arg2 ); + } + assertThat( query.getSingleResult() ).isInstanceOf( Integer.class ).isEqualTo( 1 ); + } ); + } + + @ParameterizedTest + @ArgumentsSource(TestArguments.class) + public void testNativeQueryObjectResult(String ql, Object arg1, Object arg2, SessionFactoryScope scope) { + scope.inTransaction( session -> { + NativeQuery query = session.createNativeQuery( ql, Object.class ); + if ( arg1 instanceof Integer ) { + query.setParameter( (Integer) arg1, arg2 ); + } + if ( arg1 instanceof String ) { + query.setParameter( (String) arg1, arg2 ); + } + assertThat( query.getSingleResult() ).isInstanceOf( Integer.class ).isEqualTo( 1 ); + } ); + } + + @ParameterizedTest + @ArgumentsSource(TestArguments.class) + public void testSelectionQueryObjectResult(String ql, Object arg1, Object arg2, SessionFactoryScope scope) { + scope.inTransaction( session -> { + SelectionQuery query = session.createSelectionQuery( ql, Object.class ); + if ( arg1 instanceof Integer ) { + query.setParameter( (Integer) arg1, arg2 ); + } + if ( arg1 instanceof String ) { + query.setParameter( (String) arg1, arg2 ); + } + assertThat( query.getSingleResult() ).isInstanceOf( Integer.class ).isEqualTo( 1 ); + } ); + } + + @ParameterizedTest + @ArgumentsSource(TestArguments.class) + public void testQueryArrayResult(String ql, Object arg1, Object arg2, SessionFactoryScope scope) { + scope.inTransaction( session -> { + Query query = session.createQuery( ql, Object[].class ); + if ( arg1 instanceof Integer ) { + query.setParameter( (Integer) arg1, arg2 ); + } + if ( arg1 instanceof String ) { + query.setParameter( (String) arg1, arg2 ); + } + assertThat( query.getSingleResult() ).containsExactly( 1 ); + } ); + } + + @ParameterizedTest + @ArgumentsSource(TestArguments.class) + public void testNativeQueryArrayResult(String ql, Object arg1, Object arg2, SessionFactoryScope scope) { + scope.inTransaction( session -> { + NativeQuery query = session.createNativeQuery( ql, Object[].class ); + if ( arg1 instanceof Integer ) { + query.setParameter( (Integer) arg1, arg2 ); + } + if ( arg1 instanceof String ) { + query.setParameter( (String) arg1, arg2 ); + } + assertThat( query.getSingleResult() ).containsExactly( 1 ); + } ); + } + + @ParameterizedTest + @ArgumentsSource(TestArguments.class) + public void testSelectionQueryArrayResult(String ql, Object arg1, Object arg2, SessionFactoryScope scope) { + scope.inTransaction( session -> { + SelectionQuery query = session.createSelectionQuery( ql, Object[].class ); + if ( arg1 instanceof Integer ) { + query.setParameter( (Integer) arg1, arg2 ); + } + if ( arg1 instanceof String ) { + query.setParameter( (String) arg1, arg2 ); + } + assertThat( query.getSingleResult() ).containsExactly( 1 ); + } ); + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> session.persist( new BasicEntity( 1, "entity_1" ) ) ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/id/array/PrimitiveByteArrayIdTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/id/array/PrimitiveByteArrayIdTest.java index fdc58d6266b5..6acf9d91f470 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/id/array/PrimitiveByteArrayIdTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/id/array/PrimitiveByteArrayIdTest.java @@ -13,6 +13,7 @@ import jakarta.persistence.Id; import jakarta.persistence.Table; +import org.hibernate.community.dialect.InformixDialect; import org.hibernate.dialect.MySQLDialect; import org.hibernate.dialect.OracleDialect; import org.hibernate.query.Query; @@ -35,6 +36,7 @@ */ @SkipForDialect(dialectClass = MySQLDialect.class, majorVersion = 5, reason = "BLOB/TEXT column 'id' used in key specification without a key length") @SkipForDialect(dialectClass = OracleDialect.class, matchSubTypes = true, reason = "ORA-02329: column of datatype LOB cannot be unique or a primary key") +@SkipForDialect(dialectClass = InformixDialect.class, reason = "Informix does not support unique / primary constraints on binary columns") @DomainModel( annotatedClasses = PrimitiveByteArrayIdTest.DemoEntity.class ) diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/id/enhanced/SequenceStyleConfigUnitTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/id/enhanced/SequenceStyleConfigUnitTest.java index 37a91093698a..34c39525ec05 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/id/enhanced/SequenceStyleConfigUnitTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/id/enhanced/SequenceStyleConfigUnitTest.java @@ -20,6 +20,7 @@ import org.hibernate.dialect.sequence.ANSISequenceSupport; import org.hibernate.dialect.sequence.SequenceSupport; import org.hibernate.engine.jdbc.spi.JdbcServices; +import org.hibernate.generator.GeneratorCreationContext; import org.hibernate.id.OptimizableGenerator; import org.hibernate.id.PersistentIdentifierGenerator; import org.hibernate.id.enhanced.DatabaseStructure; @@ -33,6 +34,9 @@ import org.hibernate.id.enhanced.SequenceStyleGenerator; import org.hibernate.id.enhanced.StandardOptimizerDescriptor; import org.hibernate.id.enhanced.TableStructure; +import org.hibernate.mapping.PersistentClass; +import org.hibernate.mapping.Property; +import org.hibernate.service.ServiceRegistry; import org.hibernate.type.StandardBasicTypes; import org.hibernate.type.spi.TypeConfiguration; @@ -70,8 +74,11 @@ public void testDefaultedSequenceBackedConfiguration() { () -> buildingContext.getBootstrapContext().getTypeConfiguration(), serviceRegistry ); + Database database = new Database( buildingContext.getBuildingOptions() ); + GeneratorCreationContextImpl generatorCreationContext = new GeneratorCreationContextImpl( database ); Properties props = buildGeneratorPropertiesBase( buildingContext ); SequenceStyleGenerator generator = new SequenceStyleGenerator(); + generator.create( generatorCreationContext ); generator.configure( new TypeConfiguration().getBasicTypeRegistry() .resolve( StandardBasicTypes.LONG ), @@ -79,7 +86,6 @@ public void testDefaultedSequenceBackedConfiguration() { serviceRegistry ); - Database database = new Database( buildingContext.getBuildingOptions() ); generator.registerExportables( database ); generator.initialize( SqlStringGenerationContextImpl.forTests( database.getJdbcEnvironment() ) ); @@ -120,8 +126,11 @@ public void testDefaultedTableBackedConfiguration() { () -> buildingContext.getBootstrapContext().getTypeConfiguration(), serviceRegistry ); + Database database = new Database( buildingContext.getBuildingOptions() ); + GeneratorCreationContextImpl generatorCreationContext = new GeneratorCreationContextImpl( database ); Properties props = buildGeneratorPropertiesBase( buildingContext ); SequenceStyleGenerator generator = new SequenceStyleGenerator(); + generator.create( generatorCreationContext ); generator.configure( new TypeConfiguration().getBasicTypeRegistry() .resolve( StandardBasicTypes.LONG ), @@ -129,7 +138,6 @@ public void testDefaultedTableBackedConfiguration() { serviceRegistry ); - Database database = new Database( buildingContext.getBuildingOptions() ); generator.registerExportables( database ); generator.initialize( SqlStringGenerationContextImpl.forTests( database.getJdbcEnvironment() ) ); @@ -160,10 +168,15 @@ public void testDefaultOptimizerBasedOnIncrementBackedBySequence() { () -> buildingContext.getBootstrapContext().getTypeConfiguration(), serviceRegistry ); + + Database database = new Database( buildingContext.getBuildingOptions() ); + GeneratorCreationContextImpl generatorCreationContext = new GeneratorCreationContextImpl( database ); + Properties props = buildGeneratorPropertiesBase( buildingContext ); props.setProperty( SequenceStyleGenerator.INCREMENT_PARAM, "10" ); SequenceStyleGenerator generator = new SequenceStyleGenerator(); + generator.create( generatorCreationContext ); generator.configure( new TypeConfiguration().getBasicTypeRegistry() .resolve( StandardBasicTypes.LONG ), @@ -171,7 +184,6 @@ public void testDefaultOptimizerBasedOnIncrementBackedBySequence() { serviceRegistry ); - Database database = new Database( buildingContext.getBuildingOptions() ); generator.registerExportables( database ); generator.initialize( SqlStringGenerationContextImpl.forTests( database.getJdbcEnvironment() ) ); @@ -190,17 +202,20 @@ public void testDefaultOptimizerBasedOnIncrementBackedBySequence() { () -> buildingContext.getBootstrapContext().getTypeConfiguration(), serviceRegistry ); + Database database = new Database( buildingContext.getBuildingOptions() ); + GeneratorCreationContextImpl generatorCreationContext = new GeneratorCreationContextImpl( database ); + Properties props = buildGeneratorPropertiesBase( buildingContext ); props.setProperty( SequenceStyleGenerator.INCREMENT_PARAM, "10" ); SequenceStyleGenerator generator = new SequenceStyleGenerator(); + generator.create( generatorCreationContext ); generator.configure( new TypeConfiguration().getBasicTypeRegistry() .resolve( StandardBasicTypes.LONG ), props, serviceRegistry ); - Database database = new Database( buildingContext.getBuildingOptions() ); generator.registerExportables( database ); generator.initialize( SqlStringGenerationContextImpl.forTests( database.getJdbcEnvironment() ) ); @@ -226,17 +241,20 @@ public void testDefaultOptimizerBasedOnIncrementBackedByTable() { () -> buildingContext.getBootstrapContext().getTypeConfiguration(), serviceRegistry ); + Database database = new Database( buildingContext.getBuildingOptions() ); + GeneratorCreationContextImpl generatorCreationContext = new GeneratorCreationContextImpl( database ); + Properties props = buildGeneratorPropertiesBase( buildingContext ); props.setProperty( SequenceStyleGenerator.INCREMENT_PARAM, "10" ); SequenceStyleGenerator generator = new SequenceStyleGenerator(); + generator.create( generatorCreationContext ); generator.configure( new TypeConfiguration().getBasicTypeRegistry() .resolve( StandardBasicTypes.LONG ), props, serviceRegistry ); - Database database = new Database( buildingContext.getBuildingOptions() ); generator.registerExportables( database ); generator.initialize( SqlStringGenerationContextImpl.forTests( database.getJdbcEnvironment() ) ); @@ -260,17 +278,19 @@ public void testForceTableUse() { () -> buildingContext.getBootstrapContext().getTypeConfiguration(), serviceRegistry ); + Database database = new Database( buildingContext.getBuildingOptions() ); + GeneratorCreationContextImpl generatorCreationContext = new GeneratorCreationContextImpl( database ); Properties props = buildGeneratorPropertiesBase( buildingContext ); props.setProperty( SequenceStyleGenerator.FORCE_TBL_PARAM, "true" ); SequenceStyleGenerator generator = new SequenceStyleGenerator(); + generator.create( generatorCreationContext ); generator.configure( new TypeConfiguration().getBasicTypeRegistry() .resolve( StandardBasicTypes.LONG ), props, serviceRegistry ); - Database database = new Database( buildingContext.getBuildingOptions() ); generator.registerExportables( database ); generator.initialize( SqlStringGenerationContextImpl.forTests( database.getJdbcEnvironment() ) ); @@ -285,6 +305,44 @@ public void testForceTableUse() { } } + private static class GeneratorCreationContextImpl implements GeneratorCreationContext { + private final Database database; + + public GeneratorCreationContextImpl(Database database) { + this.database = database; + } + + @Override + public Database getDatabase() { + return database; + } + + @Override + public ServiceRegistry getServiceRegistry() { + return database.getServiceRegistry(); + } + + @Override + public String getDefaultCatalog() { + throw new UnsupportedOperationException(); + } + + @Override + public String getDefaultSchema() { + throw new UnsupportedOperationException(); + } + + @Override + public PersistentClass getPersistentClass() { + throw new UnsupportedOperationException(); + } + + @Override + public Property getProperty() { + throw new UnsupportedOperationException(); + } + } + /** * Test explicitly specifying both optimizer and increment */ @@ -302,14 +360,18 @@ public void testExplicitOptimizerWithExplicitIncrementSize() { Properties props = buildGeneratorPropertiesBase( buildingContext ); props.setProperty( SequenceStyleGenerator.OPT_PARAM, StandardOptimizerDescriptor.NONE.getExternalName() ); props.setProperty( SequenceStyleGenerator.INCREMENT_PARAM, "20" ); + + Database database = new Database( buildingContext.getBuildingOptions() ); + GeneratorCreationContextImpl generatorCreationContext = new GeneratorCreationContextImpl( database ); + SequenceStyleGenerator generator = new SequenceStyleGenerator(); + generator.create( generatorCreationContext ); generator.configure( new TypeConfiguration().getBasicTypeRegistry() .resolve( StandardBasicTypes.LONG ), props, serviceRegistry ); - Database database = new Database( buildingContext.getBuildingOptions() ); generator.registerExportables( database ); generator.initialize( SqlStringGenerationContextImpl.forTests( database.getJdbcEnvironment() ) ); @@ -323,6 +385,7 @@ public void testExplicitOptimizerWithExplicitIncrementSize() { props.setProperty( SequenceStyleGenerator.OPT_PARAM, StandardOptimizerDescriptor.HILO.getExternalName() ); props.setProperty( SequenceStyleGenerator.INCREMENT_PARAM, "20" ); generator = new SequenceStyleGenerator(); + generator.create( generatorCreationContext ); generator.configure( new TypeConfiguration().getBasicTypeRegistry() .resolve( StandardBasicTypes.LONG ), @@ -341,6 +404,7 @@ public void testExplicitOptimizerWithExplicitIncrementSize() { props.setProperty( SequenceStyleGenerator.OPT_PARAM, StandardOptimizerDescriptor.POOLED.getExternalName() ); props.setProperty( SequenceStyleGenerator.INCREMENT_PARAM, "20" ); generator = new SequenceStyleGenerator(); + generator.create( generatorCreationContext ); generator.configure( new TypeConfiguration().getBasicTypeRegistry() .resolve( StandardBasicTypes.LONG ), @@ -368,16 +432,19 @@ public void testPreferredPooledOptimizerSetting() { () -> buildingContext.getBootstrapContext().getTypeConfiguration(), serviceRegistry ); + Database database = new Database( buildingContext.getBuildingOptions() ); + GeneratorCreationContextImpl generatorCreationContext = new GeneratorCreationContextImpl( database ); + Properties props = buildGeneratorPropertiesBase( buildingContext ); props.setProperty( SequenceStyleGenerator.INCREMENT_PARAM, "20" ); SequenceStyleGenerator generator = new SequenceStyleGenerator(); + generator.create( generatorCreationContext ); generator.configure( new TypeConfiguration().getBasicTypeRegistry() .resolve( StandardBasicTypes.LONG ), props, serviceRegistry ); - Database database = new Database( buildingContext.getBuildingOptions() ); generator.registerExportables( database ); generator.initialize( SqlStringGenerationContextImpl.forTests( database.getJdbcEnvironment() ) ); assertClassAssignability( SequenceStructure.class, generator.getDatabaseStructure().getClass() ); @@ -385,6 +452,7 @@ public void testPreferredPooledOptimizerSetting() { props.setProperty( Environment.PREFERRED_POOLED_OPTIMIZER, StandardOptimizerDescriptor.POOLED_LO.getExternalName() ); generator = new SequenceStyleGenerator(); + generator.create( generatorCreationContext ); generator.configure( new TypeConfiguration().getBasicTypeRegistry() .resolve( StandardBasicTypes.LONG ), @@ -398,6 +466,7 @@ public void testPreferredPooledOptimizerSetting() { props.setProperty( Environment.PREFERRED_POOLED_OPTIMIZER, StandardOptimizerDescriptor.POOLED_LOTL.getExternalName() ); generator = new SequenceStyleGenerator(); + generator.create( generatorCreationContext ); generator.configure( new TypeConfiguration().getBasicTypeRegistry() .resolve( StandardBasicTypes.LONG ), diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/id/idClass/IdClassSyntheticAttributesTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/id/idClass/IdClassSyntheticAttributesTest.java new file mode 100644 index 000000000000..22fb93f9149b --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/id/idClass/IdClassSyntheticAttributesTest.java @@ -0,0 +1,36 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.id.idClass; + +import org.hibernate.mapping.PersistentClass; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.DomainModelScope; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel( + annotatedClasses = MyEntity.class +) +@SessionFactory +public class IdClassSyntheticAttributesTest { + + @Jira("https://hibernate.atlassian.net/browse/HHH-18841") + @Test + public void test(DomainModelScope scope) { + final PersistentClass entityBinding = scope.getDomainModel().getEntityBinding(MyEntity.class.getName()); + assertThat(entityBinding.getProperties()).hasSize(2) + .anySatisfy(p -> { + assertThat(p.isSynthetic()).isTrue(); + assertThat(p.getName()).isEqualTo("_identifierMapper"); + }) + .anySatisfy(p -> { + assertThat(p.isSynthetic()).isFalse(); + assertThat(p.getName()).isEqualTo("notes"); + }); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/id/sequence/PooledWithCustomNamingStrategyTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/id/sequence/PooledWithCustomNamingStrategyTest.java new file mode 100644 index 000000000000..9d1cb8c7c19a --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/id/sequence/PooledWithCustomNamingStrategyTest.java @@ -0,0 +1,167 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.id.sequence; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; + +import org.hibernate.SessionFactory; +import org.hibernate.boot.MetadataSources; +import org.hibernate.boot.model.naming.Identifier; +import org.hibernate.boot.model.naming.PhysicalNamingStrategy; +import org.hibernate.boot.registry.StandardServiceRegistryBuilder; +import org.hibernate.boot.spi.MetadataImplementor; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.cfg.Environment; +import org.hibernate.dialect.H2Dialect; +import org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl; +import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider; +import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.internal.util.PropertiesHelper; + +import org.hibernate.testing.orm.junit.BaseUnitTest; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.RequiresDialect; +import org.hibernate.testing.util.ServiceRegistryUtil; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.SequenceGenerator; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hibernate.testing.transaction.TransactionUtil2.inTransaction; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * @author Marco Belladelli + */ +@BaseUnitTest +@RequiresDialect( H2Dialect.class ) +@Jira( "https://hibernate.atlassian.net/browse/HHH-18337" ) +public class PooledWithCustomNamingStrategyTest { + @Test + public void testWrongIncrementSize() { + final StandardServiceRegistryBuilder registryBuilder = ServiceRegistryUtil.serviceRegistryBuilder(); + registryBuilder.applySetting( AvailableSettings.PHYSICAL_NAMING_STRATEGY, MyPhysicalNamingStrategy.INSTANCE ); + final MetadataImplementor metadata = (MetadataImplementor) new MetadataSources( registryBuilder.build() ) + .addAnnotatedClass( WrongEntity.class ) + .buildMetadata(); + try (final SessionFactory sf = metadata.buildSessionFactory()) { + fail( "Default increment size of [50] should not work with the database sequence increment size [1]." ); + } + catch (Exception e) { + assertThat( e.getCause().getMessage() ).isEqualTo( + "The increment size of the [MY_SEQ] sequence is set to [50] in the entity " + + "mapping while the associated database sequence increment size is [1]." + ); + } + } + + @Test + public void testRightIncrementSize() { + final StandardServiceRegistryBuilder registryBuilder = ServiceRegistryUtil.serviceRegistryBuilder(); + registryBuilder.applySetting( AvailableSettings.PHYSICAL_NAMING_STRATEGY, MyPhysicalNamingStrategy.INSTANCE ); + final MetadataImplementor metadata = (MetadataImplementor) new MetadataSources( registryBuilder.build() ) + .addAnnotatedClass( RightEntity.class ) + .buildMetadata(); + try (final SessionFactoryImplementor sf = (SessionFactoryImplementor) metadata.buildSessionFactory()) { + // session factory should be created correctly + inTransaction( sf, session -> session.persist( new RightEntity() ) ); + inTransaction( sf, session -> { + final RightEntity result = session.createQuery( "from RightEntity", RightEntity.class ) + .getSingleResult(); + assertThat( result.id ).isNotNull(); + } ); + } + catch (Exception e) { + fail( "Expected configured increment size of [1] to be compatible with the existing sequence" ); + } + } + + ConnectionProvider connectionProvider; + + @BeforeAll + public void setUp() { + final DriverManagerConnectionProviderImpl provider = new DriverManagerConnectionProviderImpl(); + provider.configure( PropertiesHelper.map( Environment.getProperties() ) ); + connectionProvider = provider; + try (final Connection connection = connectionProvider.getConnection(); + final Statement statement = connection.createStatement()) { + statement.execute( "create sequence MY_SEQ start with 1 increment by 1" ); + statement.execute( "create table RightEntity(id bigint not null, primary key (id))" ); + } + catch (SQLException e) { + throw new RuntimeException( "Failed to setup the test", e ); + } + } + + @AfterAll + public void tearDown() { + try (final Connection connection = connectionProvider.getConnection(); + final Statement statement = connection.createStatement()) { + statement.execute( "drop sequence MY_SEQ" ); + statement.execute( "drop table RightEntity" ); + } + catch (SQLException e) { + } + } + + @Entity( name = "WrongEntity" ) + static class WrongEntity { + @Id + @GeneratedValue( strategy = GenerationType.SEQUENCE, generator = "my-sequence-generator" ) + @SequenceGenerator( name = "my-sequence-generator", sequenceName = "REPLACE_SEQ" ) + private Long id; + } + + @Entity( name = "RightEntity" ) + static class RightEntity { + @Id + @GeneratedValue( strategy = GenerationType.SEQUENCE, generator = "my-sequence-generator" ) + @SequenceGenerator( name = "my-sequence-generator", sequenceName = "REPLACE_SEQ", allocationSize = 1 ) + private Long id; + } + + static class MyPhysicalNamingStrategy implements PhysicalNamingStrategy { + static MyPhysicalNamingStrategy INSTANCE = new MyPhysicalNamingStrategy(); + + @Override + public Identifier toPhysicalCatalogName(Identifier logicalName, JdbcEnvironment jdbcEnvironment) { + return logicalName; + } + + @Override + public Identifier toPhysicalSchemaName(Identifier logicalName, JdbcEnvironment jdbcEnvironment) { + return logicalName; + } + + @Override + public Identifier toPhysicalTableName(Identifier logicalName, JdbcEnvironment jdbcEnvironment) { + return logicalName; + } + + @Override + public Identifier toPhysicalSequenceName(Identifier logicalName, JdbcEnvironment jdbcEnvironment) { + return Identifier.toIdentifier( + logicalName.getText().replaceAll( "REPLACE_", "MY_" ), + logicalName.isQuoted() + ); + } + + @Override + public Identifier toPhysicalColumnName(Identifier logicalName, JdbcEnvironment jdbcEnvironment) { + return logicalName; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/id/uuid/sqlrep/sqlbinary/UUIDBinaryTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/id/uuid/sqlrep/sqlbinary/UUIDBinaryTest.java index e59b38b77e4a..9f12fa719818 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/id/uuid/sqlrep/sqlbinary/UUIDBinaryTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/id/uuid/sqlrep/sqlbinary/UUIDBinaryTest.java @@ -10,6 +10,7 @@ import java.util.UUID; import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.community.dialect.InformixDialect; import org.hibernate.dialect.PostgreSQLDialect; import org.hibernate.dialect.SybaseDialect; import org.hibernate.metamodel.MappingMetamodel; @@ -39,6 +40,7 @@ @SkipForDialect(dialectClass = PostgreSQLDialect.class, reason = "Postgres has its own UUID type") @SkipForDialect( dialectClass = SybaseDialect.class, matchSubTypes = true, reason = "Skipped for Sybase to avoid problems with UUIDs potentially ending with a trailing 0 byte") +@SkipForDialect(dialectClass = InformixDialect.class, reason = "Informix does not support unique / primary constraints on binary columns") public class UUIDBinaryTest { private static class UUIDPair { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/idclass/mappedsuperclass/BaseSummary.java b/hibernate-core/src/test/java/org/hibernate/orm/test/idclass/mappedsuperclass/BaseSummary.java new file mode 100644 index 000000000000..822bd83011df --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/idclass/mappedsuperclass/BaseSummary.java @@ -0,0 +1,62 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.idclass.mappedsuperclass; + +import jakarta.persistence.Id; +import jakarta.persistence.IdClass; +import jakarta.persistence.MappedSuperclass; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Objects; + +@IdClass(PKey.class) +@MappedSuperclass +public class BaseSummary implements Serializable { + + @Id + private Integer year; + @Id + private Integer month; + private BigDecimal value; + + public Integer getYear() { + return year; + } + + public void setYear(Integer year) { + this.year = year; + } + + public Integer getMonth() { + return month; + } + + public void setMonth(Integer month) { + this.month = month; + } + + public BigDecimal getValue() { + return value; + } + + public void setValue(BigDecimal value) { + this.value = value; + } + + @Override + public boolean equals(Object o) { + if ( o == null || getClass() != o.getClass() ) { + return false; + } + BaseSummary that = (BaseSummary) o; + return Objects.equals( year, that.year ) && Objects.equals( month, that.month ); + } + + @Override + public int hashCode() { + return Objects.hash( year, month ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/idclass/mappedsuperclass/MappedSuperclassIdClassAttributesTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/idclass/mappedsuperclass/MappedSuperclassIdClassAttributesTest.java new file mode 100644 index 000000000000..9782f919a4d3 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/idclass/mappedsuperclass/MappedSuperclassIdClassAttributesTest.java @@ -0,0 +1,32 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.idclass.mappedsuperclass; + +import jakarta.persistence.metamodel.SingularAttribute; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel(annotatedClasses = {Summary.class, BaseSummary.class}) +@SessionFactory +@JiraKey("HHH-18858") +public class MappedSuperclassIdClassAttributesTest { + @Test + public void test(SessionFactoryScope scope) { + scope.inSession( entityManager -> { + final var yearAttribute = Summary_.year.getDeclaringType().getAttribute( "year" ); + assertThat( yearAttribute ).isEqualTo( Summary_.year ); + assertThat( ((SingularAttribute) yearAttribute).isId() ).isTrue(); + + final var monthAttribute = Summary_.month.getDeclaringType().getAttribute( "month" ); + assertThat( monthAttribute ).isEqualTo( Summary_.month ); + assertThat( ((SingularAttribute) monthAttribute).isId() ).isTrue(); + } ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/idclass/mappedsuperclass/PKey.java b/hibernate-core/src/test/java/org/hibernate/orm/test/idclass/mappedsuperclass/PKey.java new file mode 100644 index 000000000000..6757d23edcc4 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/idclass/mappedsuperclass/PKey.java @@ -0,0 +1,44 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.idclass.mappedsuperclass; + +import java.io.Serializable; +import java.util.Objects; + +public class PKey implements Serializable { + + private Integer year; + private Integer month; + + public Integer getYear() { + return year; + } + + public void setYear(Integer year) { + this.year = year; + } + + public Integer getMonth() { + return month; + } + + public void setMonth(Integer month) { + this.month = month; + } + + @Override + public boolean equals(Object o) { + if ( o == null || getClass() != o.getClass() ) { + return false; + } + PKey pKey = (PKey) o; + return Objects.equals( year, pKey.year ) && Objects.equals( month, pKey.month ); + } + + @Override + public int hashCode() { + return Objects.hash( year, month ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/idclass/mappedsuperclass/Summary.java b/hibernate-core/src/test/java/org/hibernate/orm/test/idclass/mappedsuperclass/Summary.java new file mode 100644 index 000000000000..108602a2ce16 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/idclass/mappedsuperclass/Summary.java @@ -0,0 +1,11 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.idclass.mappedsuperclass; + +import jakarta.persistence.Entity; + +@Entity +public class Summary extends BaseSummary { +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/idgen/userdefined/BeforeExecutionAssignedValuesTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/idgen/userdefined/BeforeExecutionAssignedValuesTest.java new file mode 100644 index 000000000000..04f95fd62585 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/idgen/userdefined/BeforeExecutionAssignedValuesTest.java @@ -0,0 +1,258 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.idgen.userdefined; + +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import org.hibernate.annotations.IdGeneratorType; +import org.hibernate.annotations.ValueGenerationType; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.generator.BeforeExecutionGenerator; +import org.hibernate.generator.EventType; +import org.hibernate.generator.EventTypeSets; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.util.EnumSet; +import java.util.UUID; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import static org.assertj.core.api.Assertions.assertThat; + +@SessionFactory +@DomainModel(annotatedClasses = { + BeforeExecutionAssignedValuesTest.EntityWithGeneratedId.class, + BeforeExecutionAssignedValuesTest.GeneratedCompositeId.class, + BeforeExecutionAssignedValuesTest.EntityWithGeneratedEmbeddedId.class, + BeforeExecutionAssignedValuesTest.EntityWithGeneratedProperty.class, +}) +@Jira("https://hibernate.atlassian.net/browse/HHH-19320") +class BeforeExecutionAssignedValuesTest { + @Test + void testAssignedId(SessionFactoryScope scope) { + final EntityWithGeneratedId entity1 = new EntityWithGeneratedId( "assigned-id", "assigned-entity" ); + scope.inTransaction( session -> session.persist( entity1 ) ); + assertThat( entity1.getGeneratedId() ).isEqualTo( "assigned-id" ); + + final EntityWithGeneratedId entity2 = new EntityWithGeneratedId( "stateless-id", "stateless-entity" ); + scope.inStatelessTransaction( session -> session.insert( entity2 ) ); + assertThat( entity2.getGeneratedId() ).isEqualTo( "stateless-id" ); + } + + @Test + void testGeneratedId(SessionFactoryScope scope) { + final EntityWithGeneratedId entity = new EntityWithGeneratedId( null, "assigned-entity" ); + scope.inTransaction( session -> session.persist( entity ) ); + assertThat( entity.getGeneratedId() ).isNotNull(); + } + + @Test + void testAssignedEmbeddedId(SessionFactoryScope scope) { + final EntityWithGeneratedEmbeddedId entity1 = new EntityWithGeneratedEmbeddedId( + new GeneratedCompositeId( "assigned-1", null), + "generated-entity" + ); + scope.inTransaction( session -> session.persist( entity1 ) ); + assertThat( entity1.getId().getId1() ).isEqualTo( "assigned-1" ); + assertThat( entity1.getId().getId2() ).isNotNull(); + + final EntityWithGeneratedEmbeddedId entity2 = new EntityWithGeneratedEmbeddedId( + new GeneratedCompositeId( "new-assigned-1", "assigned-2"), + "generated-entity" + ); + scope.inTransaction( session -> session.persist( entity2 ) ); + assertThat( entity2.getId().getId1() ).isEqualTo( "new-assigned-1" ); + assertThat( entity2.getId().getId2() ).isEqualTo( "assigned-2" ); + } + + @Test + void testGeneratedEmbeddedId(SessionFactoryScope scope) { + final EntityWithGeneratedEmbeddedId entity = new EntityWithGeneratedEmbeddedId( + new GeneratedCompositeId(), + "generated-entity" + ); + scope.inTransaction( session -> session.persist( entity ) ); + assertThat( entity.getId().getId1() ).isNotNull(); + assertThat( entity.getId().getId2() ).isNotNull(); + } + + @Test + void testInsertAssignedProperty(SessionFactoryScope scope) { + final String assigned = "assigned-property"; + final EntityWithGeneratedProperty entity = new EntityWithGeneratedProperty( 1L, assigned ); + scope.inTransaction( session -> session.persist( entity ) ); + assertThat( entity.getGeneratedProperty() ).isEqualTo( assigned ); + } + + @Test + void testGeneratedPropertyAndUpdate(SessionFactoryScope scope) { + final EntityWithGeneratedProperty entity = new EntityWithGeneratedProperty( 2L, null ); + scope.inTransaction( session -> { + session.persist( entity ); + session.flush(); + + assertThat( entity.getGeneratedProperty() ).isNotNull(); + + // test update + entity.setGeneratedProperty( "new-assigned-property" ); + } ); + + assertThat( entity.getGeneratedProperty() ).isEqualTo( "new-assigned-property" ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Entity(name = "EntityWithGeneratedId") + static class EntityWithGeneratedId { + @Id + @GeneratedValue + @AssignableGenerator + private String generatedId; + + private String name; + + public EntityWithGeneratedId() { + } + + public EntityWithGeneratedId(String generatedId, String name) { + this.generatedId = generatedId; + this.name = name; + } + + public String getGeneratedId() { + return generatedId; + } + + public String getName() { + return name; + } + } + + @Embeddable + static class GeneratedCompositeId { + @AssignableGenerator + private String id1; + + @AssignableGenerator + private String id2; + + public GeneratedCompositeId() { + } + + public GeneratedCompositeId(String id1, String id2) { + this.id1 = id1; + this.id2 = id2; + } + + public String getId1() { + return id1; + } + + public String getId2() { + return id2; + } + } + + @Entity(name = "EntityWithGeneratedEmbeddedId") + static class EntityWithGeneratedEmbeddedId { + @EmbeddedId + private GeneratedCompositeId id; + + private String name; + + public EntityWithGeneratedEmbeddedId() { + } + + public EntityWithGeneratedEmbeddedId(GeneratedCompositeId id, String name) { + this.id = id; + this.name = name; + } + + public GeneratedCompositeId getId() { + return id; + } + } + + @Entity(name = "EntityWithGeneratedProperty") + static class EntityWithGeneratedProperty { + @Id + private Long id; + + @AssignableGenerator + private String generatedProperty; + + public EntityWithGeneratedProperty() { + } + + public EntityWithGeneratedProperty(Long id, String generatedProperty) { + this.id = id; + this.generatedProperty = generatedProperty; + } + + public Long getId() { + return id; + } + + public String getGeneratedProperty() { + return generatedProperty; + } + + public void setGeneratedProperty(String generatedProperty) { + this.generatedProperty = generatedProperty; + } + } + + @IdGeneratorType(AssignedIdGenerator.class) + @ValueGenerationType(generatedBy = AssignedGenerator.class) + @Retention(RUNTIME) + @Target({FIELD, METHOD}) + @interface AssignableGenerator { + } + + public static class AssignedGenerator implements BeforeExecutionGenerator { + @Override + public Object generate( + SharedSessionContractImplementor session, + Object owner, + Object currentValue, + EventType eventType) { + if ( currentValue != null ) { + return currentValue; + } + return UUID.randomUUID().toString(); + } + + @Override + public EnumSet getEventTypes() { + return EventTypeSets.INSERT_AND_UPDATE; + } + } + + public static class AssignedIdGenerator extends AssignedGenerator { + @Override + public EnumSet getEventTypes() { + return EventTypeSets.INSERT_ONLY; + } + + @Override + public boolean allowAssignedIdentifiers() { + return true; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/idgen/userdefined/NativeGenerator.java b/hibernate-core/src/test/java/org/hibernate/orm/test/idgen/userdefined/NativeGenerator.java index 6fb8a192a3f8..f2b4e9190a5d 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/idgen/userdefined/NativeGenerator.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/idgen/userdefined/NativeGenerator.java @@ -8,12 +8,15 @@ import org.hibernate.generator.BeforeExecutionGenerator; import org.hibernate.generator.EventType; import org.hibernate.generator.Generator; +import org.hibernate.generator.GeneratorCreationContext; import org.hibernate.generator.OnExecutionGenerator; import org.hibernate.id.Configurable; import org.hibernate.id.PostInsertIdentityPersister; import org.hibernate.id.factory.IdentifierGeneratorFactory; import org.hibernate.id.factory.spi.CustomIdGeneratorCreationContext; import org.hibernate.id.insert.InsertGeneratedIdentifierDelegate; +import org.hibernate.mapping.PersistentClass; +import org.hibernate.mapping.Property; import org.hibernate.service.ServiceRegistry; import org.hibernate.type.Type; @@ -26,10 +29,12 @@ public class NativeGenerator private final IdentifierGeneratorFactory factory; private final String strategy; + private final CustomIdGeneratorCreationContext creationContext; private Generator generator; public NativeGenerator(NativeId nativeId, Member member, CustomIdGeneratorCreationContext creationContext) { + this.creationContext = creationContext; factory = creationContext.getIdentifierGeneratorFactory(); strategy = creationContext.getDatabase().getDialect().getNativeIdentifierGeneratorStrategy(); if ( "identity".equals(strategy) ) { @@ -49,7 +54,12 @@ public boolean generatedOnExecution() { @Override public void configure(Type type, Properties parameters, ServiceRegistry serviceRegistry) { - generator = factory.createIdentifierGenerator(strategy, type, parameters); + generator = factory.createIdentifierGenerator( + strategy, + type, + creationContext, + parameters + ); //TODO: should use this instead of the deprecated method, but see HHH-18135 // GenerationType generationType; // switch (strategy) { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/DenormalizedTableForeignKeyGeneratorTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/DenormalizedTableForeignKeyGeneratorTest.java new file mode 100644 index 000000000000..68913d2ecc1e --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/DenormalizedTableForeignKeyGeneratorTest.java @@ -0,0 +1,95 @@ +package org.hibernate.orm.test.inheritance; + +import jakarta.persistence.*; +import org.hibernate.dialect.H2Dialect; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.Jpa; +import org.hibernate.testing.orm.junit.RequiresDialect; +import org.junit.jupiter.api.Test; + +import java.io.Serializable; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Paria Hosseiny + */ +@JiraKey("HHH-18470") +@Jpa( + annotatedClasses = { + DenormalizedTableForeignKeyGeneratorTest.Employee.class, + DenormalizedTableForeignKeyGeneratorTest.Manager.class, + DenormalizedTableForeignKeyGeneratorTest.Address.class, + DenormalizedTableForeignKeyGeneratorTest.Territory.class + } +) + +@RequiresDialect(H2Dialect.class) +public class DenormalizedTableForeignKeyGeneratorTest { + + @Test + public void shouldCreateForeignKeyForSubclasses(EntityManagerFactoryScope scope) { + scope.inTransaction( + entityManager -> { + String managerQuery = "select CONSTRAINT_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS " + + "WHERE TABLE_NAME='MANAGER' " + + "AND CONSTRAINT_TYPE='FOREIGN KEY'"; + List managerForeignKeyNames = entityManager.createNativeQuery(managerQuery).getResultList(); + + String employeeQuery = "select CONSTRAINT_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS " + + "WHERE TABLE_NAME='EMPLOYEE' " + + "AND CONSTRAINT_TYPE='FOREIGN KEY'"; + String employeeForeignKeyName = entityManager.createNativeQuery(employeeQuery).getSingleResult().toString(); + + assertThat(employeeForeignKeyName).isNotNull(); + assertThat(managerForeignKeyNames).isNotNull().hasSize(2); + assertThat(managerForeignKeyNames).doesNotContain(employeeForeignKeyName); + } + ); + } + + @Entity(name = "Employee") + @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) + static class Employee implements Serializable { + + @Id + @GeneratedValue + Long id; + + @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true) + private Address address; + + } + + @Entity(name = "Manager") + static class Manager extends Employee { + + @OneToOne(fetch = FetchType.EAGER, orphanRemoval = true, cascade = CascadeType.ALL) + private Territory territory; + } + + @Entity(name = "Address") + static class Address { + + @Id + @GeneratedValue + Long id; + + @Column(nullable = false, columnDefinition = "TEXT") + private String address = ""; + } + + @Entity(name = "Territory") + static class Territory { + + @Id + @GeneratedValue + Long id; + + @Column + private String location = ""; + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/EmbeddableInheritanceHierarchyOrderTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/EmbeddableInheritanceHierarchyOrderTest.java new file mode 100644 index 000000000000..bb04a425bdbf --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/EmbeddableInheritanceHierarchyOrderTest.java @@ -0,0 +1,243 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.inheritance; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.DiscriminatorColumn; +import jakarta.persistence.Embeddable; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +@DomainModel( + annotatedClasses = { + EmbeddableInheritanceHierarchyOrderTest.Animal.class, + EmbeddableInheritanceHierarchyOrderTest.Cat.class, + EmbeddableInheritanceHierarchyOrderTest.Dog.class, + EmbeddableInheritanceHierarchyOrderTest.Fish.class, + EmbeddableInheritanceHierarchyOrderTest.Mammal.class, + // If Mammal is moved right under Animal (before Dog and Cat), test will pass + EmbeddableInheritanceHierarchyOrderTest.Owner.class + } +) +@SessionFactory +public class EmbeddableInheritanceHierarchyOrderTest { + + @AfterAll + static void clean(SessionFactoryScope scope) { + scope.inTransaction( session -> session.createMutationQuery( "delete from Owner" ).executeUpdate() ); + } + + @Test + public void test(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.persist( new Owner( 1L, new Animal( 2, "Agapius" ) ) ); + session.persist( new Owner( 2L, new Cat( 3, "Bercharius", "Blaesilla" ) ) ); + session.persist( new Owner( 3L, new Dog( 4, "Censurius", "Caesarea" ) ) ); + session.persist( new Owner( 4L, new Fish( 5, "Dionysius", 3 ) ) ); + session.persist( new Owner( 5L, new Mammal( 6, "Epagraphas", "Eanswida" ) ) ); + } ); + scope.inSession( session -> { + final Owner animalOwner = session.find( Owner.class, 1L ); + assertEquals( 2, animalOwner.getPet().getAge() ); + assertEquals( "Agapius", animalOwner.getPet().getName() ); + + final Owner fishOwner = session.find( Owner.class, 4L ); + if ( fishOwner.getPet() instanceof Fish ) { + final Fish fish = (Fish) fishOwner.getPet(); + assertEquals( 5, fish.getAge() ); + assertEquals( "Dionysius", fish.getName() ); + assertEquals( 3, fish.getFins() ); + } + else { + fail( "Not fish owner" ); + } + + final Owner mammalOwner = session.find( Owner.class, 5L ); + if ( mammalOwner.getPet() instanceof Mammal ) { + final Mammal mammal = (Mammal) mammalOwner.getPet(); + assertEquals( 6, mammal.getAge() ); + assertEquals( "Epagraphas", mammal.getName() ); + assertEquals( "Eanswida", mammal.getMother() ); + } + else { + fail( "Not mammal owner" ); + } + + final Owner catOwner = session.find( Owner.class, 2L ); + if ( catOwner.getPet() instanceof Cat ) { + final Cat cat = (Cat) catOwner.getPet(); + assertEquals( 3, cat.getAge() ); + assertEquals( "Bercharius", cat.getName() ); + assertEquals( "Blaesilla", cat.getMother() ); + } + else { + fail( "Not cat owner" ); + } + + final Owner dogOwner = session.find( Owner.class, 3L ); + if ( dogOwner.getPet() instanceof Dog ) { + final Dog dog = (Dog) dogOwner.getPet(); + assertEquals( 4, dog.getAge() ); + assertEquals( "Censurius", dog.getName() ); + assertEquals( "Caesarea", dog.getMother() ); + } + else { + fail( "Not dog owner" ); + } + } ); + } + + @Embeddable + @DiscriminatorColumn(name = "animal_type", length = 64) + static + class Animal { + private int age; + + private String name; + + public Animal() { + } + + public Animal(int age, String name) { + this.age = age; + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + @Embeddable + static + class Cat extends Mammal { + //private int mouse; + // [...] + + + public Cat() { + super(); + } + + public Cat(int age, String name, String mother) { + super( age, name, mother ); + } + } + + @Embeddable + static + class Dog extends Mammal { + //private int bone; + // [...] + + public Dog() { + } + + public Dog(int age, String name, String mother) { + super( age, name, mother ); + } + } + + @Embeddable + static + class Fish extends Animal { + private int fins; + + public Fish() { + } + + public Fish(int age, String name, int fins) { + super( age, name ); + this.fins = fins; + } + + public int getFins() { + return fins; + } + + public void setFins(int fins) { + this.fins = fins; + } + } + + @Embeddable + static + class Mammal extends Animal { + private String mother; + + public Mammal() { + } + + public Mammal(int age, String name, String mother) { + super( age, name ); + this.mother = mother; + } + + public String getMother() { + return mother; + } + + public void setMother(String mother) { + this.mother = mother; + } + } + + @Entity(name = "Owner") + static + class Owner { + @Id + private Long id; + + @Embedded + private Animal pet; + + public Owner() { + } + + public Owner(Long id, Animal pet) { + this.id = id; + this.pet = pet; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Animal getPet() { + return pet; + } + + public void setPet(Animal pet) { + this.pet = pet; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/EmbeddableInheritanceReplaceTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/EmbeddableInheritanceReplaceTest.java new file mode 100644 index 000000000000..466e04462636 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/EmbeddableInheritanceReplaceTest.java @@ -0,0 +1,119 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.inheritance; + +import jakarta.persistence.*; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +@DomainModel(annotatedClasses = { + EmbeddableInheritanceReplaceTest.Emb.class, + EmbeddableInheritanceReplaceTest.Base.class, + EmbeddableInheritanceReplaceTest.Next.class, + EmbeddableInheritanceReplaceTest.Ent.class +}) +@SessionFactory +@JiraKey("HHH-19079") +public class EmbeddableInheritanceReplaceTest { + + @Test + void merge(SessionFactoryScope scope) { + scope.inTransaction(session -> { + final var history = new Ent(); + history.setBase(new Emb(42, "Hello, World!")); + + session.merge(history); + }); + } + + @Embeddable + @DiscriminatorValue("E") + public static final class Emb extends Next { + + Emb() { + } + + public Emb(int num, String str) { + super(num, str); + } + } + + @Embeddable + @DiscriminatorColumn(discriminatorType = DiscriminatorType.CHAR) + @DiscriminatorValue("b") + public static class Base { + + protected int num; + + protected Base() { + } + + public Base(int num, String str) { + this.num = num; + } + + public int getNum() { + return num; + } + + public void setNum(int num) { + this.num = num; + } + } + + @Embeddable + @DiscriminatorValue("n") + public static class Next extends Base { + + private String str; + + public Next(int num, String str) { + super(num, str); + this.str = str; + } + + public Next() { + } + } + + @Entity(name = "Ent") + public static class Ent { + + @Id + @GeneratedValue + private Integer id; + + @Embedded + private Base base; + + public Ent() { + } + + public Ent(final Integer id) { + this.id = id; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public Base getBase() { + return base; + } + + public void setBase(Base base) { + this.base = base; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/HierarchyOrderTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/HierarchyOrderTest.java new file mode 100644 index 000000000000..5913429d224a --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/HierarchyOrderTest.java @@ -0,0 +1,195 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.inheritance; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Embeddable; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; +import jakarta.persistence.TypedQuery; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@DomainModel( + annotatedClasses = { + HierarchyOrderTest.DerOA.class, + HierarchyOrderTest.DerDA.class, + HierarchyOrderTest.DerDB.class, + HierarchyOrderTest.DerOB.class, + HierarchyOrderTest.BaseD.class, + HierarchyOrderTest.BaseO.class + } +) +@SessionFactory +class HierarchyOrderTest { + + private EntityManagerFactory emf; + private DerOA deroa; + private DerOB derob; + + @BeforeEach + void setUp() { + DerDB derba1 = new DerDB( 5 ); + DerDA derda1 = new DerDA( "1", "abase" ); + deroa = new DerOA( derda1 ); + derob = new DerOB( derba1 ); +// emf = buildEntityManagerFactory(); + } + + @Test + void testBaseProperty(SessionFactoryScope scope) { + scope.inSession( em -> { + em.getTransaction().begin(); + em.persist( deroa ); + em.persist( derob ); + em.getTransaction().commit(); + Integer ida = deroa.getId(); + Integer idb = derob.getId(); + em.clear(); + TypedQuery qa = em.createQuery( "select o from DerOA o where o.id =:id", DerOA.class ); + qa.setParameter( "id", ida ); + DerOA deroain = qa.getSingleResult(); + assertEquals( "abase", deroain.derda.baseprop ); + } ); + } + + @Test + void testDerivedProperty(SessionFactoryScope scope) { + scope.inSession( em -> { + em.getTransaction().begin(); + em.persist( deroa ); + em.persist( derob ); + em.getTransaction().commit(); + Integer idb = derob.getId(); + em.clear(); + + TypedQuery qb = em.createQuery( "select o from DerOB o where o.id =:id", DerOB.class ); + qb.setParameter( "id", idb ); + DerOB derobin = qb.getSingleResult(); + assertNotNull( derobin ); + assertEquals( 5, derobin.derdb().b ); + } ); + } + + /* + * Created on 03/12/2024 by Paul Harrison (paul.harrison@manchester.ac.uk). + */ + @Entity(name = "DerOA") + public static class DerOA extends BaseO { + public DerOA(DerDA derda) { + this.derda = derda; + } + + @Embedded + // @AttributeOverrides({ + // @AttributeOverride(name="a",column = @Column(name = "da")) + // }) + public BaseD derda; + + public DerOA() { + + } + } + + /* + * Created on 03/12/2024 by Paul Harrison (paul.harrison@manchester.ac.uk). + */ + @Embeddable + public static class DerDB extends BaseD { + public DerDB(int b) { + this.b = b; + } + + public int b; + + public DerDB() { + + } + } + + /* + * Created on 03/12/2024 by Paul Harrison (paul.harrison@manchester.ac.uk). + */ + @Embeddable + public static class DerDA extends BaseD { + public DerDA(String a, String bprop) { + super( bprop ); + this.a = a; + } + + public String a; + + public DerDA() { + + } + } + + /* + * Created on 03/12/2024 by Paul Harrison (paul.harrison@manchester.ac.uk). + */ + @Embeddable + public abstract static class BaseD { //TODO would really like this to be abstract + public String baseprop; + + public BaseD(String baseprop) { + this.baseprop = baseprop; + } + + public BaseD() { + + } + + public String getBaseprop() { + return baseprop; + } + + } + + @Entity(name = "BaseO") + @Inheritance(strategy = InheritanceType.JOINED) + public abstract static class BaseO { + @Id + @GeneratedValue + private Integer id; + + public Integer getId() { + return id; + } + } + + /* + * Created on 03/12/2024 by Paul Harrison (paul.harrison@manchester.ac.uk). + */ + @Entity(name = "DerOB") + public static class DerOB extends BaseO { + public DerOB(DerDB derdb) { + this.derdb = derdb; + } + + @Embedded + BaseD derdb; + + public DerOB() { + + } + + public DerDB derdb() { + return (DerDB) derdb; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/InheritanceJunctionExistsPredicateTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/InheritanceJunctionExistsPredicateTest.java new file mode 100644 index 000000000000..d88eec91cec0 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/InheritanceJunctionExistsPredicateTest.java @@ -0,0 +1,139 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.inheritance; + +import java.util.ArrayList; +import java.util.List; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.DiscriminatorColumn; +import jakarta.persistence.DiscriminatorType; +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Marco Belladelli + */ +@DomainModel( annotatedClasses = { + InheritanceJunctionExistsPredicateTest.AbstractEntity.class, + InheritanceJunctionExistsPredicateTest.EntityA.class, + InheritanceJunctionExistsPredicateTest.EntityB.class, + InheritanceJunctionExistsPredicateTest.EntityAContainer.class, + InheritanceJunctionExistsPredicateTest.EntityBContainer.class, +} ) +@SessionFactory( useCollectingStatementInspector = true ) +@Jira( "https://hibernate.atlassian.net/browse/HHH-18174" ) +public class InheritanceJunctionExistsPredicateTest { + @Test + public void testExistsDisjunction(SessionFactoryScope scope) { + scope.inTransaction( session -> assertThat( session.createQuery( + "select c from EntityAContainer c " + + "where exists (select 1 from c.entities e where e.identifier like 'child%') " + + "or exists (select 1 from c.entities e)", + EntityAContainer.class + ).getResultList() ).hasSize( 0 ) ); + } + + @Test + public void testExistsConjunction(SessionFactoryScope scope) { + scope.inTransaction( session -> assertThat( session.createQuery( + "select c from EntityBContainer c " + + "where exists (select 1 from c.entities e where e.identifier like 'child%') " + + "and exists (select 1 from c.entities e)", + EntityBContainer.class + ).getResultList() ).hasSize( 1 ) ); + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final EntityB entityB = new EntityB(); + entityB.setIdentifier( "child_b" ); + session.persist( entityB ); + final EntityAContainer containerA = new EntityAContainer(); + containerA.id = 1; + session.persist( containerA ); + final EntityBContainer containerB = new EntityBContainer(); + containerB.id = 1; + containerB.entities.add( entityB ); + session.persist( containerB ); + } ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.createMutationQuery( "delete from AbstractEntity" ).executeUpdate(); + session.createMutationQuery( "delete from EntityAContainer" ).executeUpdate(); + session.createMutationQuery( "delete from EntityBContainer" ).executeUpdate(); + } ); + } + + @Entity( name = "AbstractEntity" ) + @DiscriminatorColumn( name = "disc_col", discriminatorType = DiscriminatorType.INTEGER ) + static abstract class AbstractEntity { + @Id + @GeneratedValue + private Integer id; + + private String identifier; + + public String getIdentifier() { + return identifier; + } + + public void setIdentifier(String identifier) { + this.identifier = identifier; + } + } + + @Entity( name = "EntityA" ) + @DiscriminatorValue( "1" ) + static class EntityA extends AbstractEntity { + } + + @Entity( name = "EntityB" ) + @DiscriminatorValue( "2" ) + static class EntityB extends AbstractEntity { + } + + @Entity( name = "EntityAContainer" ) + @Table( name = "a_container" ) + static class EntityAContainer { + @Id + private Integer id; + + @OneToMany + @JoinColumn( name = "reference" ) + private List entities = new ArrayList<>(); + } + + @Entity( name = "EntityBContainer" ) + @Table( name = "b_container" ) + static class EntityBContainer { + @Id + private Integer id; + + @OneToMany + @JoinColumn( name = "reference" ) + private List entities = new ArrayList<>(); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/InheritanceQueryGroupByTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/InheritanceQueryGroupByTest.java index 56590e7cc0be..f7a43ba705da 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/InheritanceQueryGroupByTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/InheritanceQueryGroupByTest.java @@ -6,6 +6,7 @@ */ package org.hibernate.orm.test.inheritance; +import org.hibernate.community.dialect.InformixDialect; import org.hibernate.dialect.Dialect; import org.hibernate.dialect.FunctionalDependencyAnalysisSupport; import org.hibernate.metamodel.mapping.EntityMappingType; @@ -16,6 +17,7 @@ import org.hibernate.testing.orm.junit.Jira; import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.SkipForDialect; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -86,6 +88,7 @@ public void testGroupBySingleTable(SessionFactoryScope scope) { } @Test + @SkipForDialect( dialectClass = InformixDialect.class , reason = "Informix does not support case expressions within the GROUP BY clause") public void testGroupByJoined(SessionFactoryScope scope) { testGroupBy( scope, "joinedParent", JoinedParent.class, "joined_child_one", 1 ); } @@ -149,8 +152,8 @@ private void testGroupByNotSelected( Long.class ).getSingleResult(); assertThat( sum ).isEqualTo( 3L ); - // When not selected, group by should only use the foreign key (parent_id) - statementInspector.assertNumberOfOccurrenceInQueryNoSpace( 0, parentFkName, 2 ); + // Association is joined, so every use of the join alias will make use of target table columns + statementInspector.assertNumberOfOccurrenceInQueryNoSpace( 0, parentFkName, 1 ); statementInspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "child_one_col", childPropCount ); statementInspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "child_two_col", childPropCount ); } ); @@ -162,6 +165,7 @@ public void testGroupByAndOrderBySingleTable(SessionFactoryScope scope) { } @Test + @SkipForDialect( dialectClass = InformixDialect.class , reason = "Informix does not support case expressions within the GROUP BY clause") public void testGroupByAndOrderByJoined(SessionFactoryScope scope) { testGroupByAndOrderBy( scope, "joinedParent", JoinedParent.class, "joined_child_one", 1 ); } @@ -232,8 +236,8 @@ private void testGroupByAndOrderByNotSelected( Long.class ).getSingleResult(); assertThat( sum ).isEqualTo( 3L ); - // When not selected, group by should only use the foreign key (parent_id) - statementInspector.assertNumberOfOccurrenceInQueryNoSpace( 0, parentFkName, 3 ); + // Association is joined, so every use of the join alias will make use of target table columns + statementInspector.assertNumberOfOccurrenceInQueryNoSpace( 0, parentFkName, 1 ); statementInspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "child_one_col", childPropCount ); statementInspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "child_two_col", childPropCount ); } ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/JoinedInheritanceTreatQueryTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/JoinedInheritanceTreatQueryTest.java index 72aaaed55477..4f2ef352d1e1 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/JoinedInheritanceTreatQueryTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/JoinedInheritanceTreatQueryTest.java @@ -6,6 +6,7 @@ */ package org.hibernate.orm.test.inheritance; +import org.hibernate.testing.jdbc.SQLStatementInspector; import org.hibernate.testing.orm.junit.DomainModel; import org.hibernate.testing.orm.junit.Jira; import org.hibernate.testing.orm.junit.SessionFactory; @@ -28,7 +29,7 @@ /** * @author Marco Belladelli */ -@SessionFactory +@SessionFactory( useCollectingStatementInspector = true ) @DomainModel( annotatedClasses = { JoinedInheritanceTreatQueryTest.Product.class, JoinedInheritanceTreatQueryTest.ProductOwner.class, @@ -37,6 +38,7 @@ JoinedInheritanceTreatQueryTest.Description.class, } ) @Jira( "https://hibernate.atlassian.net/browse/HHH-16574" ) +@Jira( "https://hibernate.atlassian.net/browse/HHH-18745" ) public class JoinedInheritanceTreatQueryTest { @BeforeAll public void setUp(SessionFactoryScope scope) { @@ -63,6 +65,8 @@ public void tearDown(SessionFactoryScope scope) { @Test public void testTreatedJoin(SessionFactoryScope scope) { + final SQLStatementInspector inspector = scope.getCollectingStatementInspector(); + inspector.clear(); scope.inTransaction( session -> { final Product result = session.createQuery( "from Product p " + @@ -72,11 +76,14 @@ public void testTreatedJoin(SessionFactoryScope scope) { ).getSingleResult(); assertThat( result.getOwner() ).isInstanceOf( ProductOwner1.class ); assertThat( ( (ProductOwner1) result.getOwner() ).getDescription().getText() ).isEqualTo( "description" ); + inspector.assertNumberOfJoins( 0, 3 ); } ); } @Test public void testImplicitTreatedJoin(SessionFactoryScope scope) { + final SQLStatementInspector inspector = scope.getCollectingStatementInspector(); + inspector.clear(); scope.inTransaction( session -> { final Product result = session.createQuery( "from Product p " + @@ -85,11 +92,14 @@ public void testImplicitTreatedJoin(SessionFactoryScope scope) { ).getSingleResult(); assertThat( result.getOwner() ).isInstanceOf( ProductOwner1.class ); assertThat( ( (ProductOwner1) result.getOwner() ).getDescription().getText() ).isEqualTo( "description" ); + inspector.assertNumberOfJoins( 0, 3 ); } ); } @Test public void testTreatedRoot(SessionFactoryScope scope) { + final SQLStatementInspector inspector = scope.getCollectingStatementInspector(); + inspector.clear(); scope.inTransaction( session -> { final ProductOwner result = session.createQuery( "from ProductOwner owner " + @@ -98,11 +108,14 @@ public void testTreatedRoot(SessionFactoryScope scope) { ).getSingleResult(); assertThat( result ).isInstanceOf( ProductOwner1.class ); assertThat( ( (ProductOwner1) result ).getDescription().getText() ).isEqualTo( "description" ); + inspector.assertNumberOfJoins( 0, 3 ); } ); } @Test public void testTreatedEntityJoin(SessionFactoryScope scope) { + final SQLStatementInspector inspector = scope.getCollectingStatementInspector(); + inspector.clear(); scope.inTransaction( session -> { final Product result = session.createQuery( "from Product p " + @@ -112,11 +125,14 @@ public void testTreatedEntityJoin(SessionFactoryScope scope) { ).getSingleResult(); assertThat( result.getOwner() ).isInstanceOf( ProductOwner1.class ); assertThat( ( (ProductOwner1) result.getOwner() ).getDescription().getText() ).isEqualTo( "description" ); + inspector.assertNumberOfJoins( 0, 3 ); } ); } @Test public void testBasicProperty(SessionFactoryScope scope) { + final SQLStatementInspector inspector = scope.getCollectingStatementInspector(); + inspector.clear(); scope.inTransaction( session -> { final Product result = session.createQuery( "from Product p " + @@ -126,6 +142,7 @@ public void testBasicProperty(SessionFactoryScope scope) { ).getSingleResult(); assertThat( result.getOwner() ).isInstanceOf( ProductOwner2.class ); assertThat( ( (ProductOwner2) result.getOwner() ).getBasicProp() ).isEqualTo( "basic_prop" ); + inspector.assertNumberOfJoins( 0, 2 ); } ); } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/discriminator/JoinedInheritanceDiscriminatorSelectionTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/discriminator/JoinedInheritanceDiscriminatorSelectionTest.java index 932c6ae6ffa9..2b6967e54957 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/discriminator/JoinedInheritanceDiscriminatorSelectionTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/discriminator/JoinedInheritanceDiscriminatorSelectionTest.java @@ -38,6 +38,7 @@ @SessionFactory( useCollectingStatementInspector = true ) @Jira( "https://hibernate.atlassian.net/browse/HHH-17727" ) @Jira( "https://hibernate.atlassian.net/browse/HHH-17806" ) +@Jira( "https://hibernate.atlassian.net/browse/HHH-18583" ) public class JoinedInheritanceDiscriminatorSelectionTest { @BeforeAll public void setUp(SessionFactoryScope scope) { @@ -51,7 +52,7 @@ public void setUp(SessionFactoryScope scope) { @AfterAll public void tearDown(SessionFactoryScope scope) { - scope.inTransaction( session -> session.createMutationQuery( "delete from ParentEntity" ).executeUpdate() ); + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); } @Test @@ -93,6 +94,23 @@ public void testSelectParentAttribute(SessionFactoryScope scope) { String.class ).getResultList() ).containsOnly( "parent", "child_a" ); inspector.assertNumberOfJoins( 0, 0 ); + inspector.clear(); + + // With treat() we preserve the join + + assertThat( session.createQuery( + "select p.name from ParentEntity p where treat(p as ChildA).id is not null", + String.class + ).getResultList() ).containsExactlyInAnyOrder( "child_a", "sub_child_a" ); + inspector.assertNumberOfJoins( 0, 1 ); + inspector.clear(); + + assertThat( session.createQuery( + "select p.name from ParentEntity p where treat(p as ChildB).id is not null", + String.class + ).getSingleResult() ).isEqualTo( "child_b" ); + inspector.assertNumberOfJoins( 0, 1 ); + inspector.clear(); } ); } @@ -123,27 +141,44 @@ public void testSelectInstance(SessionFactoryScope scope) { inspector.clear(); scope.inTransaction( session -> { - // NOTE: we currently always join all subclasses when selecting the entity instance. We could - // maybe avoid this when we have a physical discriminator column and a type filter + // With type filters we still join all subclasses that have properties when selecting the entity instance + // because we are not aware of the type restriction when processing the selection + assertThat( session.createQuery( "from ParentEntity p where type(p) = ParentEntity", ParentEntity.class ).getResultList() ).hasSize( 1 ); - inspector.assertNumberOfJoins( 0, 3 ); + inspector.assertNumberOfJoins( 0, 2 ); inspector.clear(); assertThat( session.createQuery( "from ParentEntity p where type(p) = ChildA", ParentEntity.class ).getResultList() ).hasSize( 1 ); - inspector.assertNumberOfJoins( 0, 3 ); + inspector.assertNumberOfJoins( 0, 2 ); inspector.clear(); assertThat( session.createQuery( "from ParentEntity p where type(p) = SubChildA", ParentEntity.class ).getResultList() ).hasSize( 1 ); - inspector.assertNumberOfJoins( 0, 3 ); + inspector.assertNumberOfJoins( 0, 2 ); + inspector.clear(); + + // With treat() we only join the needed subclasses + + assertThat( session.createQuery( + "select treat(p as ChildA) from ParentEntity p", + ParentEntity.class + ).getResultList() ).hasSize( 2 ); + inspector.assertNumberOfJoins( 0, 2 ); + inspector.clear(); + + assertThat( session.createQuery( + "select treat(p as ChildB) from ParentEntity p", + ParentEntity.class + ).getResultList() ).hasSize( 1 ); + inspector.assertNumberOfJoins( 0, 1 ); } ); } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/embeddable/InheritedPropertyTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/embeddable/InheritedPropertyTest.java new file mode 100644 index 000000000000..3e0e07a1fa8d --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/embeddable/InheritedPropertyTest.java @@ -0,0 +1,120 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.inheritance.embeddable; + +import jakarta.persistence.DiscriminatorColumn; +import jakarta.persistence.Embeddable; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + +@DomainModel( + annotatedClasses = { + InheritedPropertyTest.Animal.class, + InheritedPropertyTest.Cat.class, + InheritedPropertyTest.Dog.class, + InheritedPropertyTest.Fish.class, + InheritedPropertyTest.Mammal.class, + InheritedPropertyTest.Owner.class + } +) +@SessionFactory +public class InheritedPropertyTest { + + @BeforeEach + void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var cat = new Cat(); + cat.age = 7; + cat.name = "Jones"; + cat.mother = "Kitty"; + final var owner = new Owner(); + owner.id = 1L; + owner.pet = cat; + session.persist( owner ); + } ); + } + + @AfterEach + void tearDown(SessionFactoryScope scope) { + scope.inTransaction( session -> session.createMutationQuery( "delete from Owner" ).executeUpdate() ); + } + + @Test + void testInheritedProperty(SessionFactoryScope scope) { + assertDoesNotThrow( () -> scope.inSession( + session -> session.createQuery( + "select o from Owner o where treat(o.pet as InheritedPropertyTest$Cat).mother = :mother", + Owner.class ) ) ); + scope.inSession( + session -> { + final var cats = session.createQuery( + "select o from Owner o where treat(o.pet as InheritedPropertyTest$Cat).mother = :mother", + Owner.class ) + .setParameter( "mother", "Kitty" ) + .getResultList(); + assertEquals( 1, cats.size() ); + final var owner = cats.get( 0 ); + assertInstanceOf( Cat.class, owner.pet ); + assertEquals( "Jones", owner.pet.name ); + assertEquals( "Kitty", ((Cat) owner.pet).mother ); + } ); + } + + @Test + void testDeclaredPropertyCreateQuery(SessionFactoryScope scope) { + assertDoesNotThrow( () -> scope.inSession( + session -> session.createQuery( + "select o from Owner o where treat(o.pet as InheritedPropertyTest$Mammal).mother = :mother", + Owner.class ) ) ); + } + + @Entity(name = "Owner") + public static class Owner { + @Id + Long id; + + @Embedded + Animal pet; + } + + @Embeddable + @DiscriminatorColumn(name = "animal_type") + public static class Animal { + int age; + + String name; + } + + @Embeddable + public static class Fish extends Animal { + int fins; + } + + @Embeddable + public static class Mammal extends Animal { + String mother; + } + + @Embeddable + public static class Cat extends Mammal { + // [...] + } + + @Embeddable + public static class Dog extends Mammal { + // [...] + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithJoinedInheritanceTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithJoinedInheritanceTest.java index c5896e34233e..c5ff0c9d3715 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithJoinedInheritanceTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithJoinedInheritanceTest.java @@ -57,6 +57,33 @@ public void cleanup(SessionFactoryScope scope) { } ); } + @Test + @Jira( "https://hibernate.atlassian.net/browse/HHH-17646" ) + public void testLeftJoinSelectFk(SessionFactoryScope scope) { + scope.inTransaction( s -> { + final ChildEntityA childEntityA = new SubChildEntityA1( 11 ); + s.persist( childEntityA ); + final ChildEntityB childEntityB = new ChildEntityB( 21 ); + s.persist( childEntityB ); + s.persist( new RootOne( 1, childEntityA ) ); + s.persist( new RootOne( 2, null ) ); + } ); + scope.inTransaction( s -> { + // simulate association with ChildEntityB + s.createNativeMutationQuery( "update root_one set child_id = 21 where id = 2" ).executeUpdate(); + } ); + scope.inTransaction( s -> { + final List resultList = s.createQuery( + "select ce.id " + + "from RootOne r left join r.child ce ", + Integer.class + ).getResultList(); + assertEquals( 2, resultList.size() ); + assertEquals( 11, resultList.get( 0 ) ); + assertNull( resultList.get( 1 ) ); + } ); + } + @Test public void testLeftJoin(SessionFactoryScope scope) { scope.inTransaction( s -> { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithNaturalJoinedInheritanceTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithNaturalJoinedInheritanceTest.java new file mode 100644 index 000000000000..08ef6698e023 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithNaturalJoinedInheritanceTest.java @@ -0,0 +1,225 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.inheritance.join; + +import java.util.List; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Column; +import jakarta.persistence.DiscriminatorColumn; +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.Tuple; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; + +@DomainModel( annotatedClasses = { + AttributeJoinWithNaturalJoinedInheritanceTest.BaseClass.class, + AttributeJoinWithNaturalJoinedInheritanceTest.ChildEntityA.class, + AttributeJoinWithNaturalJoinedInheritanceTest.SubChildEntityA1.class, + AttributeJoinWithNaturalJoinedInheritanceTest.SubChildEntityA2.class, + AttributeJoinWithNaturalJoinedInheritanceTest.ChildEntityB.class, + AttributeJoinWithNaturalJoinedInheritanceTest.RootOne.class +} ) +@SessionFactory +@Jira( "https://hibernate.atlassian.net/browse/HHH-17646" ) +public class AttributeJoinWithNaturalJoinedInheritanceTest { + @AfterEach + public void cleanup(SessionFactoryScope scope) { + scope.inTransaction( s -> { + s.createMutationQuery( "delete from RootOne" ).executeUpdate(); + s.createMutationQuery( "delete from SubChildEntityA1" ).executeUpdate(); + s.createMutationQuery( "delete from SubChildEntityA2" ).executeUpdate(); + s.createMutationQuery( "delete from BaseClass" ).executeUpdate(); + } ); + } + + @Test + public void testLeftJoinWithDiscriminatorFiltering(SessionFactoryScope scope) { + scope.inTransaction( s -> { + final ChildEntityA childEntityA1 = new SubChildEntityA1( 11 ); + s.persist( childEntityA1 ); + final ChildEntityA childEntityA2 = new SubChildEntityA2( 21 ); + s.persist( childEntityA2 ); + s.persist( new RootOne( 1, childEntityA1 ) ); + s.persist( new RootOne( 2, childEntityA2 ) ); + } ); + scope.inTransaction( s -> { + final List resultList = s.createQuery( + "select r, ce, ce.uk " + + "from RootOne r left join treat(r.child as SubChildEntityA1) ce " + + "order by r.id", + Tuple.class + ).getResultList(); + assertEquals( 2, resultList.size() ); + assertResult( resultList.get( 0 ), 1, 11, 11, "child_a_1", SubChildEntityA1.class, 11 ); + assertResult( resultList.get( 1 ), 2, 21, null, null, null, null ); + } ); + } + + private void assertResult( + Tuple result, + Integer rootId, + Integer rootChildId, + Integer childId, + String discValue, + Class subClass, + Integer uk) { + if ( rootId != null ) { + final RootOne root = result.get( 0, RootOne.class ); + assertEquals( rootId, root.getId() ); + assertEquals( rootChildId, root.getChildId() ); + } + else { + assertNull( result.get( 0 ) ); + } + if ( subClass != null ) { + assertInstanceOf( subClass, result.get( 1 ) ); + final ChildEntityA sub1 = result.get( 1, subClass ); + assertEquals( childId, sub1.getId() ); + assertEquals( discValue, sub1.getDiscCol() ); + } + else { + assertNull( result.get( 1 ) ); + } + if ( uk != null ) { + assertEquals( uk, result.get( 2 ) ); + } + else { + assertNull( result.get( 2 ) ); + } + } + + /** + * NOTE: We define a {@link DiscriminatorColumn} to allow multiple subclasses + * to share the same table name. This will need additional care when pruning + * the table expression, since we'll have to add the discriminator condition + * before joining with the subclass tables + */ + @Entity( name = "BaseClass" ) + @Inheritance( strategy = InheritanceType.JOINED ) + @DiscriminatorColumn( name = "disc_col" ) + public static class BaseClass { + @Id + private Integer id; + + @Column( name = "disc_col", insertable = false, updatable = false ) + private String discCol; + + public BaseClass() { + } + + public BaseClass(Integer id) { + this.id = id; + } + + public Integer getId() { + return id; + } + + public String getDiscCol() { + return discCol; + } + } + + @Entity( name = "ChildEntityA" ) + @Table( name = "child_entity" ) + public static abstract class ChildEntityA extends BaseClass { + @Column(unique = true) + private Integer uk; + + public ChildEntityA() { + } + + public ChildEntityA(Integer id) { + super( id ); + this.uk = id; + } + + public Integer getUk() { + return uk; + } + } + + @Entity( name = "SubChildEntityA1" ) + @DiscriminatorValue( "child_a_1" ) + public static class SubChildEntityA1 extends ChildEntityA { + public SubChildEntityA1() { + } + + public SubChildEntityA1(Integer id) { + super( id ); + } + } + + @Entity( name = "SubChildEntityA2" ) + @DiscriminatorValue( "child_a_2" ) + public static class SubChildEntityA2 extends ChildEntityA { + public SubChildEntityA2() { + } + + public SubChildEntityA2(Integer id) { + super( id ); + } + } + + @Entity( name = "ChildEntityB" ) + @Table( name = "child_entity" ) + public static class ChildEntityB extends BaseClass { + + public ChildEntityB() { + } + + public ChildEntityB(Integer id) { + super( id ); + } + } + + @Entity( name = "RootOne" ) + @Table( name = "root_one" ) + public static class RootOne { + @Id + private Integer id; + + @Column( name = "child_id", insertable = false, updatable = false ) + private Integer childId; + + @ManyToOne + @JoinColumn( name = "child_id", referencedColumnName = "uk") + private ChildEntityA child; + + public RootOne() { + } + + public RootOne(Integer id, ChildEntityA child) { + this.id = id; + this.child = child; + } + + public Integer getId() { + return id; + } + + public Integer getChildId() { + return childId; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithRestrictedJoinedInheritanceTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithRestrictedJoinedInheritanceTest.java new file mode 100644 index 000000000000..4e64027d59e4 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithRestrictedJoinedInheritanceTest.java @@ -0,0 +1,215 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.inheritance.join; + +import java.util.List; + +import org.hibernate.annotations.SQLRestriction; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Column; +import jakarta.persistence.DiscriminatorColumn; +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.Tuple; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; + +@DomainModel( annotatedClasses = { + AttributeJoinWithRestrictedJoinedInheritanceTest.BaseClass.class, + AttributeJoinWithRestrictedJoinedInheritanceTest.ChildEntityA.class, + AttributeJoinWithRestrictedJoinedInheritanceTest.SubChildEntityA1.class, + AttributeJoinWithRestrictedJoinedInheritanceTest.SubChildEntityA2.class, + AttributeJoinWithRestrictedJoinedInheritanceTest.ChildEntityB.class, + AttributeJoinWithRestrictedJoinedInheritanceTest.RootOne.class +} ) +@SessionFactory +@Jira( "https://hibernate.atlassian.net/browse/HHH-17646" ) +public class AttributeJoinWithRestrictedJoinedInheritanceTest { + @AfterEach + public void cleanup(SessionFactoryScope scope) { + scope.inTransaction( s -> { + s.createMutationQuery( "delete from RootOne" ).executeUpdate(); + s.createMutationQuery( "delete from SubChildEntityA1" ).executeUpdate(); + s.createMutationQuery( "delete from SubChildEntityA2" ).executeUpdate(); + s.createMutationQuery( "delete from BaseClass" ).executeUpdate(); + } ); + } + + @Test + public void testLeftJoinWithDiscriminatorFiltering(SessionFactoryScope scope) { + scope.inTransaction( s -> { + final ChildEntityA childEntityA1 = new SubChildEntityA1( 11 ); + s.persist( childEntityA1 ); + final ChildEntityA childEntityA2 = new SubChildEntityA2( 21 ); + s.persist( childEntityA2 ); + s.persist( new RootOne( 1, childEntityA1 ) ); + s.persist( new RootOne( 2, childEntityA2 ) ); + } ); + scope.inTransaction( s -> { + final List resultList = s.createQuery( + "select r, ce " + + "from RootOne r left join r.child ce " + + "order by r.id", + Tuple.class + ).getResultList(); + assertEquals( 2, resultList.size() ); + assertResult( resultList.get( 0 ), 1, 11, 11, "child_a_1", SubChildEntityA1.class ); + assertResult( resultList.get( 1 ), 2, 21, null, null, null ); + } ); + } + + private void assertResult( + Tuple result, + Integer rootId, + Integer rootChildId, + Integer childId, + String discValue, + Class subClass) { + if ( rootId != null ) { + final RootOne root = result.get( 0, RootOne.class ); + assertEquals( rootId, root.getId() ); + assertEquals( rootChildId, root.getChildId() ); + } + else { + assertNull( result.get( 0 ) ); + } + if ( subClass != null ) { + assertInstanceOf( subClass, result.get( 1 ) ); + final ChildEntityA sub1 = result.get( 1, subClass ); + assertEquals( childId, sub1.getId() ); + assertEquals( discValue, sub1.getDiscCol() ); + } + else { + assertNull( result.get( 1 ) ); + } + } + + /** + * NOTE: We define a {@link DiscriminatorColumn} to allow multiple subclasses + * to share the same table name. This will need additional care when pruning + * the table expression, since we'll have to add the discriminator condition + * before joining with the subclass tables + */ + @Entity( name = "BaseClass" ) + @Inheritance( strategy = InheritanceType.JOINED ) + @DiscriminatorColumn( name = "disc_col" ) + @SQLRestriction( "ident < 20" ) + public static class BaseClass { + @Id + @Column(name = "ident") + private Integer id; + + @Column( name = "disc_col", insertable = false, updatable = false ) + private String discCol; + + public BaseClass() { + } + + public BaseClass(Integer id) { + this.id = id; + } + + public Integer getId() { + return id; + } + + public String getDiscCol() { + return discCol; + } + } + + @Entity( name = "ChildEntityA" ) + @Table( name = "child_entity" ) + public static abstract class ChildEntityA extends BaseClass { + + public ChildEntityA() { + } + + public ChildEntityA(Integer id) { + super( id ); + } + } + + @Entity( name = "SubChildEntityA1" ) + @DiscriminatorValue( "child_a_1" ) + public static class SubChildEntityA1 extends ChildEntityA { + public SubChildEntityA1() { + } + + public SubChildEntityA1(Integer id) { + super( id ); + } + } + + @Entity( name = "SubChildEntityA2" ) + @DiscriminatorValue( "child_a_2" ) + public static class SubChildEntityA2 extends ChildEntityA { + public SubChildEntityA2() { + } + + public SubChildEntityA2(Integer id) { + super( id ); + } + } + + @Entity( name = "ChildEntityB" ) + @Table( name = "child_entity" ) + public static class ChildEntityB extends BaseClass { + + public ChildEntityB() { + } + + public ChildEntityB(Integer id) { + super( id ); + } + } + + @Entity( name = "RootOne" ) + @Table( name = "root_one" ) + public static class RootOne { + @Id + private Integer id; + + @Column( name = "child_id", insertable = false, updatable = false ) + private Integer childId; + + @ManyToOne + @JoinColumn( name = "child_id") + private ChildEntityA child; + + public RootOne() { + } + + public RootOne(Integer id, ChildEntityA child) { + this.id = id; + this.child = child; + } + + public Integer getId() { + return id; + } + + public Integer getChildId() { + return childId; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/JoinedHierarchyDiscIdSelectTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/JoinedHierarchyDiscIdSelectTest.java new file mode 100644 index 000000000000..d7e7dc3b8afc --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/JoinedHierarchyDiscIdSelectTest.java @@ -0,0 +1,106 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.inheritance.join; + +import java.io.Serializable; +import java.util.List; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import jakarta.persistence.DiscriminatorColumn; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Root; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel( annotatedClasses = { + JoinedHierarchyDiscIdSelectTest.DooredVehicle.class, + JoinedHierarchyDiscIdSelectTest.BaseVehicle.class, + JoinedHierarchyDiscIdSelectTest.BaseEntity.class, +} ) +@SessionFactory +@Jira( "https://hibernate.atlassian.net/browse/HHH-18503" ) +public class JoinedHierarchyDiscIdSelectTest { + @ParameterizedTest + @ValueSource( classes = { + DooredVehicle.class, + BaseVehicle.class, + BaseEntity.class, + } ) + void testSelectCriteriaId(Class klass, SessionFactoryScope scope) { + scope.inTransaction( session -> { + final CriteriaBuilder criteriaBuilder = session.getCriteriaBuilder(); + final CriteriaQuery criteriaQuery = criteriaBuilder.createQuery( Long.class ); + final Root root = criteriaQuery.from( klass ); + criteriaQuery.select( root.get( "id" ) ); + final List resultList = session.createQuery( criteriaQuery ).getResultList(); + assertThat( resultList ).hasSize( 1 ).containsOnly( 1L ); + } ); + } + + @ParameterizedTest + @ValueSource( classes = { + DooredVehicle.class, + BaseVehicle.class, + BaseEntity.class, + } ) + void testSelectId(Class klass, SessionFactoryScope scope) { + scope.inTransaction( session -> { + final Long result = session.createQuery( + String.format( "select id from %s", klass.getSimpleName() ), + Long.class + ).getSingleResult(); + assertThat( result ).isEqualTo( 1L ); + } ); + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> session.persist( DooredVehicle.create( 1L ) ) ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Entity( name = "DooredVehicle" ) + static class DooredVehicle extends BaseVehicle { + public String doorType; + + public static DooredVehicle create(Long id) { + DooredVehicle vehicle = new DooredVehicle(); + vehicle.id = id; + return vehicle; + } + } + + @Entity( name = "BaseVehicle" ) + static class BaseVehicle extends BaseEntity { + public String bodyType; + } + + @Entity( name = "BaseEntity" ) + @DiscriminatorColumn( name = "type" ) + @Inheritance( strategy = InheritanceType.JOINED ) + static class BaseEntity implements Serializable { + @Id + public Long id; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/ReloadMultipleCollectionElementsTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/ReloadMultipleCollectionElementsTest.java new file mode 100644 index 000000000000..516bf16c9f33 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/ReloadMultipleCollectionElementsTest.java @@ -0,0 +1,307 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.inheritance.join; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.hibernate.Hibernate; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@DomainModel( annotatedClasses = { + ReloadMultipleCollectionElementsTest.Flight.class, + ReloadMultipleCollectionElementsTest.Ticket.class, + ReloadMultipleCollectionElementsTest.Customer.class, + ReloadMultipleCollectionElementsTest.Company.class +} ) +@SessionFactory +@Jira("https://hibernate.atlassian.net/browse/HHH-18493") +public class ReloadMultipleCollectionElementsTest { + + @BeforeEach + public void setup(SessionFactoryScope scope) { + scope.inTransaction( s -> { + Flight f2 = new Flight(); + f2.setId(1L); + f2.setName("Flight two"); + + Company us = new Company(); + us.setName("Airline 2"); + f2.setCompany(us); + + Customer c1 = new Customer(); + c1.setId( 1L ); + c1.setName("Tom"); + + Customer c2 = new Customer(); + c2.setId( 2L ); + c2.setName("Pete"); + + Ticket t1 = new Ticket(); + t1.setId(1L); + t1.setCustomer(c2); + t1.setNumber( "123" ); + + Ticket t2 = new Ticket(); + t2.setId(2L); + t2.setCustomer(c2); + t2.setNumber( "456" ); + + f2.setCustomers(Set.of(c1, c2)); + + s.persist(c1); + s.persist(c2); + s.persist(f2); + s.persist(t1); + s.persist(t2); + } ); + } + + @AfterEach + public void cleanup(SessionFactoryScope scope) { + scope.inTransaction( s -> { + s.createMutationQuery( "delete from Ticket" ).executeUpdate(); + s.createMutationQuery( "delete from Flight" ).executeUpdate(); + s.createMutationQuery( "delete from Customer" ).executeUpdate(); + s.createMutationQuery( "delete from Company" ).executeUpdate(); + } ); + } + + @Test + public void testResolveElementOfInitializedCollection(SessionFactoryScope scope) { + scope.inTransaction( s -> { + // First load all customers with their flights collection and corresponding customers + List customers = s.createQuery( + "from Customer c join fetch c.flights f join fetch f.customers order by c.id", + Customer.class + ).getResultList(); + assertEquals( 2, customers.size() ); + assertFalse( Hibernate.isInitialized( customers.get( 0 ).getTickets() ) ); + assertFalse( Hibernate.isInitialized( customers.get( 1 ).getTickets() ) ); + + // Then load all flights with their customers collection, but in addition, also the customers tickets + // This will trigger resolveInstance(Object, Data) with the existing collection and will + // fetch tickets data into existing customers + s.createQuery( "from Flight f join fetch f.customers c left join fetch c.tickets", Flight.class ).getResultList(); + + assertTrue( Hibernate.isInitialized( customers.get( 0 ).getTickets() ) ); + assertTrue( Hibernate.isInitialized( customers.get( 1 ).getTickets() ) ); + assertEquals( 0, customers.get( 0 ).getTickets().size() ); + assertEquals( 2, customers.get( 1 ).getTickets().size() ); + } ); + } + + @Entity( name = "Flight" ) + public static class Flight { + private Long id; + private String name; + private Company company; + private Set customers; + + public Flight() { + } + + @Id + @Column(name = "flight_id") + public Long getId() { + return id; + } + + public void setId(Long long1) { + id = long1; + } + + @Column(updatable = false, name = "flight_name", nullable = false, length = 50) + public String getName() { + return name; + } + + public void setName(String string) { + name = string; + } + + + @ManyToOne(cascade = {CascadeType.ALL}) + @JoinColumn(name = "comp_id") + public Company getCompany() { + return company; + } + + public void setCompany(Company company) { + this.company = company; + } + + @ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}, fetch = FetchType.EAGER) + public Set getCustomers() { + return customers; + } + + public void setCustomers(Set customers) { + this.customers = customers; + } + } + + @Entity( name = "Ticket" ) + public static class Ticket { + + Long id; + String number; + Customer customer; + + public Ticket() { + } + + @Id + public Long getId() { + return id; + } + + public void setId(Long long1) { + id = long1; + } + + @Column(name = "nr") + public String getNumber() { + return number; + } + + public void setNumber(String string) { + number = string; + } + + @ManyToOne(cascade = CascadeType.ALL) + @JoinColumn(name = "CUSTOMER_ID") + public Customer getCustomer() { + return customer; + } + + public void setCustomer(Customer customer) { + this.customer = customer; + } + } + + @Entity( name = "Customer" ) + public static class Customer { + private Long id; + private String name; + private String address; + private Set tickets; + private Set flights; + + // Address address; + + public Customer() { + } + + @Id + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getAddress() { + return address; + } + + public void setAddress(String address) { + this.address = address; + } + + @OneToMany(cascade = CascadeType.ALL, mappedBy = "customer") + public Set getTickets() { + return tickets; + } + + public void setTickets(Set tickets) { + this.tickets = tickets; + } + + @ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}, mappedBy = "customers") + public Set getFlights() { + return flights; + } + + public void setFlights(Set flights) { + this.flights = flights; + } + } + + @Entity( name = "Company" ) + public static class Company { + + private Long id; + private String name; + private Set flights = new HashSet(); + + public Company() { + } + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + @Column(name = "comp_id") + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public void setId(Long newId) { + id = newId; + } + + public void setName(String string) { + name = string; + } + + @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, mappedBy = "company") + @Column(name = "flight_id") + public Set getFlights() { + return flights; + } + + public void setFlights(Set flights) { + this.flights = flights; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jdbc/internal/AggressiveReleaseTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jdbc/internal/AggressiveReleaseTest.java index eac9332b1546..cb9329494976 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/jdbc/internal/AggressiveReleaseTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jdbc/internal/AggressiveReleaseTest.java @@ -20,6 +20,7 @@ import org.hibernate.testing.boot.BasicTestingJdbcServiceImpl; import org.hibernate.testing.orm.junit.BaseSessionFactoryFunctionalTest; +import org.hibernate.testing.orm.junit.Jira; import org.hibernate.testing.orm.junit.RequiresDialect; import org.hibernate.testing.orm.jdbc.PreparedStatementSpyConnectionProvider; import org.junit.jupiter.api.AfterEach; @@ -118,9 +119,9 @@ protected void cleanupTest() throws Exception { @Test public void testBasicRelease() { - connectionProvider.clear(); ResourceRegistry registry = sessionFactoryScope().fromSession( session -> { + connectionProvider.clear(); JdbcCoordinatorImpl jdbcCoord = (JdbcCoordinatorImpl) session.getJdbcCoordinator(); ResourceRegistry resourceRegistry = jdbcCoord.getLogicalConnection().getResourceRegistry(); try { @@ -155,9 +156,9 @@ public void testBasicRelease() { @Test public void testReleaseCircumventedByHeldResources() { - connectionProvider.clear(); ResourceRegistry registry = sessionFactoryScope().fromSession( session -> { + connectionProvider.clear(); JdbcCoordinatorImpl jdbcCoord = (JdbcCoordinatorImpl) session.getJdbcCoordinator(); ResourceRegistry resourceRegistry = jdbcCoord.getLogicalConnection().getResourceRegistry(); @@ -219,10 +220,9 @@ public void testReleaseCircumventedByHeldResources() { @Test public void testReleaseCircumventedManually() { - connectionProvider.clear(); - connectionProvider.clear(); ResourceRegistry registry = sessionFactoryScope().fromSession( session -> { + connectionProvider.clear(); JdbcCoordinatorImpl jdbcCoord = (JdbcCoordinatorImpl) session.getJdbcCoordinator(); ResourceRegistry resourceRegistry = jdbcCoord.getLogicalConnection().getResourceRegistry(); @@ -275,4 +275,20 @@ public void testReleaseCircumventedManually() { assertEquals( 0, connectionProvider.getAcquiredConnections().size() ); assertEquals( 2, connectionProvider.getReleasedConnections().size() ); } + + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-19477") + public void testHql() { + sessionFactoryScope().inTransaction( session -> { + connectionProvider.clear(); + JdbcCoordinatorImpl jdbcCoord = (JdbcCoordinatorImpl) session.getJdbcCoordinator(); + ResourceRegistry resourceRegistry = jdbcCoord.getLogicalConnection().getResourceRegistry(); + + session.createSelectionQuery( "select 1" ).uniqueResult(); + + assertFalse( resourceRegistry.hasRegisteredResources() ); + assertEquals( 0, connectionProvider.getAcquiredConnections().size() ); + assertEquals( 1, connectionProvider.getReleasedConnections().size() ); + } ); + } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/join/JoinAndFetchWithCriteriaSelectionQueryTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/join/JoinAndFetchWithCriteriaSelectionQueryTest.java new file mode 100644 index 000000000000..cb85f948df8b --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/join/JoinAndFetchWithCriteriaSelectionQueryTest.java @@ -0,0 +1,295 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.join; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import org.hibernate.Hibernate; +import org.hibernate.metamodel.model.domain.EntityDomainType; +import org.hibernate.query.Order; + +import org.hibernate.testing.jdbc.SQLStatementInspector; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Fetch; +import jakarta.persistence.criteria.Join; +import jakarta.persistence.criteria.Root; +import jakarta.persistence.metamodel.SingularAttribute; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * When fetching and joining associations using the criteria API, we need to check that we are not affected by the order + * of the operations + */ +@SessionFactory +@DomainModel(annotatedClasses = { JoinAndFetchWithCriteriaSelectionQueryTest.Book.class, JoinAndFetchWithCriteriaSelectionQueryTest.Author.class }) +@Jira("https://hibernate.atlassian.net/browse/HHH-19034") +class JoinAndFetchWithCriteriaSelectionQueryTest { + + static final Author amal = new Author( 1L, "Amal El-Mohtar" ); + static final Author max = new Author( 2L, "Max Gladstone" ); + static final Author ursula = new Author( 3L, "Ursula K. Le Guin" ); + static final Book timeWar = new Book( 1L, "This Is How You Lose the Time War" ); + static final Book leftHand = new Book( 2L, "The Left Hand of Darkness" ); + + @BeforeAll + public static void populateDb(SessionFactoryScope scope) { + timeWar.getAuthors().add( amal ); + timeWar.getAuthors().add( max ); + leftHand.getAuthors().add( ursula ); + scope.inTransaction( session -> { + session.persist( amal ); + session.persist( max ); + session.persist( ursula ); + session.persist( timeWar ); + session.persist( leftHand ); + } ); + } + + @Test + void fetchBeforeJoinWithWhereClauseTest(SessionFactoryScope scope) { + final SQLStatementInspector inspector = scope.getCollectingStatementInspector(); + inspector.clear(); + scope.inTransaction( session -> { + // Find all the books from an author, and load the authors association eagerly + CriteriaBuilder cb = session.getCriteriaBuilder(); + CriteriaQuery query = cb.createQuery( Book.class ); + Root from = query.from( Book.class ); + + // The fetch MUST BE created before the join for this test + Fetch fetch = from.fetch( "authors" ); + Join join = from.join( "authors" ); + query.where( cb.equal( join.get( "id" ), 2L ) ); + + // Because there's a filter on the association, they need to be two distinct joins + assertThat( join ).isNotEqualTo( fetch ); + + Book book = session.createQuery( query ).getSingleResult(); + assertThat( book ).isEqualTo( timeWar ); + assertThat( Hibernate.isInitialized( book.getAuthors() ) ).isTrue(); + assertThat( book.getAuthors() ).containsExactlyInAnyOrder( amal, max ); + inspector.assertExecutedCount( 1 ); + inspector.assertNumberOfOccurrenceInQuery( 0, "join", 3 ); + } ); + } + + @Test + void fetchAfterJoinWithWhereClauseTest(SessionFactoryScope scope) { + final SQLStatementInspector inspector = scope.getCollectingStatementInspector(); + inspector.clear(); + scope.inTransaction( session -> { + // Find all the books from an author, and load the authors association eagerly + CriteriaBuilder cb = session.getCriteriaBuilder(); + CriteriaQuery query = cb.createQuery( Book.class ); + Root from = query.from( Book.class ); + + // The join MUST BE created before the fetch for this test + Join join = from.join( "authors" ); + Fetch fetch = from.fetch( "authors" ); + query.where( cb.equal( join.get( "id" ), 2L ) ); + + // Because there's a filter on the association, they need to be two distinct joins + assertThat( join ).isNotEqualTo( fetch ); + Book book = session.createQuery( query ).getSingleResult(); + + assertThat( book ).isEqualTo( timeWar ); + assertThat( Hibernate.isInitialized( book.getAuthors() ) ).isTrue(); + assertThat( book.getAuthors() ).containsExactlyInAnyOrder( amal, max ); + inspector.assertExecutedCount( 1 ); + inspector.assertNumberOfOccurrenceInQuery( 0, "join", 3 ); + } ); + } + + @Test + void fetchAfterJoinWithoutWhereClauseTest(SessionFactoryScope scope) { + final SQLStatementInspector inspector = scope.getCollectingStatementInspector(); + inspector.clear(); + scope.inTransaction( session -> { + CriteriaBuilder cb = session.getCriteriaBuilder(); + CriteriaQuery query = cb.createQuery( Book.class ); + Root from = query.from( Book.class ); + + // The join MUST BE created before the fetch for this test + Join join = from.join( "authors" ); + Fetch fetch = from.fetch( "authors" ); + + // The current behaviour, but we could reuse the same join in this case + assertThat( join ).isNotEqualTo( fetch ); + + EntityDomainType bookType = scope.getSessionFactory().getJpaMetamodel().findEntityType( Book.class ); + SingularAttribute title = bookType.findSingularAttribute( "title" ); + query.select( from ).distinct( true ); + + List books = session + .createSelectionQuery( query ) + .setOrder( Order.asc( title ) ) + .getResultList(); + assertThat( books ).containsExactly( leftHand, timeWar ); + assertThat( Hibernate.isInitialized( books.get( 0 ).getAuthors() ) ).isTrue(); + assertThat( Hibernate.isInitialized( books.get( 1 ).getAuthors() ) ).isTrue(); + + inspector.assertExecutedCount( 1 ); + // The current behaviour, but we could generate a query with only 2 join + inspector.assertNumberOfOccurrenceInQuery( 0, "join", 3 ); + } ); + } + + @Test + void fetchBeforeJoinWithoutWhereClauseTest(SessionFactoryScope scope) { + final SQLStatementInspector inspector = scope.getCollectingStatementInspector(); + inspector.clear(); + scope.inTransaction( session -> { + CriteriaBuilder cb = session.getCriteriaBuilder(); + CriteriaQuery query = cb.createQuery( Book.class ); + Root from = query.from( Book.class ); + + // The fetch MUST BE created before the join for this test + Fetch fetch = from.fetch( "authors" ); + Join join = from.join( "authors" ); + + // The current behaviour, but we could reuse the same join in this case + assertThat( join ).isNotEqualTo( fetch ); + + EntityDomainType bookType = scope.getSessionFactory().getJpaMetamodel().findEntityType( Book.class ); + SingularAttribute title = bookType.findSingularAttribute( "title" ); + query.select( from ).distinct( true ); + + List books = session + .createSelectionQuery( query ) + .setOrder( Order.asc( title ) ) + .getResultList(); + assertThat( books ).containsExactly( leftHand, timeWar ); + assertThat( Hibernate.isInitialized( books.get( 0 ).getAuthors() ) ).isTrue(); + assertThat( Hibernate.isInitialized( books.get( 1 ).getAuthors() ) ).isTrue(); + + inspector.assertExecutedCount( 1 ); + // The current behaviour, but we could generate a query with only 2 join + inspector.assertNumberOfOccurrenceInQuery( 0, "join", 3 ); + } ); + } + + @Entity(name = "Author") + public static class Author { + @Id + private Long id; + private String name; + + public Author() { + } + + public Author(Long id, String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public boolean equals(Object object) { + if ( object == null || getClass() != object.getClass() ) { + return false; + } + Author author = (Author) object; + return Objects.equals( name, author.name ); + } + + @Override + public int hashCode() { + return Objects.hashCode( name ); + } + + @Override + public String toString() { + return id + ":" + name; + } + } + + @Entity(name = "Book") + public static class Book { + @Id + private Long id; + private String title; + @OneToMany + private List authors = new ArrayList<>(); + + public Book() { + } + + public Book(Long id, String title) { + this.id = id; + this.title = title; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public List getAuthors() { + return authors; + } + + public void setAuthors(List authors) { + this.authors = authors; + } + + @Override + public boolean equals(Object object) { + if ( object == null || getClass() != object.getClass() ) { + return false; + } + Book book = (Book) object; + return Objects.equals( title, book.title ); + } + + @Override + public int hashCode() { + return Objects.hashCode( title ); + } + + @Override + public String toString() { + return id + ":" + title; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/join/OuterJoinTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/join/OuterJoinTest.java index b2eabc30a64d..6b2aaf126d7a 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/join/OuterJoinTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/join/OuterJoinTest.java @@ -17,7 +17,6 @@ import org.hibernate.testing.orm.junit.Jpa; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -411,7 +410,6 @@ public void testJoinOrderWithRightJoinWithInnerImplicitJoins(EntityManagerFactor } @Test - @Disabled("Hibernate doesn't support implicit joins") public void testJoinOrderWithRightNormalJoinWithInnerImplicitJoins(EntityManagerFactoryScope scope) { scope.inTransaction( em -> { List resultList = em.createQuery( diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/joinsubquery/JoinSubqueryTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/joinsubquery/JoinSubqueryTest.java new file mode 100644 index 000000000000..d54a7421cbd7 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/joinsubquery/JoinSubqueryTest.java @@ -0,0 +1,129 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.joinsubquery; + +import jakarta.persistence.*; +import org.hibernate.annotations.JoinColumnOrFormula; +import org.hibernate.annotations.JoinFormula; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.io.Serializable; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@DomainModel(annotatedClasses = { + JoinSubqueryTest.RecordItem.class, + JoinSubqueryTest.RecordType.class +}) +@SessionFactory +@JiraKey("HHH-19052") +class JoinSubqueryTest { + + @BeforeAll + static void setUp(SessionFactoryScope scope) throws Exception { + scope.inTransaction(session -> { + final var id = 1L; + final var typeId = 42L; + final var recordType = new RecordType(id, typeId); + session.persist(recordType); + final var item = new RecordItem(id, typeId, recordType); + session.persist(item); + }); + } + + @Test + void test(SessionFactoryScope scope) throws Exception { + scope.inSession(session -> { + final var item = session.get(RecordItem.class, 1L); + assertNotNull(item); + }); + } + + @Entity + @Table(name = "record_items") + public static class RecordItem implements Serializable { + + @Id + protected Long id; + + @Column(name = "type_id", insertable = false, updatable = false) + private Long typeId; + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumnOrFormula(column = @JoinColumn(name = "type_id", referencedColumnName = "entity_id")) + @JoinColumnOrFormula(formula = @JoinFormula(value = "(SELECT x.id FROM record_types x WHERE x.entity_id = type_id)", referencedColumnName = "id")) + private RecordType type; + + RecordItem() { + } + + public RecordItem(Long id, Long typeId, RecordType type) { + this.id = id; + this.typeId = typeId; + this.type = type; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getId() { + return this.id; + } + + public Long getTypeId() { + return typeId; + } + + public RecordType getType() { + return type; + } + + + } + + @Entity + @Table(name = "record_types") + public static class RecordType implements Serializable { + + @Id + protected Long id; + + @Column(name = "entity_id") + private Long entityId; + + RecordType() { + } + + public RecordType(Long id, Long entityId) { + this.id = id; + this.entityId = entityId; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getId() { + return this.id; + } + + public Long getEntityId() { + return entityId; + } + + public void setEntityId(Long entityId) { + this.entityId = entityId; + } + + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/DetachedPreviousRowStateTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/DetachedPreviousRowStateTest.java new file mode 100644 index 000000000000..5f1b5ebf2ed0 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/DetachedPreviousRowStateTest.java @@ -0,0 +1,177 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.jpa; + +import jakarta.persistence.Entity; +import jakarta.persistence.EntityGraph; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Subgraph; +import jakarta.persistence.Table; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Root; +import org.hibernate.Hibernate; +import org.hibernate.jpa.SpecHints; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.Jpa; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Réda Housni Alaoui + */ +@Jpa(annotatedClasses = { + DetachedPreviousRowStateTest.Version.class, + DetachedPreviousRowStateTest.Product.class, + DetachedPreviousRowStateTest.Description.class, + DetachedPreviousRowStateTest.LocalizedDescription.class +}) +class DetachedPreviousRowStateTest { + + @BeforeEach + void setupData(EntityManagerFactoryScope scope) { + scope.inTransaction( em -> { + Product product = new Product(); + em.persist( product ); + + Description description = new Description( product ); + em.persist( description ); + + LocalizedDescription englishDescription = new LocalizedDescription( description ); + em.persist( englishDescription ); + LocalizedDescription frenchDescription = new LocalizedDescription( description ); + em.persist( frenchDescription ); + } ); + } + + @AfterEach + void cleanupData(EntityManagerFactoryScope scope) { + scope.inTransaction( em -> { + em.createQuery( "delete from LocalizedDescription l" ).executeUpdate(); + em.createQuery( "delete from Description d" ).executeUpdate(); + em.createQuery( "delete from Product p" ).executeUpdate(); + } ); + } + + @Test + @JiraKey(value = "HHH-18719") + void test(EntityManagerFactoryScope scope) { + scope.inTransaction( em -> { + CriteriaBuilder cb = em.getCriteriaBuilder(); + CriteriaQuery query = cb.createQuery( LocalizedDescription.class ); + Root root = query.from( LocalizedDescription.class ); + query.select( root ); + + EntityGraph localizedDescriptionGraph = + em.createEntityGraph( LocalizedDescription.class ); + Subgraph descriptionGraph = localizedDescriptionGraph.addSubgraph( "description" ); + Subgraph productGraph = descriptionGraph.addSubgraph( "product" ); + productGraph.addSubgraph( "versions" ); + + AtomicInteger resultCount = new AtomicInteger(); + + em.createQuery( query ) + .setHint( SpecHints.HINT_SPEC_LOAD_GRAPH, localizedDescriptionGraph ) + .getResultStream() + .forEach( localizedDescription -> { + resultCount.incrementAndGet(); + + assertThat( em.contains( localizedDescription.description.product ) ) + .withFailMessage( "'localizedDescription.description.product' is detached" ) + .isTrue(); + assertThat( Hibernate.isInitialized( localizedDescription.description.product ) ) + .withFailMessage( "'localizedDescription.description.product' is not initialized" ) + .isTrue(); + + em.flush(); + em.clear(); + } ); + + assertThat( resultCount.get() ).isEqualTo( 2 ); + } ); + } + + @Entity(name = "Version") + @Table(name = "version_tbl") + public static class Version { + + @Id + @GeneratedValue + private long id; + + private String description; + + @ManyToOne(fetch = FetchType.LAZY) + private Product product; + } + + @Entity(name = "Product") + @Table(name = "product_tbl") + public static class Product { + @Id + @GeneratedValue + private long id; + + private String description; + + @OneToMany(mappedBy = "product") + private final List versions = new ArrayList<>(); + } + + @Entity(name = "Description") + @Table(name = "description_tbl") + public static class Description { + @Id + @GeneratedValue + private long id; + + private String description; + + @OneToOne(fetch = FetchType.LAZY) + private Product product; + + public Description() { + } + + public Description(Product product) { + this.product = product; + } + } + + @Entity(name = "LocalizedDescription") + @Table(name = "localized_description_tbl") + public static class LocalizedDescription { + + @Id + @GeneratedValue + private long id; + + private String localizedDescription; + + @ManyToOne(fetch = FetchType.LAZY) + private Description description; + + public LocalizedDescription() { + } + + public LocalizedDescription(Description description) { + this.description = description; + } + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/EmptyMapTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/EmptyMapTest.java new file mode 100644 index 000000000000..695ade2e13d2 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/EmptyMapTest.java @@ -0,0 +1,104 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.jpa; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.MapKeyJoinColumn; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.Jpa; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Réda Housni Alaoui + */ +@Jpa(annotatedClasses = {EmptyMapTest.User.class, EmptyMapTest.Identity.class, EmptyMapTest.UserIdentity.class}) +public class EmptyMapTest { + + @BeforeEach + public void setupData(EntityManagerFactoryScope scope) { + scope.inTransaction( em -> em.persist( new User( 1 ) ) ); + } + + @AfterEach + public void cleanupData(EntityManagerFactoryScope scope) { + scope.inTransaction(em -> em.createQuery( "delete from User u" ).executeUpdate() ); + } + + @Test + @JiraKey(value = "HHH-18658") + public void test(EntityManagerFactoryScope scope) { + scope.inTransaction( em -> { + assertThat( em.find( User.class, 1 ) ).isNotNull(); + } ); + } + + @Entity(name = "User") + @Table(name = "user_tbl") + public static class User { + @Id + private int id; + private String name; + + @OneToMany( + mappedBy = "user", + fetch = FetchType.EAGER, + cascade = CascadeType.ALL, + orphanRemoval = true) + @MapKeyJoinColumn + private final Map userIdentityByIdentity = new HashMap<>(); + + public User() { + } + + public User(int id) { + this.id = id; + } + } + + @Entity(name = "Identity") + @Table(name = "identity_tbl") + public static class Identity { + @Id + @GeneratedValue + private int id; + private String name; + } + + @Entity(name = "UserIdentity") + @Table(name = "user_identity_tbl") + public static class UserIdentity { + @Id + @GeneratedValue + private int id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(nullable = false, updatable = false) + private User user; + + @OneToOne(fetch = FetchType.EAGER) + @JoinColumn(nullable = false, updatable = false) + private Identity identity; + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/compliance/LocalTimeTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/compliance/LocalTimeTest.java index 985f9b0e9e58..18fe576ca7a8 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/compliance/LocalTimeTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/compliance/LocalTimeTest.java @@ -35,7 +35,7 @@ ) public class LocalTimeTest { - private static final LocalTime LOCAL_TIME = DateTimeUtils.roundToDefaultPrecision( + private static final LocalTime LOCAL_TIME = DateTimeUtils.adjustToDefaultPrecision( LocalTime.now(), DialectContext.getDialect() ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/CoalesceTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/CoalesceTest.java index 1f0c10c1065e..7962db6fd220 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/CoalesceTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/CoalesceTest.java @@ -6,15 +6,24 @@ */ package org.hibernate.orm.test.jpa.criteria; -import org.hibernate.testing.TestForIssue; +import java.math.BigDecimal; +import java.util.List; + +import org.hibernate.testing.orm.junit.JiraKey; import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.Jira; import org.hibernate.testing.orm.junit.Jpa; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.Embedded; import jakarta.persistence.Entity; import jakarta.persistence.Id; +import jakarta.persistence.Tuple; import jakarta.persistence.TypedQuery; import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.CriteriaQuery; @@ -22,13 +31,18 @@ import jakarta.persistence.criteria.ParameterExpression; import jakarta.persistence.criteria.Root; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Will Dazy */ -@Jpa(annotatedClasses = CoalesceTest.HHH15291Entity.class) -@TestForIssue( jiraKey = "HHH-15291") +@Jpa( annotatedClasses = { + CoalesceTest.HHH15291Entity.class, + CoalesceTest.ComponentEntity.class, + CoalesceTest.ComponentA.class, +} ) +@JiraKey( value = "HHH-15291") public class CoalesceTest { - @Test public void hhh15291JPQL1Test(EntityManagerFactoryScope scope) { scope.inEntityManager( @@ -97,9 +111,74 @@ public void hhh15291Criteria2Test(EntityManagerFactoryScope scope) { ); } + @Test + @Jira( "https://hibernate.atlassian.net/browse/HHH-18321" ) + public void testCoalesceInBinaryArithmetic(EntityManagerFactoryScope scope) { + scope.inTransaction( entityManager -> { + final CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + final CriteriaQuery cquery = cb.createTupleQuery(); + final Root root = cquery.from( ComponentEntity.class ); + + cquery.select( cb.tuple( + root.get( "id" ), + cb.diff( + cb.coalesce( root.get( "componentA" ).get( "income" ), BigDecimal.ZERO ), + cb.coalesce( root.get( "componentA" ).get( "expense" ), BigDecimal.ZERO ) + ) + ) ); + + final List resultList = entityManager.createQuery( cquery ).getResultList(); + assertThat( resultList ).hasSize( 2 ); + for ( Tuple result : resultList ) { + final Long id = result.get( 0, Long.class ); + assertThat( result.get( 1, BigDecimal.class ).intValue() ).isEqualTo( id == 1L ? 0 : 1 ); + } + } ); + } + + @Test + @Jira( "https://hibernate.atlassian.net/browse/HHH-18321" ) + public void testCoalesceInBinaryArithmeticParam(EntityManagerFactoryScope scope) { + scope.inTransaction( entityManager -> { + final CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + final CriteriaQuery cquery = cb.createTupleQuery(); + final Root root = cquery.from( ComponentEntity.class ); + + final ParameterExpression defaultValue = cb.parameter( BigDecimal.class, "default-value" ); + + cquery.select( cb.tuple( + root.get( "id" ), + cb.diff( + defaultValue, + cb.coalesce( root.get( "componentA" ).get( "expense" ), defaultValue ) + ) + ) ); + + final List resultList = entityManager.createQuery( cquery ) + .setParameter( "default-value", BigDecimal.ZERO ).getResultList(); + assertThat( resultList ).hasSize( 2 ); + for ( Tuple result : resultList ) { + final Long id = result.get( 0, Long.class ); + assertThat( result.get( 1, BigDecimal.class ).intValue() ).isEqualTo( id == 1L ? -1 : 0 ); + } + } ); + } + + @BeforeAll + public void setUp(EntityManagerFactoryScope scope) { + scope.inTransaction( entityManager -> { + entityManager.persist( new ComponentEntity( 1L, new ComponentA( BigDecimal.ONE, BigDecimal.ONE ) ) ); + entityManager.persist( new ComponentEntity( 2L, new ComponentA( BigDecimal.ONE, null ) ) ); + } ); + } + + @AfterAll + public void tearDown(EntityManagerFactoryScope scope) { + scope.inTransaction( entityManager -> entityManager.createQuery( "delete from ComponentEntity" ).executeUpdate() ); + } + @Entity(name = "HHH15291Entity") public static class HHH15291Entity { - @Id @Column(name = "KEY_CHAR") private String KeyString; @@ -118,53 +197,36 @@ public static class HHH15291Entity { @Column(name = "ITEM_INTEGER1") private Integer itemInteger1; + } - public String getKeyString() { - return KeyString; - } - - public void setKeyString(String keyString) { - KeyString = keyString; - } - - public String getItemString1() { - return itemString1; - } - - public void setItemString1(String itemString1) { - this.itemString1 = itemString1; - } - - public String getItemString2() { - return itemString2; - } - - public void setItemString2(String itemString2) { - this.itemString2 = itemString2; - } + @Entity( name = "ComponentEntity" ) + static class ComponentEntity { + @Id + private Long id; - public String getItemString3() { - return itemString3; - } + @Embedded + private ComponentA componentA; - public void setItemString3(String itemString3) { - this.itemString3 = itemString3; + public ComponentEntity() { } - public String getItemString4() { - return itemString4; + public ComponentEntity(Long id, ComponentA componentA) { + this.id = id; + this.componentA = componentA; } + } - public void setItemString4(String itemString4) { - this.itemString4 = itemString4; - } + @Embeddable + static class ComponentA { + private BigDecimal income; + private BigDecimal expense; - public Integer getItemInteger1() { - return itemInteger1; + public ComponentA() { } - public void setItemInteger1(Integer itemInteger1) { - this.itemInteger1 = itemInteger1; + public ComponentA(BigDecimal income, BigDecimal expense) { + this.income = income; + this.expense = expense; } } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/CriteriaSelectOneToOneUnownedTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/CriteriaSelectOneToOneUnownedTest.java new file mode 100644 index 000000000000..cf086f3e7b3e --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/CriteriaSelectOneToOneUnownedTest.java @@ -0,0 +1,138 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.jpa.criteria; + +import java.util.List; + +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.Jpa; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToOne; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Join; +import jakarta.persistence.criteria.JoinType; +import jakarta.persistence.criteria.Root; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@Jpa( + annotatedClasses = { + CriteriaSelectOneToOneUnownedTest.Parent.class, + CriteriaSelectOneToOneUnownedTest.Child.class, + } +) +@JiraKey("HHH-18628") +public class CriteriaSelectOneToOneUnownedTest { + + @BeforeEach + public void setUp(EntityManagerFactoryScope scope) { + scope.inTransaction( + entityManager -> { + Child child = new Child( 1l, "child" ); + Parent parent = new Parent( 1l, "parent", child ); + entityManager.persist( parent ); + } + ); + } + + @AfterEach + public void tearDown(EntityManagerFactoryScope scope) { + scope.inTransaction( + entityManager -> { + entityManager.createQuery( "delete from Child" ).executeUpdate(); + entityManager.createQuery( "delete from Parent" ).executeUpdate(); + } + ); + } + + @Test + public void testCriteriaInnerJoin(EntityManagerFactoryScope scope) { + scope.inTransaction( + entityManager -> { + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + + CriteriaQuery query = cb.createQuery( Child.class ); + Root parent = query.from( Parent.class ); + Join child = parent.join( "child", JoinType.INNER ); + query.select( child ); + + List children = entityManager.createQuery( query ).getResultList(); + assertThat( children ).isNotNull(); + assertThat( children.size() ).isEqualTo( 1 ); + Child c = children.get( 0 ); + assertThat( c ).isNotNull(); + } ); + } + + @Test + public void testCriteriaLeftJoin(EntityManagerFactoryScope scope) { + scope.inTransaction( + entityManager -> { + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + + CriteriaQuery query = cb.createQuery( Child.class ); + Root parent = query.from( Parent.class ); + Join child = parent.join( "child", JoinType.LEFT ); + query.select( child ); + + List children = entityManager.createQuery( query ).getResultList(); + assertThat( children ).isNotNull(); + assertThat( children.size() ).isEqualTo( 1 ); + Child c = children.get( 0 ); + assertThat( c ).isNotNull(); + } ); + } + + @Entity(name = "Parent") + public static class Parent { + @Id + private Long id; + + private String name; + + @OneToOne(mappedBy = "parent", cascade = CascadeType.PERSIST) + private Child child; + + public Parent() { + } + + public Parent(Long id, String name, Child child) { + this.id = id; + this.name = name; + this.child = child; + child.parent = this; + } + } + + @Entity(name = "Child") + public static class Child { + + @Id + private Long id; + + private String name; + + @OneToOne(optional = false, fetch = FetchType.LAZY) + private Parent parent; + + public Child() { + } + + public Child(Long id, String name) { + this.id = id; + this.name = name; + } + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/QueryBuilderTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/QueryBuilderTest.java index 8c0abed81806..5c5996a90fa8 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/QueryBuilderTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/QueryBuilderTest.java @@ -20,6 +20,8 @@ import jakarta.persistence.criteria.SetJoin; import jakarta.persistence.metamodel.EntityType; import jakarta.persistence.metamodel.Metamodel; + +import org.hibernate.community.dialect.InformixDialect; import org.hibernate.dialect.CockroachDialect; import org.hibernate.orm.test.jpa.BaseEntityManagerFunctionalTestCase; import org.hibernate.orm.test.jpa.metamodel.Address; @@ -38,8 +40,8 @@ import org.hibernate.query.sqm.tree.predicate.SqmComparisonPredicate; import org.hibernate.testing.FailureExpected; -import org.hibernate.testing.SkipForDialect; import org.hibernate.testing.TestForIssue; +import org.hibernate.testing.orm.junit.SkipForDialect; import org.junit.Test; import static org.hibernate.testing.transaction.TransactionUtil.doInJPA; @@ -229,7 +231,7 @@ public void testMultiselectWithPredicates() { } @Test - @SkipForDialect(value = CockroachDialect.class, strictMatching = true) + @SkipForDialect(dialectClass = CockroachDialect.class) public void testDateTimeFunctions() { EntityManager em = getOrCreateEntityManager(); em.getTransaction().begin(); @@ -255,6 +257,7 @@ public void testDateTimeFunctions() { } @Test + @SkipForDialect(dialectClass = InformixDialect.class, majorVersion = 11, minorVersion = 70, reason = "Informix does not support count literals") public void testFunctionDialectFunctions() { EntityManager em = getOrCreateEntityManager(); em.getTransaction().begin(); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/paths/PluralAttributeExpressionsTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/paths/PluralAttributeExpressionsTest.java index 8d13761af3f7..af33816d2929 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/paths/PluralAttributeExpressionsTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/paths/PluralAttributeExpressionsTest.java @@ -9,6 +9,9 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; + +import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.Root; @@ -24,10 +27,11 @@ import org.hibernate.query.criteria.HibernateCriteriaBuilder; import org.hibernate.query.criteria.JpaExpression; -import org.hibernate.testing.TestForIssue; +import org.hibernate.testing.orm.junit.Jira; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; import static org.hibernate.testing.transaction.TransactionUtil.doInJPA; /** @@ -72,7 +76,7 @@ public void testCollectionIsEmptyCriteria() { } @Test - @TestForIssue( jiraKey = "HHH-11225" ) + @Jira("https://hibernate.atlassian.net/browse/HHH-11225") public void testElementMapIsEmptyHql() { doInJPA( this::entityManagerFactory, entityManager -> { entityManager.createQuery( "select m from MapEntity m where m.localized is empty" ).getResultList(); @@ -80,7 +84,7 @@ public void testElementMapIsEmptyHql() { } @Test - @TestForIssue( jiraKey = "HHH-11225" ) + @Jira("https://hibernate.atlassian.net/browse/HHH-11225") public void testElementMapIsEmptyCriteria() { doInJPA( this::entityManagerFactory, entityManager -> { final HibernateCriteriaBuilder cb = (HibernateCriteriaBuilder) entityManager.getCriteriaBuilder(); @@ -96,7 +100,7 @@ public void testElementMapIsEmptyCriteria() { } @Test - @TestForIssue( jiraKey = "HHH-11225" ) + @Jira("https://hibernate.atlassian.net/browse/HHH-11225") public void testEntityMapIsEmptyHql() { doInJPA( this::entityManagerFactory, entityManager -> { entityManager.createQuery( "select a from Article a where a.translations is empty" ).getResultList(); @@ -104,7 +108,7 @@ public void testEntityMapIsEmptyHql() { } @Test - @TestForIssue( jiraKey = "HHH-11225" ) + @Jira("https://hibernate.atlassian.net/browse/HHH-11225") public void testEntityMapIsEmptyCriteria() { doInJPA( this::entityManagerFactory, entityManager -> { final HibernateCriteriaBuilder cb = (HibernateCriteriaBuilder) entityManager.getCriteriaBuilder(); @@ -146,7 +150,7 @@ public void testCollectionSizeCriteria() { } @Test - @TestForIssue( jiraKey = "HHH-11225" ) + @Jira("https://hibernate.atlassian.net/browse/HHH-11225") public void testElementMapSizeHql() { doInJPA( this::entityManagerFactory, entityManager -> { entityManager.createQuery( "select m from MapEntity m where size( m.localized ) > 1" ).getResultList(); @@ -154,7 +158,7 @@ public void testElementMapSizeHql() { } @Test - @TestForIssue( jiraKey = "HHH-11225" ) + @Jira("https://hibernate.atlassian.net/browse/HHH-11225") public void testElementMapSizeCriteria() { doInJPA( this::entityManagerFactory, entityManager -> { final HibernateCriteriaBuilder cb = (HibernateCriteriaBuilder) entityManager.getCriteriaBuilder(); @@ -170,7 +174,7 @@ public void testElementMapSizeCriteria() { } @Test - @TestForIssue( jiraKey = "HHH-11225" ) + @Jira("https://hibernate.atlassian.net/browse/HHH-11225") public void testEntityMapSizeHql() { doInJPA( this::entityManagerFactory, entityManager -> { entityManager.createQuery( "select a from Article a where size(a.translations) > 1" ).getResultList(); @@ -178,7 +182,7 @@ public void testEntityMapSizeHql() { } @Test - @TestForIssue( jiraKey = "HHH-11225" ) + @Jira("https://hibernate.atlassian.net/browse/HHH-11225") public void testEntityMapSizeCriteria() { doInJPA( this::entityManagerFactory, entityManager -> { final HibernateCriteriaBuilder cb = (HibernateCriteriaBuilder) entityManager.getCriteriaBuilder(); @@ -193,4 +197,31 @@ public void testEntityMapSizeCriteria() { }); } + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-19126") + public void testPluralMapPathJavaType() { + doInJPA( this::entityManagerFactory, entityManager -> { + final CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + + final CriteriaQuery
    criteria = cb.createQuery( Article.class ); + final Root
    root = criteria.from( Article.class ); + + assertThat( root.get( Article_.translations ).getJavaType() ).isEqualTo( Map.class ); + assertThat( root.get( "translations" ).getJavaType() ).isEqualTo( Map.class ); + } ); + } + + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-19126") + public void testPluralListPathJavaType() { + doInJPA( this::entityManagerFactory, entityManager -> { + final CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + + final CriteriaQuery
    criteria = cb.createQuery( Address.class ); + final Root
    root = criteria.from( Address.class ); + + assertThat( root.get( Address_.phones ).getJavaType() ).isEqualTo( List.class ); + assertThat( root.get( "phones" ).getJavaType() ).isEqualTo( List.class ); + } ); + } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/orphan/onetomany/merge/MergeBidirectionalCascadeTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/orphan/onetomany/merge/MergeBidirectionalCascadeTest.java new file mode 100644 index 000000000000..edd9322aac6f --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/orphan/onetomany/merge/MergeBidirectionalCascadeTest.java @@ -0,0 +1,218 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.jpa.orphan.onetomany.merge; + +import java.util.ArrayList; +import java.util.List; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel( + annotatedClasses = { + MergeBidirectionalCascadeTest.Parent.class, + MergeBidirectionalCascadeTest.Child.class, + } +) +@SessionFactory +@JiraKey("HHH-18389") +public class MergeBidirectionalCascadeTest { + + private static final Long ID_PARENT_WITHOUT_CHILDREN = 1L; + private static final Long ID_PARENT_WITH_CHILDREN = 2L; + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITHOUT_CHILDREN, "old name" ); + session.persist( parent ); + + Parent parent2 = new Parent( ID_PARENT_WITH_CHILDREN, "old name" ); + Child child = new Child( 2l, "Child" ); + parent2.addChild( child ); + + session.persist( child ); + session.persist( parent2 ); + } + ); + } + + @AfterEach + private void tearDown(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.createMutationQuery( "delete from Child" ).executeUpdate(); + session.createMutationQuery( "delete from Parent" ).executeUpdate(); + } + ); + } + + @Test + public void testMergeParentWihoutChildren(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITHOUT_CHILDREN, "new name" ); + Parent merged = session.merge( parent ); + assertThat( merged.getName() ).isEqualTo( "new name" ); + + } + ); + } + + @Test + public void testMergeParentWithChildren(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITH_CHILDREN, "new name" ); + Child child = new Child( 2l, "Child" ); + parent.addChild( child ); + Parent merged = session.merge( parent ); + assertThat( merged.getName() ).isEqualTo( "new name" ); + + } + ); + scope.inTransaction( + session -> { + Parent parent = session.get( Parent.class, ID_PARENT_WITH_CHILDREN ); + assertThat( parent.getChildren().size() ).isEqualTo( 1 ); + + } + ); + } + + @Test + public void testMergeParentWithChildren2(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITH_CHILDREN, "new name" ); + Parent merged = session.merge( parent ); + + } + ); + scope.inTransaction( + session -> { + Parent parent = session.get( Parent.class, ID_PARENT_WITH_CHILDREN ); + assertThat( parent.getName() ).isEqualTo( "new name" ); + assertThat( parent.getChildren().size() ).isEqualTo( 1 ); + + List children = session.createQuery( "Select c from Child c", Child.class ).list(); + assertThat( children.size() ).isEqualTo( 1 ); + } + ); + } + + @Entity(name = "Parent") + public static class Parent { + + @Id + private Long id; + + private String name; + + @OneToMany(cascade = CascadeType.MERGE, mappedBy = "parent") + private List children; + + public Parent() { + } + + public Parent(Long id, String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getChildren() { + return children; + } + + public void setChildren(List children) { + this.children = children; + } + + public void addChild(Child child) { + if ( children == null ) { + children = new ArrayList(); + } + children.add( child ); + child.setParent( this ); + } + } + + @Entity(name = "Child") + public static class Child { + + @Id + private Long id; + + private String name; + + public Child() { + } + + public Child(Long id, String name) { + this.id = id; + this.name = name; + } + + @ManyToOne + @JoinColumn + private Parent parent; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Parent getParent() { + return parent; + } + + public void setParent(Parent parent) { + this.parent = parent; + } + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/orphan/onetomany/merge/MergeCascadeTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/orphan/onetomany/merge/MergeCascadeTest.java new file mode 100644 index 000000000000..5a3a603dc2df --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/orphan/onetomany/merge/MergeCascadeTest.java @@ -0,0 +1,200 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.jpa.orphan.onetomany.merge; + +import java.util.ArrayList; +import java.util.List; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel( + annotatedClasses = { + MergeCascadeTest.Parent.class, + MergeCascadeTest.Child.class, + } +) +@SessionFactory +@JiraKey("HHH-18389") +public class MergeCascadeTest { + + private static final Long ID_PARENT_WITHOUT_CHILDREN = 1L; + private static final Long ID_PARENT_WITH_CHILDREN = 2L; + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITHOUT_CHILDREN, "old name" ); + session.persist( parent ); + + Parent parent2 = new Parent( ID_PARENT_WITH_CHILDREN, "old name" ); + Child child = new Child( 2l, "Child" ); + parent2.addChild( child ); + + session.persist( child ); + session.persist( parent2 ); + } + ); + } + + @AfterEach + private void tearDown(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.createMutationQuery( "delete from Parent" ).executeUpdate(); + session.createMutationQuery( "delete from Child" ).executeUpdate(); + } + ); + } + + @Test + public void testMergeParentWihoutChildren(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITHOUT_CHILDREN, "new name" ); + Parent merged = session.merge( parent ); + assertThat( merged.getName() ).isEqualTo( "new name" ); + } + ); + } + + @Test + public void testMergeParentWithChildren(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITH_CHILDREN, "new name" ); + Child child = new Child( 2l, "Child" ); + parent.addChild( child ); + Parent merged = session.merge( parent ); + assertThat( merged.getName() ).isEqualTo( "new name" ); + } + ); + scope.inTransaction( + session -> { + Parent parent = session.get( Parent.class, ID_PARENT_WITH_CHILDREN ); + assertThat( parent.getChildren().size() ).isEqualTo( 1 ); + } + ); + } + + @Test + public void testMergeParentWithChildren2(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITH_CHILDREN, "new name" ); + session.merge( parent ); + } + ); + scope.inTransaction( + session -> { + Parent parent = session.get( Parent.class, ID_PARENT_WITH_CHILDREN ); + assertThat( parent.getName() ).isEqualTo( "new name" ); + assertThat( parent.getChildren().size() ).isEqualTo( 0 ); + + List children = session.createQuery( "Select c from Child c", Child.class ).list(); + assertThat( children.size() ).isEqualTo( 1 ); + } + ); + } + + @Entity(name = "Parent") + public static class Parent { + + @Id + private Long id; + + private String name; + + @OneToMany(cascade = CascadeType.MERGE) + private List children; + + public Parent() { + } + + public Parent(Long id, String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getChildren() { + return children; + } + + public void setChildren(List children) { + this.children = children; + } + + public void addChild(Child child) { + if ( children == null ) { + children = new ArrayList(); + } + children.add( child ); + } + } + + @Entity(name = "Child") + public static class Child { + + @Id + private Long id; + + private String name; + + public Child() { + } + + public Child(Long id, String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/orphan/onetomany/merge/MergeCascadeWithMapCollectionTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/orphan/onetomany/merge/MergeCascadeWithMapCollectionTest.java new file mode 100644 index 000000000000..1675d935f536 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/orphan/onetomany/merge/MergeCascadeWithMapCollectionTest.java @@ -0,0 +1,200 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.jpa.orphan.onetomany.merge; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel( + annotatedClasses = { + MergeCascadeWithMapCollectionTest.Parent.class, + MergeCascadeWithMapCollectionTest.Child.class, + } +) +@SessionFactory +@JiraKey("HHH-18842") +public class MergeCascadeWithMapCollectionTest { + + private static final Long ID_PARENT_WITHOUT_CHILDREN = 1L; + private static final Long ID_PARENT_WITH_CHILDREN = 2L; + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITHOUT_CHILDREN, "old name" ); + session.persist( parent ); + + Parent parent2 = new Parent( ID_PARENT_WITH_CHILDREN, "old name" ); + Child child = new Child( 2l, "Child" ); + parent2.addChild( child ); + + session.persist( child ); + session.persist( parent2 ); + } + ); + } + + @AfterEach + private void tearDown(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.createMutationQuery( "delete from Parent" ).executeUpdate(); + session.createMutationQuery( "delete from Child" ).executeUpdate(); + } + ); + } + + @Test + public void testMergeParentWihoutChildren(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITHOUT_CHILDREN, "new name" ); + Parent merged = session.merge( parent ); + assertThat( merged.getName() ).isEqualTo( "new name" ); + } + ); + } + + @Test + public void testMergeParentWithChildren(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITH_CHILDREN, "new name" ); + Child child = new Child( 2l, "Child" ); + parent.addChild( child ); + Parent merged = session.merge( parent ); + assertThat( merged.getName() ).isEqualTo( "new name" ); + } + ); + scope.inTransaction( + session -> { + Parent parent = session.get( Parent.class, ID_PARENT_WITH_CHILDREN ); + assertThat( parent.getChildren().size() ).isEqualTo( 1 ); + } + ); + } + + @Test + public void testMergeParentWithChildren2(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITH_CHILDREN, "new name" ); + session.merge( parent ); + } + ); + scope.inTransaction( + session -> { + Parent parent = session.get( Parent.class, ID_PARENT_WITH_CHILDREN ); + assertThat( parent.getName() ).isEqualTo( "new name" ); + assertThat( parent.getChildren().size() ).isEqualTo( 0 ); + + List children = session.createQuery( "Select c from Child c", Child.class ).list(); + assertThat( children.size() ).isEqualTo( 1 ); + } + ); + } + + @Entity(name = "Parent") + public static class Parent { + + @Id + private Long id; + + private String name; + + @OneToMany(cascade = CascadeType.MERGE) + private Map children; + + public Parent() { + } + + public Parent(Long id, String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Map getChildren() { + return children; + } + + public void setChildren(Map children) { + this.children = children; + } + + public void addChild(Child child) { + if ( children == null ) { + children = new HashMap<>(); + } + children.put( child.getName(), child ); + } + } + + @Entity(name = "Child") + public static class Child { + + @Id + private Long id; + + private String name; + + public Child() { + } + + public Child(Long id, String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/orphan/onetomany/merge/MergeOrphanRemovalArrayBidirectionalTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/orphan/onetomany/merge/MergeOrphanRemovalArrayBidirectionalTest.java new file mode 100644 index 000000000000..9f2cc05f2a15 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/orphan/onetomany/merge/MergeOrphanRemovalArrayBidirectionalTest.java @@ -0,0 +1,217 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.jpa.orphan.onetomany.merge; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel( + annotatedClasses = { + MergeOrphanRemovalArrayBidirectionalTest.Parent.class, + MergeOrphanRemovalArrayBidirectionalTest.Child.class, + } +) +@SessionFactory +@JiraKey("HHH-18842") +public class MergeOrphanRemovalArrayBidirectionalTest { + + private static final Long ID_PARENT_WITHOUT_CHILDREN = 1L; + private static final Long ID_PARENT_WITH_CHILDREN = 2L; + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITHOUT_CHILDREN, "old name" ); + session.persist( parent ); + + Parent parent2 = new Parent( ID_PARENT_WITH_CHILDREN, "old name" ); + Child child = new Child( 2l, "Child" ); + Child[] children = new Child[1]; + children[0] = child; + parent2.setChildren( children ); + + session.persist( child ); + session.persist( parent2 ); + } + ); + } + + @AfterEach + private void tearDown(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.createMutationQuery( "delete from Child" ).executeUpdate(); + session.createMutationQuery( "delete from Parent" ).executeUpdate(); + } + ); + } + + @Test + public void testMergeParentWihoutChildren(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITHOUT_CHILDREN, "new name" ); + Parent merged = session.merge( parent ); + assertThat( merged.getName() ).isEqualTo( "new name" ); + + } + ); + } + + @Test + public void testMergeParentWithChildren(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITH_CHILDREN, "new name" ); + Child child = new Child( 2l, "Child" ); + Child[] children = new Child[1]; + children[0] = child; + parent.setChildren( children ); + Parent merged = session.merge( parent ); + assertThat( merged.getName() ).isEqualTo( "new name" ); + + } + ); + scope.inTransaction( + session -> { + Parent parent = session.get( Parent.class, ID_PARENT_WITH_CHILDREN ); + assertThat( parent.getChildren().length ).isEqualTo( 1 ); + + } + ); + } + + @Test + public void testMergeParentWithChildren2(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITH_CHILDREN, "new name" ); + Parent merged = session.merge( parent ); + + } + ); + scope.inTransaction( + session -> { + Parent parent = session.get( Parent.class, ID_PARENT_WITH_CHILDREN ); + assertThat( parent.getName() ).isEqualTo( "new name" ); + assertThat( parent.getChildren().length ).isEqualTo( 0 ); + + List children = session.createQuery( "Select c from Child c", Child.class ).list(); + assertThat( children.size() ).isEqualTo( 0 ); + } + ); + + } + + @Entity(name = "Parent") + public static class Parent { + + @Id + private Long id; + + private String name; + + @OneToMany(orphanRemoval = true, mappedBy = "parent") + private Child[] children; + + public Parent() { + } + + public Parent(Long id, String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Child[] getChildren() { + return children; + } + + public void setChildren(Child[] children) { + this.children = children; + for ( int i = 0; i < children.length; i++ ) { + children[i].setParent( this ); + } + } + + } + + @Entity(name = "Child") + public static class Child { + + @Id + private Long id; + + private String name; + + public Child() { + } + + public Child(Long id, String name) { + this.id = id; + this.name = name; + } + + @ManyToOne + @JoinColumn + private Parent parent; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Parent getParent() { + return parent; + } + + public void setParent(Parent parent) { + this.parent = parent; + } + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/orphan/onetomany/merge/MergeOrphanRemovalBidirectionalTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/orphan/onetomany/merge/MergeOrphanRemovalBidirectionalTest.java new file mode 100644 index 000000000000..89aac97c0f6a --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/orphan/onetomany/merge/MergeOrphanRemovalBidirectionalTest.java @@ -0,0 +1,217 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.jpa.orphan.onetomany.merge; + +import java.util.ArrayList; +import java.util.List; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel( + annotatedClasses = { + MergeOrphanRemovalBidirectionalTest.Parent.class, + MergeOrphanRemovalBidirectionalTest.Child.class, + } +) +@SessionFactory +@JiraKey("HHH-18389") +public class MergeOrphanRemovalBidirectionalTest { + + private static final Long ID_PARENT_WITHOUT_CHILDREN = 1L; + private static final Long ID_PARENT_WITH_CHILDREN = 2L; + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITHOUT_CHILDREN, "old name" ); + session.persist( parent ); + + Parent parent2 = new Parent( ID_PARENT_WITH_CHILDREN, "old name" ); + Child child = new Child( 2l, "Child" ); + parent2.addChild( child ); + + session.persist( child ); + session.persist( parent2 ); + } + ); + } + + @AfterEach + private void tearDown(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.createMutationQuery( "delete from Child" ).executeUpdate(); + session.createMutationQuery( "delete from Parent" ).executeUpdate(); + } + ); + } + + @Test + public void testMergeParentWihoutChildren(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITHOUT_CHILDREN, "new name" ); + Parent merged = session.merge( parent ); + assertThat( merged.getName() ).isEqualTo( "new name" ); + + } + ); + } + + @Test + public void testMergeParentWithChildren(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITH_CHILDREN, "new name" ); + Child child = new Child( 2l, "Child" ); + parent.addChild( child ); + Parent merged = session.merge( parent ); + assertThat( merged.getName() ).isEqualTo( "new name" ); + + } + ); + scope.inTransaction( + session -> { + Parent parent = session.get( Parent.class, ID_PARENT_WITH_CHILDREN ); + assertThat( parent.getChildren().size() ).isEqualTo( 1 ); + + } + ); + } + + @Test + public void testMergeParentWithChildren2(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITH_CHILDREN, "new name" ); + Parent merged = session.merge( parent ); + + } + ); + scope.inTransaction( + session -> { + Parent parent = session.get( Parent.class, ID_PARENT_WITH_CHILDREN ); + assertThat( parent.getName() ).isEqualTo( "new name" ); + assertThat( parent.getChildren().size() ).isEqualTo( 0 ); + + List children = session.createQuery( "Select c from Child c", Child.class ).list(); + assertThat( children.size() ).isEqualTo( 0 ); + } + ); + } + + @Entity(name = "Parent") + public static class Parent { + + @Id + private Long id; + + private String name; + + @OneToMany(orphanRemoval = true, mappedBy = "parent") + private List children; + + public Parent() { + } + + public Parent(Long id, String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getChildren() { + return children; + } + + public void setChildren(List children) { + this.children = children; + } + + public void addChild(Child child) { + if ( children == null ) { + children = new ArrayList(); + } + children.add( child ); + child.setParent( this ); + } + } + + @Entity(name = "Child") + public static class Child { + + @Id + private Long id; + + private String name; + + public Child() { + } + + public Child(Long id, String name) { + this.id = id; + this.name = name; + } + + @ManyToOne + @JoinColumn + private Parent parent; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Parent getParent() { + return parent; + } + + public void setParent(Parent parent) { + this.parent = parent; + } + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/orphan/onetomany/merge/MergeOrphanRemovalTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/orphan/onetomany/merge/MergeOrphanRemovalTest.java new file mode 100644 index 000000000000..88197e74386e --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/orphan/onetomany/merge/MergeOrphanRemovalTest.java @@ -0,0 +1,229 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.jpa.orphan.onetomany.merge; + +import java.util.ArrayList; +import java.util.List; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel( + annotatedClasses = { + MergeOrphanRemovalTest.Parent.class, + MergeOrphanRemovalTest.Child.class, + } +) +@SessionFactory +@JiraKey("HHH-18389") +public class MergeOrphanRemovalTest { + + private static final Long ID_PARENT_WITHOUT_CHILDREN = 1L; + private static final Long ID_PARENT_WITH_CHILDREN = 2L; + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITHOUT_CHILDREN, "old name" ); + session.persist( parent ); + + Parent parent2 = new Parent( ID_PARENT_WITH_CHILDREN, "old name" ); + Child child = new Child( 2l, "Child" ); + parent2.addChild( child ); + + session.persist( child ); + session.persist( parent2 ); + } + ); + } + + @AfterEach + private void tearDown(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.createMutationQuery( "delete from Parent" ).executeUpdate(); + session.createMutationQuery( "delete from Child" ).executeUpdate(); + } + ); + } + + @Test + public void testMergeParentWihoutChildren(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITHOUT_CHILDREN, "new name" ); + Parent merged = session.merge( parent ); + assertThat( merged.getName() ).isEqualTo( "new name" ); + + } + ); + } + + @Test + public void testMergeParentWithChildren(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITH_CHILDREN, "new name" ); + Child child = new Child( 2l, "Child" ); + parent.addChild( child ); + Parent merged = session.merge( parent ); + assertThat( merged.getName() ).isEqualTo( "new name" ); + + } + ); + scope.inTransaction( + session -> { + Parent parent = session.get( Parent.class, ID_PARENT_WITH_CHILDREN ); + assertThat( parent.getChildren().size() ).isEqualTo( 1 ); + + } + ); + } + + @Test + public void testMergeParentWithChildren2(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITH_CHILDREN, "new name" ); + Parent merged = session.merge( parent ); + + } + ); + scope.inTransaction( + session -> { + Parent parent = session.get( Parent.class, ID_PARENT_WITH_CHILDREN ); + assertThat( parent.getName() ).isEqualTo( "new name" ); + assertThat( parent.getChildren().size() ).isEqualTo( 0 ); + + List children = session.createQuery( "Select c from Child c", Child.class ).list(); + assertThat( children.size() ).isEqualTo( 0 ); + } + ); + } + + @Test + public void testMergeParentWithChildren3(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITH_CHILDREN, "new name" ); + Child child = new Child( 3l, "Child2" ); + parent.addChild( child ); + session.persist( child ); + Parent merged = session.merge( parent ); + + } + ); + scope.inTransaction( + session -> { + Parent parent = session.get( Parent.class, ID_PARENT_WITH_CHILDREN ); + assertThat( parent.getName() ).isEqualTo( "new name" ); + assertThat( parent.getChildren().size() ).isEqualTo( 1 ); + assertThat( parent.getChildren().get( 0 ).getName() ).isEqualTo( "Child2" ); + + List children = session.createQuery( "Select c from Child c", Child.class ).list(); + assertThat( children.size() ).isEqualTo( 1 ); + assertThat( children.get( 0 ).getName() ).isEqualTo( "Child2" ); + } + ); + } + + @Entity(name = "Parent") + public static class Parent { + + @Id + private Long id; + + private String name; + + @OneToMany(orphanRemoval = true) + private List children; + + public Parent() { + } + + public Parent(Long id, String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getChildren() { + return children; + } + + public void setChildren(List children) { + this.children = children; + } + + public void addChild(Child child) { + if ( children == null ) { + children = new ArrayList(); + } + children.add( child ); + } + } + + @Entity(name = "Child") + public static class Child { + + @Id + private Long id; + + private String name; + + public Child() { + } + + public Child(Long id, String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/orphan/onetomany/merge/MergeTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/orphan/onetomany/merge/MergeTest.java new file mode 100644 index 000000000000..1e9304804ab0 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/orphan/onetomany/merge/MergeTest.java @@ -0,0 +1,200 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.jpa.orphan.onetomany.merge; + +import java.util.ArrayList; +import java.util.List; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel( + annotatedClasses = { + MergeTest.Parent.class, + MergeTest.Child.class, + } +) +@SessionFactory +@JiraKey("HHH-18389") +public class MergeTest { + + private static final Long ID_PARENT_WITHOUT_CHILDREN = 1L; + private static final Long ID_PARENT_WITH_CHILDREN = 2L; + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITHOUT_CHILDREN, "old name" ); + session.persist( parent ); + + Parent parent2 = new Parent( ID_PARENT_WITH_CHILDREN, "old name" ); + Child child = new Child( 2l, "Child" ); + parent2.addChild( child ); + + session.persist( child ); + session.persist( parent2 ); + } + ); + } + + @AfterEach + private void tearDown(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.createMutationQuery( "delete from Parent" ).executeUpdate(); + session.createMutationQuery( "delete from Child" ).executeUpdate(); + } + ); + } + + @Test + public void testMergeParentWihoutChildren(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITHOUT_CHILDREN, "new name" ); + Parent merged = session.merge( parent ); + assertThat( merged.getName() ).isEqualTo( "new name" ); + } + ); + } + + @Test + public void testMergeParentWithChildren(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITH_CHILDREN, "new name" ); + Child child = new Child( 2l, "Child" ); + parent.addChild( child ); + Parent merged = session.merge( parent ); + assertThat( merged.getName() ).isEqualTo( "new name" ); + } + ); + scope.inTransaction( + session -> { + Parent parent = session.get( Parent.class, ID_PARENT_WITH_CHILDREN ); + assertThat( parent.getChildren().size() ).isEqualTo( 1 ); + } + ); + } + + @Test + public void testMergeParentWithChildren2(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITH_CHILDREN, "new name" ); + session.merge( parent ); + } + ); + scope.inTransaction( + session -> { + Parent parent = session.get( Parent.class, ID_PARENT_WITH_CHILDREN ); + assertThat( parent.getName() ).isEqualTo( "new name" ); + assertThat( parent.getChildren().size() ).isEqualTo( 0 ); + + List children = session.createQuery( "Select c from Child c", Child.class ).list(); + assertThat( children.size() ).isEqualTo( 1 ); + } + ); + } + + + @Entity(name = "Parent") + public static class Parent { + + @Id + private Long id; + + private String name; + + @OneToMany + private List children; + + public Parent() { + } + + public Parent(Long id, String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getChildren() { + return children; + } + + public void setChildren(List children) { + this.children = children; + } + + public void addChild(Child child) { + if ( children == null ) { + children = new ArrayList(); + } + children.add( child ); + } + } + + @Entity(name = "Child") + public static class Child { + + @Id + private Long id; + + private String name; + + public Child() { + } + + public Child(Long id, String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/ql/JoinTableOptimizationTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/ql/JoinTableOptimizationTest.java index 9f79f5066e00..eb6b87b3abbf 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/ql/JoinTableOptimizationTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/ql/JoinTableOptimizationTest.java @@ -8,7 +8,6 @@ import java.util.Set; -import org.hibernate.testing.TestForIssue; import org.hibernate.testing.jdbc.SQLStatementInspector; import org.hibernate.testing.orm.junit.DomainModel; import org.hibernate.testing.orm.junit.JiraKey; @@ -21,19 +20,28 @@ import jakarta.persistence.ElementCollection; import jakarta.persistence.Embeddable; import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; import jakarta.persistence.JoinTable; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; -@TestForIssue(jiraKey = "HHH-16691") @DomainModel( annotatedClasses = { - JoinTableOptimizationTest.Document.class, JoinTableOptimizationTest.Person.class + JoinTableOptimizationTest.Document.class, + JoinTableOptimizationTest.Person.class, + JoinTableOptimizationTest.File.class, + JoinTableOptimizationTest.Picture.class }) @SessionFactory(useCollectingStatementInspector = true) public class JoinTableOptimizationTest { @Test + @JiraKey("HHH-16691") public void testOnlyCollectionTableJoined(SessionFactoryScope scope) { SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); statementInspector.clear(); @@ -48,6 +56,7 @@ public void testOnlyCollectionTableJoined(SessionFactoryScope scope) { } @Test + @JiraKey("HHH-16691") public void testInnerJoin(SessionFactoryScope scope) { SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); statementInspector.clear(); @@ -68,6 +77,7 @@ public void testInnerJoin(SessionFactoryScope scope) { } @Test + @JiraKey("HHH-16691") public void testLeftJoin(SessionFactoryScope scope) { SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); statementInspector.clear(); @@ -88,6 +98,7 @@ public void testLeftJoin(SessionFactoryScope scope) { } @Test + @JiraKey("HHH-16691") public void testInnerJoinCustomOnClause(SessionFactoryScope scope) { SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); statementInspector.clear(); @@ -108,6 +119,7 @@ public void testInnerJoinCustomOnClause(SessionFactoryScope scope) { } @Test + @JiraKey("HHH-16691") public void testLeftJoinCustomOnClause(SessionFactoryScope scope) { SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); statementInspector.clear(); @@ -147,6 +159,199 @@ public void testElementCollectionJoinCustomOnClause(SessionFactoryScope scope) { ); } + @Test + @JiraKey("HHH-17646") + public void testLeftJoinFetch(SessionFactoryScope scope) { + SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + statementInspector.clear(); + scope.inTransaction( + s -> { + s.createQuery( "select d from Document d left join fetch d.people" ).list(); + statementInspector.assertExecutedCount( 1 ); + Assertions.assertEquals( + "select d1_0.id,d1_0.name,p1_0.Document_id,p1_1.id,p1_1.name " + + "from Document d1_0 " + + "left join people p1_0 on d1_0.id=p1_0.Document_id " + + "left join Person p1_1 on p1_1.id=p1_0.people_id", + statementInspector.getSqlQueries().get( 0 ), + "Nested join was not optimized away" + ); + } + ); + } + + @Test + @JiraKey("HHH-17646") + public void testJoinTablePolymorphicJoined(SessionFactoryScope scope) { + SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + statementInspector.clear(); + scope.inTransaction( + s -> { + s.createQuery( "select f from Document d join d.manyFiles f" ).list(); + statementInspector.assertExecutedCount( 1 ); + Assertions.assertEquals( + "select mf1_1.id,case when mf1_2.id is not null then 1 when mf1_1.id is not null then 0 end,mf1_1.document_id,mf1_1.name,mf1_2.extension " + + "from Document d1_0 " + + "join many_files mf1_0 on d1_0.id=mf1_0.Document_id " + + "join file_tbl mf1_1 on mf1_1.id=mf1_0.manyFiles_id " + + "left join Picture mf1_2 on mf1_1.id=mf1_2.id", + statementInspector.getSqlQueries().get( 0 ), + "Nested join was not optimized away" + ); + } + ); + } + + @Test + @JiraKey("HHH-17646") + public void testJoinTablePolymorphicLeftJoined(SessionFactoryScope scope) { + SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + statementInspector.clear(); + scope.inTransaction( + s -> { + s.createQuery( "select f from Document d left join d.manyFiles f" ).list(); + statementInspector.assertExecutedCount( 1 ); + Assertions.assertEquals( + "select mf1_1.id,case when mf1_2.id is not null then 1 when mf1_1.id is not null then 0 end,mf1_1.document_id,mf1_1.name,mf1_2.extension " + + "from Document d1_0 " + + "left join many_files mf1_0 on d1_0.id=mf1_0.Document_id " + + "left join file_tbl mf1_1 on mf1_1.id=mf1_0.manyFiles_id " + + "left join Picture mf1_2 on mf1_1.id=mf1_2.id", + statementInspector.getSqlQueries().get( 0 ), + "Nested join was not optimized away" + ); + } + ); + } + + @Test + @JiraKey("HHH-17646") + public void testJoinPolymorphicJoined(SessionFactoryScope scope) { + SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + statementInspector.clear(); + scope.inTransaction( + s -> { + s.createQuery( "select f from Document d join d.files f" ).list(); + statementInspector.assertExecutedCount( 1 ); + Assertions.assertEquals( + "select f1_0.id,case when f1_1.id is not null then 1 when f1_0.id is not null then 0 end,d1_0.id,d1_0.name,f1_0.name,f1_1.extension " + + "from Document d1_0 " + + "join file_tbl f1_0 on d1_0.id=f1_0.document_id " + + "left join Picture f1_1 on f1_0.id=f1_1.id", + statementInspector.getSqlQueries().get( 0 ), + "Nested join was not optimized away" + ); + } + ); + } + + @Test + @JiraKey("HHH-17646") + public void testJoinPolymorphicLeftJoined(SessionFactoryScope scope) { + SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + statementInspector.clear(); + scope.inTransaction( + s -> { + s.createQuery( "select f from Document d left join d.files f" ).list(); + statementInspector.assertExecutedCount( 1 ); + Assertions.assertEquals( + "select f1_0.id,case when f1_1.id is not null then 1 when f1_0.id is not null then 0 end,d1_0.id,d1_0.name,f1_0.name,f1_1.extension " + + "from Document d1_0 " + + "left join file_tbl f1_0 on d1_0.id=f1_0.document_id " + + "left join Picture f1_1 on f1_0.id=f1_1.id", + statementInspector.getSqlQueries().get( 0 ), + "Nested join was not optimized away" + ); + } + ); + } + + @Test + @JiraKey("HHH-17646") + public void testJoinTablePolymorphicSubtypeJoined(SessionFactoryScope scope) { + SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + statementInspector.clear(); + scope.inTransaction( + s -> { + s.createQuery( "select f from Document d join d.manyPictures f" ).list(); + statementInspector.assertExecutedCount( 1 ); + Assertions.assertEquals( + "select mp1_1.id,mp1_2.document_id,mp1_2.name,mp1_1.extension " + + "from Document d1_0 " + + "join many_pictures mp1_0 on d1_0.id=mp1_0.Document_id " + + "join Picture mp1_1 on mp1_1.id=mp1_0.manyPictures_id " + + "join file_tbl mp1_2 on mp1_1.id=mp1_2.id", + statementInspector.getSqlQueries().get( 0 ), + "Nested join was not optimized away" + ); + } + ); + } + + @Test + @JiraKey("HHH-17646") + public void testJoinTablePolymorphicSubtypeLeftJoined(SessionFactoryScope scope) { + SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + statementInspector.clear(); + scope.inTransaction( + s -> { + s.createQuery( "select f from Document d left join d.manyPictures f" ).list(); + statementInspector.assertExecutedCount( 1 ); + Assertions.assertEquals( + "select mp1_1.id,mp1_2.document_id,mp1_2.name,mp1_1.extension " + + "from Document d1_0 " + + "left join many_pictures mp1_0 on d1_0.id=mp1_0.Document_id " + + "left join Picture mp1_1 on mp1_1.id=mp1_0.manyPictures_id " + + "left join file_tbl mp1_2 on mp1_1.id=mp1_2.id", + statementInspector.getSqlQueries().get( 0 ), + "Nested join was not optimized away" + ); + } + ); + } + + @Test + @JiraKey("HHH-17646") + public void testJoinPolymorphicSubtypeJoined(SessionFactoryScope scope) { + SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + statementInspector.clear(); + scope.inTransaction( + s -> { + s.createQuery( "select f from Document d join d.pictures f" ).list(); + statementInspector.assertExecutedCount( 1 ); + Assertions.assertEquals( + "select p1_0.id,p1_1.document_id,p1_1.name,p1_0.extension " + + "from Document d1_0 " + + "join file_tbl p1_1 on d1_0.id=p1_1.document_id " + + "join Picture p1_0 on p1_0.id=p1_1.id", + statementInspector.getSqlQueries().get( 0 ), + "Nested join was not optimized away" + ); + } + ); + } + + @Test + @JiraKey("HHH-17646") + public void testJoinPolymorphicSubtypeLeftJoined(SessionFactoryScope scope) { + SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + statementInspector.clear(); + scope.inTransaction( + s -> { + s.createQuery( "select f from Document d left join d.pictures f" ).list(); + statementInspector.assertExecutedCount( 1 ); + Assertions.assertEquals( + "select p1_0.id,p1_1.document_id,p1_1.name,p1_0.extension " + + "from Document d1_0 " + + "left join file_tbl p1_1 on d1_0.id=p1_1.document_id " + + "left join Picture p1_0 on p1_0.id=p1_1.id", + statementInspector.getSqlQueries().get( 0 ), + "Nested join was not optimized away" + ); + } + ); + } + @Entity(name = "Document") public static class Document { @Id @@ -158,6 +363,16 @@ public static class Document { @ElementCollection @CollectionTable(name = "document_pages") Set pages; + @OneToMany(mappedBy = "document") + Set files; + @ManyToMany + @JoinTable(name = "many_files") + Set manyFiles; + @OneToMany(mappedBy = "document") + Set pictures; + @ManyToMany + @JoinTable(name = "many_pictures") + Set manyPictures; } @Entity(name = "Person") public static class Person { @@ -165,6 +380,20 @@ public static class Person { Long id; String name; } + @Entity(name = "File") + @Table(name = "file_tbl") + @Inheritance(strategy = InheritanceType.JOINED) + public static class File { + @Id + Long id; + String name; + @ManyToOne(fetch = FetchType.LAZY) + Document document; + } + @Entity(name = "Picture") + public static class Picture extends File { + String extension; + } @Embeddable public static class Page { String text; diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/ql/MapIssueTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/ql/MapIssueTest.java index c735fa79af3d..64705998e664 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/ql/MapIssueTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/ql/MapIssueTest.java @@ -52,13 +52,27 @@ public void testOnlyCollectionTableJoined(SessionFactoryScope scope) { } @Test - public void testMapKeyJoinIsOmitted(SessionFactoryScope scope) { + public void testMapKeyJoinIsNotOmitted(SessionFactoryScope scope) { SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); statementInspector.clear(); scope.inTransaction( s -> { s.createQuery( "select c from MapOwner as o join o.contents c join c.relationship r where r.id is not null" ).list(); statementInspector.assertExecutedCount( 1 ); + // Assert 3 joins, collection table, collection element and collection key (relationship) + statementInspector.assertNumberOfJoins( 0, 3 ); + } + ); + } + + @Test + public void testMapKeyJoinIsOmitted2(SessionFactoryScope scope) { + SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + statementInspector.clear(); + scope.inTransaction( + s -> { + s.createQuery( "select c from MapOwner as o join o.contents c where c.relationship.id is not null" ).list(); + statementInspector.assertExecutedCount( 1 ); // Assert 2 joins, collection table and collection element. No need to join the relationship because it is not nullable statementInspector.assertNumberOfJoins( 0, 2 ); } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/query/DateTimeParameterTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/query/DateTimeParameterTest.java index e3a523d0039b..e75b668747a5 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/query/DateTimeParameterTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/query/DateTimeParameterTest.java @@ -6,100 +6,118 @@ */ package org.hibernate.orm.test.jpa.query; -import java.util.Date; -import java.util.GregorianCalendar; -import java.util.List; import jakarta.persistence.Entity; -import jakarta.persistence.EntityManager; import jakarta.persistence.Id; +import jakarta.persistence.Parameter; import jakarta.persistence.Query; import jakarta.persistence.Table; import jakarta.persistence.Temporal; import jakarta.persistence.TemporalType; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.Jpa; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; -import org.hibernate.orm.test.jpa.BaseEntityManagerFunctionalTestCase; - -import org.junit.Test; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.List; -import static org.junit.Assert.assertEquals; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; /** * @author Steve Ebersole */ -public class DateTimeParameterTest extends BaseEntityManagerFunctionalTestCase { +@Jpa(annotatedClasses = {DateTimeParameterTest.Thing.class}) +public class DateTimeParameterTest { private static GregorianCalendar nowCal = new GregorianCalendar(); private static Date now = new Date( nowCal.getTime().getTime() ); - @Override - protected Class[] getAnnotatedClasses() { - return new Class[] { Thing.class }; - } - @Test - public void testBindingCalendarAsDate() { - createTestData(); - - EntityManager em = getOrCreateEntityManager(); - em.getTransaction().begin(); - - try { - Query query = em.createQuery( "from Thing t where t.someDate = :aDate" ); + public void testBindingCalendarAsDate(EntityManagerFactoryScope scope) { + scope.inTransaction( entityManager -> { + final Query query = entityManager.createQuery( "from Thing t where t.someDate = :aDate" ); query.setParameter( "aDate", nowCal, TemporalType.DATE ); - List list = query.getResultList(); - assertEquals( 1, list.size() ); - } - finally { - em.getTransaction().rollback(); - em.close(); - } - - deleteTestData(); + final List list = query.getResultList(); + assertThat( list.size() ).isEqualTo( 1 ); + } ); } @Test - public void testBindingNulls() { - EntityManager em = getOrCreateEntityManager(); - em.getTransaction().begin(); - - try { - Query query = em.createQuery( "from Thing t where t.someDate = :aDate or t.someTime = :aTime or t.someTimestamp = :aTimestamp" ); + public void testBindingNulls(EntityManagerFactoryScope scope) { + scope.inTransaction( entityManager -> { + final Query query = entityManager.createQuery( + "from Thing t where t.someDate = :aDate or t.someTime = :aTime or t.someTimestamp = :aTimestamp" + ); query.setParameter( "aDate", (Date) null, TemporalType.DATE ); query.setParameter( "aTime", (Date) null, TemporalType.DATE ); query.setParameter( "aTimestamp", (Date) null, TemporalType.DATE ); - } - finally { - em.getTransaction().rollback(); - em.close(); - } + } ); + + } + + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-17151") + public void testBindingNullNativeQueryPositional(EntityManagerFactoryScope scope) { + scope.inTransaction( entityManager -> { + final Query query = entityManager.createNativeQuery( "update Thing set someDate = ?1 where id = 1" ); + //noinspection deprecation + query.setParameter( 1, (Date) null, TemporalType.DATE ); + assertThat( query.executeUpdate() ).isEqualTo( 1 ); + } ); + scope.inTransaction( entityManager -> assertThat( entityManager.find( Thing.class, 1 ).someDate ).isNull() ); + } + + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-17151") + public void testBindingNullNativeQueryNamed(EntityManagerFactoryScope scope) { + scope.inTransaction( entityManager -> { + final Query query = entityManager.createNativeQuery( "update Thing set someDate = :me where id = 1" ); + Parameter p = new Parameter<>() { + @Override + public String getName() { + return "me"; + } + + @Override + public Integer getPosition() { + return null; + } + + @Override + public Class getParameterType() { + return Date.class; + } + }; + //noinspection deprecation + query.setParameter( p, null, TemporalType.DATE ); + assertThat( query.executeUpdate() ).isEqualTo( 1 ); + } ); + scope.inTransaction( entityManager -> assertThat( entityManager.find( Thing.class, 1 ).someDate ).isNull() ); } - private void createTestData() { - EntityManager em = getOrCreateEntityManager(); - em.getTransaction().begin(); - em.persist( new Thing( 1, "test", now, now, now ) ); - em.getTransaction().commit(); - em.close(); + @BeforeEach + public void createTestData(EntityManagerFactoryScope scope) { + scope.inTransaction( entityManager -> entityManager.persist( new Thing( 1, "test", now, now, now ) ) ); } - private void deleteTestData() { - EntityManager em = getOrCreateEntityManager(); - em.getTransaction().begin(); - em.createQuery( "delete Thing" ).executeUpdate(); - em.getTransaction().commit(); - em.close(); + @AfterEach + public void deleteTestData(EntityManagerFactoryScope scope) { + scope.inTransaction( entityManager -> entityManager.createQuery( "delete from Thing" ).executeUpdate() ); } - @Entity( name="Thing" ) - @Table( name = "THING" ) + @Entity(name = "Thing") + @Table(name = "Thing") public static class Thing { @Id public Integer id; public String someString; - @Temporal( TemporalType.DATE ) + @Temporal(TemporalType.DATE) public Date someDate; - @Temporal( TemporalType.TIME ) + @Temporal(TemporalType.TIME) public Date someTime; - @Temporal( TemporalType.TIMESTAMP ) + @Temporal(TemporalType.TIMESTAMP) public Date someTimestamp; public Thing() { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/txn/JtaObtainConnectionExceptionTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/txn/JtaObtainConnectionExceptionTest.java new file mode 100644 index 000000000000..f909dba75995 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/txn/JtaObtainConnectionExceptionTest.java @@ -0,0 +1,113 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.jpa.txn; + +import jakarta.transaction.HeuristicMixedException; +import jakarta.transaction.HeuristicRollbackException; +import jakarta.transaction.InvalidTransactionException; +import jakarta.transaction.NotSupportedException; +import jakarta.transaction.RollbackException; +import jakarta.transaction.SystemException; +import jakarta.transaction.Transaction; +import jakarta.transaction.TransactionManager; +import org.hibernate.HibernateException; +import org.hibernate.engine.jdbc.connections.spi.JdbcConnectionAccess; +import org.hibernate.jdbc.AbstractReturningWork; +import org.hibernate.resource.transaction.backend.jta.internal.JtaIsolationDelegate; +import org.hibernate.testing.orm.junit.JiraKey; +import org.junit.jupiter.api.Test; + +import java.sql.Connection; +import java.sql.SQLException; + +import static org.junit.jupiter.api.Assertions.assertThrowsExactly; + +@JiraKey("HHH-18961") +public class JtaObtainConnectionExceptionTest { + + @Test + public void testIt() { + final JtaIsolationDelegate isolationDelegate = new JtaIsolationDelegate( + new ObtainConnectionSqlExceptionConnectionAccess(), + null, + new TransactionManagerImpl() ); + + assertThrowsExactly( HibernateException.class, () -> + isolationDelegate.delegateWork( + new AbstractReturningWork<>() { + @Override + public Object execute(Connection connection) { + return null; + } + }, + false + ) + ); + } + + public static class ObtainConnectionSqlExceptionConnectionAccess implements JdbcConnectionAccess { + @Override + public Connection obtainConnection() throws SQLException { + throw new SQLException( "No connection" ); + } + + @Override + public void releaseConnection(Connection connection) throws SQLException { + + } + + @Override + public boolean supportsAggressiveRelease() { + return false; + } + } + + public static class TransactionManagerImpl implements TransactionManager { + @Override + public void begin() throws NotSupportedException, SystemException { + + } + + @Override + public void commit() + throws RollbackException, HeuristicMixedException, HeuristicRollbackException, SecurityException, IllegalStateException, SystemException { + } + + @Override + public int getStatus() throws SystemException { + return 0; + } + + @Override + public Transaction getTransaction() throws SystemException { + return null; + } + + @Override + public void resume(Transaction transaction) + throws InvalidTransactionException, IllegalStateException, SystemException { + } + + @Override + public void rollback() throws IllegalStateException, SecurityException, SystemException { + + } + + @Override + public void setRollbackOnly() throws IllegalStateException, SystemException { + + } + + @Override + public void setTransactionTimeout(int i) throws SystemException { + + } + + @Override + public Transaction suspend() throws SystemException { + return null; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/lazyonetoone/LazyOneToOneWithEntityGraphTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/lazyonetoone/LazyOneToOneWithEntityGraphTest.java new file mode 100644 index 000000000000..817ab91217c7 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/lazyonetoone/LazyOneToOneWithEntityGraphTest.java @@ -0,0 +1,117 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.lazyonetoone; + +import java.util.List; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.NamedAttributeNode; +import jakarta.persistence.NamedEntityGraph; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.hibernate.Hibernate.isInitialized; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@DomainModel( + annotatedClasses = { + LazyOneToOneWithEntityGraphTest.Company.class, + LazyOneToOneWithEntityGraphTest.Employee.class, + LazyOneToOneWithEntityGraphTest.Project.class + } +) +@SessionFactory +@BytecodeEnhanced(runNotEnhancedAsWell = true) +public class LazyOneToOneWithEntityGraphTest { + @BeforeAll + void setUp(SessionFactoryScope scope) { + scope.inTransaction(session -> { + // Create company + Company company = new Company(); + company.id = 1L; + company.name = "Hibernate"; + session.persist(company); + + // Create project + Project project = new Project(); + project.id = 1L; + session.persist(project); + + // Create employee + Employee employee = new Employee(); + employee.id = 1L; + employee.company = company; + employee.projects = List.of(project); + session.persist(employee); + }); + } + + @AfterAll + void tearDown(SessionFactoryScope scope) { + scope.inTransaction(session -> { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + }); + } + + + @Test + void reproducerTest(SessionFactoryScope scope) { + scope.inTransaction(session -> { + // Load employee using entity graph + Employee employee = session.createQuery( + "select e from Employee e where e.id = :id", Employee.class) + .setParameter("id", 1L) + .setHint("javax.persistence.fetchgraph", session.getEntityGraph("employee.projects")) + .getSingleResult(); + + assertTrue(isInitialized(employee.projects)); + assertEquals("Hibernate", employee.company.name); + }); + } + + @Entity(name = "Company") + public static class Company { + @Id + private Long id; + + private String name; + } + + @Entity(name = "Employee") + @NamedEntityGraph( + name = "employee.projects", + attributeNodes = @NamedAttributeNode("projects") + ) + public static class Employee { + @Id + private Long id; + + @OneToOne + @JoinColumn(name = "company_name", referencedColumnName = "name") + private Company company; + + @OneToMany(fetch = FetchType.LAZY) + private List projects; + } + + @Entity(name = "Project") + public static class Project { + @Id + private Long id; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/length/LengthTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/length/LengthTest.java index d1aba0d93c2c..bc1178e65fd9 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/length/LengthTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/length/LengthTest.java @@ -1,6 +1,7 @@ package org.hibernate.orm.test.length; import org.hibernate.Length; +import org.hibernate.community.dialect.InformixDialect; import org.hibernate.dialect.Dialect; import org.hibernate.metamodel.mapping.BasicValuedMapping; import org.hibernate.type.SqlTypes; @@ -8,12 +9,14 @@ import org.hibernate.testing.orm.junit.DomainModel; import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.SkipForDialect; import org.junit.jupiter.api.Test; import static org.junit.Assert.assertEquals; @SessionFactory @DomainModel(annotatedClasses = {WithLongStrings.class,WithLongTypeStrings.class}) +@SkipForDialect(dialectClass = InformixDialect.class, reason = "Informix rowsize to exceed the allowable limit (32767).") public class LengthTest { @Test public void testLength(SessionFactoryScope scope) { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/manytomany/generic/ManyToManyGenericTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/manytomany/generic/ManyToManyGenericTest.java new file mode 100644 index 000000000000..d5385faf4d09 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/manytomany/generic/ManyToManyGenericTest.java @@ -0,0 +1,98 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.manytomany.generic; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinTable; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.Jpa; +import org.junit.jupiter.api.Test; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +@Jpa( + annotatedClasses = { + ManyToManyGenericTest.NodeTree.class, + ManyToManyGenericTest.GenericNode.class + } +) +public class ManyToManyGenericTest { + + @Test + void testSelfReferencingGeneric(final EntityManagerFactoryScope scope) { + final UUID treeId = scope.fromTransaction(em -> { + final NodeTree tree = new NodeTree(); + final GenericNode root = new GenericNode<>(); + root.tree = tree; + final GenericNode branch = new GenericNode<>(); + branch.tree = tree; + tree.nodes.add(root); + tree.nodes.add(branch); + root.children.add(branch); + em.persist(tree); + return tree.id; + }); + + final NodeTree nodeTree = scope.fromEntityManager(em -> em.find(NodeTree.class, treeId)); + + assertThat(nodeTree, is(notNullValue())); + assertThat(nodeTree.id, is(treeId)); + assertThat(nodeTree.nodes, iterableWithSize(2)); + assertThat(nodeTree.nodes, containsInAnyOrder(List.of( + hasProperty("children", iterableWithSize(1)), + hasProperty("children", emptyIterable()) + ))); + } + + @Entity(name = "tree") + public static class NodeTree { + @Id + @GeneratedValue(strategy = GenerationType.UUID) + public UUID id; + + @OneToMany(mappedBy = "tree", fetch = FetchType.EAGER, cascade = CascadeType.ALL) + public Set> nodes = new HashSet<>(); + } + + @Entity(name = "node") + public static class GenericNode { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + public UUID id; + + @ManyToOne(optional = false) + @JoinColumn(name = "TREE_ID") + public NodeTree tree; + + @ManyToMany(fetch = FetchType.EAGER, cascade = {CascadeType.PERSIST, CascadeType.DETACH}) + @JoinTable(name = "NODE_CHILDREN", + joinColumns = {@JoinColumn(name = "TREE_ID", referencedColumnName = "TREE_ID"), @JoinColumn(name = "NODE_ID", referencedColumnName = "ID")}, + inverseJoinColumns = {@JoinColumn(name = "CHILD_ID", referencedColumnName = "ID")} + ) + private final Set> children = new HashSet<>(); + + public Set> getChildren() { + return children; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/manytomany/generic/ManyToManyNonGenericTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/manytomany/generic/ManyToManyNonGenericTest.java new file mode 100644 index 000000000000..8a10874c8b92 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/manytomany/generic/ManyToManyNonGenericTest.java @@ -0,0 +1,109 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.manytomany.generic; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinTable; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.Jpa; +import org.junit.jupiter.api.Test; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.emptyIterable; +import static org.hamcrest.Matchers.hasProperty; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.iterableWithSize; +import static org.hamcrest.Matchers.notNullValue; + +@Jpa( + annotatedClasses = { + ManyToManyNonGenericTest.NodeTree.class, + ManyToManyNonGenericTest.Node.class + } +) +public class ManyToManyNonGenericTest { + + @Test + void testSelfReferencingGeneric(final EntityManagerFactoryScope scope) { + final UUID treeId = scope.fromTransaction(em -> { + final NodeTree tree = new NodeTree(); + final Node root = new Node(); + root.tree = tree; + final Node branch = new Node(); + branch.tree = tree; + tree.nodes.add(root); + tree.nodes.add(branch); + root.children.add(branch); + em.persist(tree); + return tree.id; + }); + + final NodeTree nodeTree = scope.fromEntityManager(em -> em.find(NodeTree.class, treeId)); + + assertThat(nodeTree, is(notNullValue())); + assertThat(nodeTree.id, is(treeId)); + assertThat(nodeTree.nodes, iterableWithSize(2)); + assertThat(nodeTree.nodes, containsInAnyOrder(List.of( + hasProperty("children", iterableWithSize(1)), + hasProperty("children", emptyIterable()) + ))); + } + + @Entity(name = "tree") + public static class NodeTree { + @Id + @GeneratedValue(strategy = GenerationType.UUID) + public UUID id; + + @OneToMany(mappedBy = "tree", fetch = FetchType.EAGER, cascade = CascadeType.ALL) + public Set nodes = new HashSet<>(); + } + + @Entity(name = "node") + public static class Node { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + public UUID id; + + @ManyToOne(optional = false) + @JoinColumn(name = "TREE_ID") + public NodeTree tree; + + @ManyToMany(fetch = FetchType.EAGER, cascade = {CascadeType.PERSIST, CascadeType.DETACH}) + @JoinTable(name = "NODE_CHILDREN", + joinColumns = {@JoinColumn(name = "TREE_ID", referencedColumnName = "TREE_ID"), @JoinColumn(name = "NODE_ID", referencedColumnName = "ID")}, + inverseJoinColumns = {@JoinColumn(name = "CHILD_ID", referencedColumnName = "ID")} + ) + private final Set children = new HashSet<>(); + + public Set getChildren() { + return children; + } + + @Override + public String toString() { + return String.format("node [%s] parent of %s", id, children.stream().map(n -> n.id).collect(Collectors.toList())); + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/array/ArrayOfArraysTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/array/ArrayOfArraysTest.java new file mode 100644 index 000000000000..0f7becd61deb --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/array/ArrayOfArraysTest.java @@ -0,0 +1,98 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.mapping.array; + +import org.hibernate.MappingException; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.cfg.Configuration; +import org.hibernate.dialect.CockroachDialect; +import org.hibernate.testing.orm.junit.SkipForDialect; +import org.hibernate.type.SqlTypes; + +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import org.assertj.core.data.Index; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +/** + * @author Marco Belladelli + */ +@RequiresDialectFeature( feature = DialectFeatureChecks.SupportsStandardArrays.class ) +public class ArrayOfArraysTest { + @DomainModel( annotatedClasses = ArrayOfArraysTest.EntityWithDoubleByteArray.class ) + @SessionFactory + @ServiceRegistry( settings = @Setting( name = AvailableSettings.HBM2DDL_AUTO, value = "create-drop" ) ) + @Test + @SkipForDialect( dialectClass = CockroachDialect.class, reason = "Unable to find server array type for provided name bytes" ) + public void testDoubleByteArrayWorks(SessionFactoryScope scope) { + final Long id = scope.fromTransaction( session -> { + final EntityWithDoubleByteArray entity = new EntityWithDoubleByteArray(); + entity.setByteArray( new byte[][] { new byte[] { 1 } } ); + session.persist( entity ); + return entity.getId(); + } ); + scope.inSession( session -> { + final byte[][] byteArray = session.find( EntityWithDoubleByteArray.class, id ).getByteArray(); + assertThat( byteArray ).hasDimensions( 1, 1 ).contains( new byte[] { 1 }, Index.atIndex( 0 ) ); + } ); + } + + @Test + public void testDoubleIntegerArrayThrows() { + final Configuration cfg = new Configuration(); + cfg.addAnnotatedClass( EntityWithDoubleIntegerArray.class ); + try (final org.hibernate.SessionFactory sf = cfg.buildSessionFactory()) { + fail( "Expecting Integer[][] to trigger exception as non-byte multidimensional arrays are not supported" ); + } + catch (Exception e) { + assertThat( e ).isInstanceOf( MappingException.class ).hasMessage( "Nested arrays (with the exception of byte[][]) are not supported" ); + } + } + + @Entity( name = "EntityWithDoubleByteArray" ) + static class EntityWithDoubleByteArray { + @Id + @GeneratedValue + private Long id; + + @JdbcTypeCode( SqlTypes.ARRAY ) + private byte[][] byteArray; + + public Long getId() { + return id; + } + + public byte[][] getByteArray() { + return byteArray; + } + + public void setByteArray(byte[][] byteArray) { + this.byteArray = byteArray; + } + } + + @Entity( name = "EntityWithDoubleIntegerArray" ) + static class EntityWithDoubleIntegerArray { + @Id + @GeneratedValue + private Long id; + + @JdbcTypeCode( SqlTypes.ARRAY ) + private Integer[][] integers; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/array/MySqlArrayOfTimestampsTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/array/MySqlArrayOfTimestampsTest.java new file mode 100644 index 000000000000..bb2c636e5058 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/array/MySqlArrayOfTimestampsTest.java @@ -0,0 +1,172 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.mapping.array; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; + +import org.hibernate.dialect.MariaDBDialect; +import org.hibernate.dialect.MySQLDialect; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.RequiresDialect; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.type.descriptor.java.JdbcTimestampJavaType; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; + +import java.sql.Timestamp; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.Month; +import java.util.Calendar; +import java.util.Date; +import java.util.TimeZone; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel(annotatedClasses = MySqlArrayOfTimestampsTest.Foo.class) +@SessionFactory +@JiraKey("HHH-18881") +class MySqlArrayOfTimestampsTest { + + private static final LocalDateTime[] dataArray = { + // Unix epoch start if you're in the UK + LocalDateTime.of( 1970, Month.JANUARY, 1, 0, 0, 0, 0 ), + // pre-Y2K + LocalDateTime.of( 1999, Month.DECEMBER, 31, 23, 59, 59, 0 ), + // We survived! Why was anyone worried? + LocalDateTime.of( 2000, Month.JANUARY, 1, 0, 0, 0, 0 ), + // Silence will fall! + LocalDateTime.of( 2010, Month.JUNE, 26, 20, 4, 0, 0 ), + // 2024 summer time + LocalDateTime.of( 2024, 6, 20, 0, 0, 0 ), + // 2023 winer time + LocalDateTime.of( 2023, 12, 22, 0, 0, 0 ) + }; + + private TimeZone currentDefault; + + @BeforeAll + void setTimeZone() { + currentDefault = TimeZone.getDefault(); + TimeZone.setDefault( TimeZone.getTimeZone( "Europe/Zagreb" ) ); + } + + @AfterAll + void restoreTimeZone() { + TimeZone.setDefault( currentDefault ); + } + + @Test + @Order(1) + @RequiresDialect(MySQLDialect.class) + @RequiresDialect(MariaDBDialect.class) + public void testLocalDateTime(SessionFactoryScope scope) { + + final Integer basicId = scope.fromTransaction( session -> { + Foo basic = new Foo(); + basic.localDateTimeArray = dataArray; + basic.localDateTimeField = dataArray[0]; + session.persist( basic ); + return basic.id; + } ); + + scope.inTransaction( session -> { + Foo found = session.find( Foo.class, basicId ); + assertThat( found.localDateTimeField ).isEqualTo( dataArray[0] ); + assertThat( found.localDateTimeArray ).isEqualTo( dataArray ); + } ); + } + + + @Test + @Order(2) + @RequiresDialect(MySQLDialect.class) + @RequiresDialect(MariaDBDialect.class) + public void testDate(SessionFactoryScope scope) { + Date[] dataArray = {Calendar.getInstance().getTime(), Calendar.getInstance().getTime()}; + + final Integer basicId = scope.fromTransaction( session -> { + Foo basic = new Foo(); + basic.dateArray = dataArray; + basic.dateField = dataArray[0]; + session.persist( basic ); + return basic.id; + } ); + + scope.inTransaction( session -> { + Foo found = session.find( Foo.class, basicId ); + assertThat( found.dateField.getTime() ).isEqualTo( dataArray[0].getTime() ); + for ( int i = 0; i < dataArray.length; i++ ) { + assertThat( found.dateArray[i].getTime() ).isEqualTo( dataArray[i].getTime() ); + } + } ); + } + + private static final LocalDateTime SUMMER = LocalDate.of( 2024, 6, 20 ).atStartOfDay(); + private static final LocalDateTime WINTER = LocalDate.of( 2023, 12, 22 ).atStartOfDay(); + private static final LocalDate EPOCH = LocalDate.of( 1970, Month.JANUARY, 1 ); + + private static final TimeZone[] TEST_TIME_ZONES = Stream.of( + "Africa/Monrovia", + "Europe/Zagreb", + "Asia/Singapore", + "Europe/Tallinn", + "Europe/Minsk", + "America/Anchorage" + ).map( TimeZone::getTimeZone ).toArray( TimeZone[]::new ); + + @Test + void encodeThenDecodeLocalDateTime() { + for ( final TimeZone zone : TEST_TIME_ZONES ) { + final TimeZone currentTimeZone = TimeZone.getDefault(); + TimeZone.setDefault( zone ); + try { + for ( LocalDateTime dateTime : dataArray ) { + final MySqlAppender appender = new MySqlAppender(); + final Timestamp expected = Timestamp.valueOf( dateTime ); + JdbcTimestampJavaType.INSTANCE.appendEncodedString( appender, expected ); + final Date actual = JdbcTimestampJavaType.INSTANCE.fromEncodedString( appender.stringBuilder, 0, + appender.stringBuilder.length() ); + Assertions.assertEquals( expected, actual ); + } + } + finally { + TimeZone.setDefault( currentTimeZone ); + } + } + } + + @Entity(name = "Foo") + public static class Foo { + @Id + @GeneratedValue + public Integer id; + public Date[] dateArray; + public LocalDateTime[] localDateTimeArray; + public Date dateField; + public LocalDateTime localDateTimeField; + } + + private static class MySqlAppender implements SqlAppender { + + private final StringBuilder stringBuilder = new StringBuilder(); + + @Override + public void appendSql(String fragment) { + stringBuilder.append( fragment ); + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/BlobAttributeQueryUpdateTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/BlobAttributeQueryUpdateTest.java new file mode 100644 index 000000000000..05afcf6cf1f8 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/BlobAttributeQueryUpdateTest.java @@ -0,0 +1,172 @@ +package org.hibernate.orm.test.mapping.basic; + +import java.nio.charset.StandardCharsets; +import java.sql.Blob; +import java.sql.SQLException; + +import org.hibernate.LobHelper; +import org.hibernate.engine.jdbc.BlobProxy; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.TypedQuery; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +@DomainModel( + annotatedClasses = { + BlobAttributeQueryUpdateTest.TestEntity.class + } +) +@SessionFactory +@JiraKey("HHH-18480") +public class BlobAttributeQueryUpdateTest { + + private static final byte[] INITIAL_BYTES = "initial".getBytes( StandardCharsets.UTF_8 ); + private static final byte[] UPDATED_BYTES_1 = "update1".getBytes( StandardCharsets.UTF_8 ); + private static final byte[] UPDATED_BYTES_2 = "update2".getBytes( StandardCharsets.UTF_8 ); + + @BeforeEach + public void setup(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + LobHelper lobHelper = session.getLobHelper(); + TestEntity testEntity = new TestEntity( + 1, + "test", + lobHelper.createBlob( INITIAL_BYTES ) + ); + session.persist( testEntity ); + } + ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( + session -> + session.createMutationQuery( "delete from TestEntity" ).executeUpdate() + ); + } + + @Test + public void testUpdateUsingBlobProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + TestEntity testEntity = session.find( TestEntity.class, 1 ); + Blob blobValue1 = BlobProxy.generateProxy( UPDATED_BYTES_1 ); + testEntity.setBlobValue( blobValue1 ); + } ); + + scope.inTransaction( session -> { + TestEntity testEntity = session.find( TestEntity.class, 1 ); + checkBlobValue( testEntity, UPDATED_BYTES_1 ); + } ); + + scope.inTransaction( + session -> { + TypedQuery query = session.createQuery( + "UPDATE TestEntity b SET b.blobValue = :blobValue WHERE b.id = :id", + null + ); + query.setParameter( "id", 1 ); + Blob value = BlobProxy.generateProxy( UPDATED_BYTES_2 ); + query.setParameter( "blobValue", value ); + query.executeUpdate(); + } + ); + + scope.inTransaction( + session -> { + TestEntity testEntity = session.find( TestEntity.class, 1 ); + checkBlobValue( testEntity, UPDATED_BYTES_2 ); + } + ); + } + + @Test + public void testUpdateUsingLobHelper(SessionFactoryScope scope) { + scope.inTransaction( session -> { + LobHelper lobHelper = session.getLobHelper(); + TestEntity testEntity = session.find( TestEntity.class, 1 ); + Blob blobValue1 = lobHelper.createBlob( UPDATED_BYTES_1 ); + testEntity.setBlobValue( blobValue1 ); + } ); + + scope.inTransaction( session -> { + TestEntity testEntity = session.find( TestEntity.class, 1 ); + checkBlobValue( testEntity, UPDATED_BYTES_1 ); + } ); + + + scope.inTransaction( + session -> { + LobHelper lobHelper = session.getLobHelper(); + TypedQuery query = session.createQuery( + "UPDATE TestEntity b SET b.blobValue = :blobValue WHERE b.id = :id", + null + ); + query.setParameter( "id", 1 ); + Blob value = lobHelper.createBlob( UPDATED_BYTES_2 ); + query.setParameter( "blobValue", value ); + query.executeUpdate(); + } + ); + + scope.inTransaction( + session -> { + TestEntity testEntity = session.find( TestEntity.class, 1 ); + checkBlobValue( testEntity, UPDATED_BYTES_2 ); + } + ); + } + + private static void checkBlobValue(TestEntity testEntity, byte[] expectedValue) { + try { + assertThat( new String( testEntity.getBlobValue() + .getBytes( 1, (int) testEntity.getBlobValue().length() ) ) ) + .isEqualTo( new String( expectedValue ) ); + } + catch (SQLException e) { + throw new RuntimeException( e ); + } + } + + @Entity(name = "TestEntity") + public static class TestEntity { + @Id + private Integer id; + + private String name; + + private Blob blobValue; + + public TestEntity() { + } + + public TestEntity(Integer id, String name, Blob blobValue) { + this.id = id; + this.name = name; + this.blobValue = blobValue; + } + + public Integer getId() { + return id; + } + + public Blob getBlobValue() { + return blobValue; + } + + public void setBlobValue(Blob blobValue) { + this.blobValue = blobValue; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/JsonMappingTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/JsonMappingTests.java index f3187767f214..6195e5556d1d 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/JsonMappingTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/JsonMappingTests.java @@ -12,25 +12,24 @@ import java.util.List; import java.util.Map; -import com.fasterxml.jackson.databind.JsonNode; -import jakarta.json.JsonValue; import org.hibernate.annotations.JdbcTypeCode; import org.hibernate.cfg.AvailableSettings; import org.hibernate.community.dialect.AltibaseDialect; import org.hibernate.dialect.AbstractHANADialect; import org.hibernate.dialect.DerbyDialect; import org.hibernate.dialect.OracleDialect; -import org.hibernate.dialect.PostgreSQLDialect; import org.hibernate.dialect.SybaseDialect; import org.hibernate.metamodel.mapping.internal.BasicAttributeMapping; import org.hibernate.metamodel.spi.MappingMetamodelImplementor; import org.hibernate.persister.entity.EntityPersister; -import org.hibernate.testing.orm.junit.RequiresDialect; +import org.hibernate.query.MutationQuery; +import org.hibernate.query.criteria.HibernateCriteriaBuilder; import org.hibernate.type.SqlTypes; import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; import org.hibernate.testing.orm.junit.JiraKey; import org.hibernate.testing.orm.junit.ServiceRegistry; import org.hibernate.testing.orm.junit.SessionFactory; @@ -41,15 +40,19 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import com.fasterxml.jackson.databind.JsonNode; +import jakarta.json.JsonValue; import jakarta.persistence.Entity; import jakarta.persistence.Id; import jakarta.persistence.Table; +import jakarta.persistence.criteria.CriteriaUpdate; +import jakarta.persistence.criteria.Root; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.isOneOf; import static org.hamcrest.Matchers.isA; +import static org.hamcrest.Matchers.isOneOf; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; @@ -124,7 +127,8 @@ public void verifyMappings(SessionFactoryScope scope) { "objectMap" ); final BasicAttributeMapping listAttribute = (BasicAttributeMapping) entityDescriptor.findAttributeMapping( "list" ); - final BasicAttributeMapping jsonAttribute = (BasicAttributeMapping) entityDescriptor.findAttributeMapping( "jsonString" ); + final BasicAttributeMapping jsonAttribute = (BasicAttributeMapping) entityDescriptor.findAttributeMapping( + "jsonString" ); assertThat( stringMapAttribute.getJavaType().getJavaTypeClass(), equalTo( Map.class ) ); assertThat( objectMapAttribute.getJavaType().getJavaTypeClass(), equalTo( Map.class ) ); @@ -146,8 +150,8 @@ public void verifyReadWorks(SessionFactoryScope scope) { assertThat( entityWithJson.stringMap, is( stringMap ) ); assertThat( entityWithJson.objectMap, is( objectMap ) ); assertThat( entityWithJson.list, is( list ) ); - assertThat( entityWithJson.jsonNode, is( nullValue() )); - assertThat( entityWithJson.jsonValue, is( nullValue() )); + assertThat( entityWithJson.jsonNode, is( nullValue() ) ); + assertThat( entityWithJson.jsonValue, is( nullValue() ) ); } ); } @@ -167,14 +171,14 @@ public void verifyMergeWorks(SessionFactoryScope scope) { assertThat( entityWithJson.objectMap, is( nullValue() ) ); assertThat( entityWithJson.list, is( nullValue() ) ); assertThat( entityWithJson.jsonString, is( nullValue() ) ); - assertThat( entityWithJson.jsonNode, is( nullValue() )); - assertThat( entityWithJson.jsonValue, is( nullValue() )); + assertThat( entityWithJson.jsonNode, is( nullValue() ) ); + assertThat( entityWithJson.jsonValue, is( nullValue() ) ); } ); } @Test - @JiraKey( "HHH-16682" ) + @JiraKey("HHH-16682") public void verifyDirtyChecking(SessionFactoryScope scope) { scope.inTransaction( (session) -> { @@ -198,7 +202,7 @@ public void verifyDirtyChecking(SessionFactoryScope scope) { @SkipForDialect(dialectClass = AltibaseDialect.class, reason = "Altibase doesn't support comparing CLOBs with the = operator") public void verifyComparisonWorks(SessionFactoryScope scope) { scope.inTransaction( - (session) -> { + (session) -> { // PostgreSQL returns the JSON slightly formatted String alternativeJson = "{\"name\": \"abc\"}"; EntityWithJson entityWithJson = session.createQuery( @@ -243,6 +247,32 @@ else if ( nativeJson instanceof Clob ) { ); } + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-18709") + public void verifyCriteriaUpdateQueryWorks(SessionFactoryScope scope) { + final Map newMap = Map.of( "name", "ABC" ); + final List newList = List.of( new StringNode( "ABC" ) ); + final String newJson = "{\"count\":123}"; + scope.inTransaction( session -> { + final HibernateCriteriaBuilder builder = session.getCriteriaBuilder(); + final CriteriaUpdate criteria = builder.createCriteriaUpdate( EntityWithJson.class ); + final Root root = criteria.from( EntityWithJson.class ); + criteria.set( root.get( "stringMap" ), newMap ); + criteria.set( "list", newList ); + criteria.set( root.get( "jsonString" ), newJson ); + criteria.where( builder.equal( root.get( "id" ), 1 ) ); + final MutationQuery query = session.createMutationQuery( criteria ); + final int count = query.executeUpdate(); + assertThat( count, is( 1 ) ); + } ); + scope.inSession( session -> { + final EntityWithJson entityWithJson = session.find( EntityWithJson.class, 1 ); + assertThat( entityWithJson.stringMap, is( newMap ) ); + assertThat( entityWithJson.list, is( newList ) ); + assertThat( entityWithJson.jsonString.replaceAll( "\\s", "" ), is( newJson ) ); + } ); + } + @Entity(name = "EntityWithJson") @Table(name = "EntityWithJson") public static class EntityWithJson { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/converted/converter/object/ConvertedClassAttributeTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/converted/converter/object/ConvertedClassAttributeTest.java new file mode 100644 index 000000000000..ae2190ed1d0a --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/converted/converter/object/ConvertedClassAttributeTest.java @@ -0,0 +1,119 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.mapping.converted.converter.object; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Convert; +import jakarta.persistence.Converter; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Marco Belladelli + */ +@DomainModel( annotatedClasses = { + ConvertedClassAttributeTest.EntityWithStatus.class, + Status.class, +} ) +@SessionFactory +@Jira( "https://hibernate.atlassian.net/browse/HHH-18564" ) +public class ConvertedClassAttributeTest { + @Test + public void testConvertedAttributeSelection(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var resultList = session.createQuery( + "select t.status from EntityWithStatus t", + Status.class + ).getResultList(); + assertThat( resultList ).hasSize( 2 ).containsExactlyInAnyOrder( Status.ONE, Status.TWO ); + } ); + } + + @Test + public void testLiteralPredicateDomainForm(SessionFactoryScope scope) { + scope.inTransaction( session -> { + var result = session.createQuery( + String.format( "select t from EntityWithStatus t where t.status > %s.ONE", Status.class.getName() ), + EntityWithStatus.class + ).getSingleResult(); + assertThat( result.getStatus().getValue() ).isEqualTo( 2 ); + } ); + } + + @Test + public void testLiteralPredicateRelationalForm(SessionFactoryScope scope) { + scope.inTransaction( session -> { + var result = session.createQuery( + "select t from EntityWithStatus t where t.status > 1", + EntityWithStatus.class + ).getSingleResult(); + assertThat( result.getStatus().getValue() ).isEqualTo( 2 ); + } ); + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final EntityWithStatus entity1 = new EntityWithStatus(); + entity1.setStatus( Status.ONE ); + session.persist( entity1 ); + final EntityWithStatus entity2 = new EntityWithStatus(); + entity2.setStatus( Status.TWO ); + session.persist( entity2 ); + } ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Entity( name = "EntityWithStatus" ) + static class EntityWithStatus { + @Id + @GeneratedValue + private Long id; + + @Convert( converter = StatusConverter.class ) + private Status status; + + public Long getId() { + return id; + } + + public Status getStatus() { + return status; + } + + public void setStatus(Status status) { + this.status = status; + } + } + + @Converter + static class StatusConverter implements AttributeConverter { + @Override + public Integer convertToDatabaseColumn(Status attribute) { + return attribute == null ? null : attribute.getValue(); + } + + @Override + public Status convertToEntityAttribute(Integer dbData) { + return dbData == null ? null : Status.from( dbData ); + } + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/converted/converter/object/Status.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/converted/converter/object/Status.java new file mode 100644 index 000000000000..c862e197d1c6 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/converted/converter/object/Status.java @@ -0,0 +1,53 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.mapping.converted.converter.object; + +import org.hibernate.annotations.Imported; + +/** + * @author Marco Belladelli + */ +@Imported +public class Status { + public static Status ONE = new Status( 1 ); + public static Status TWO = new Status( 2 ); + + private final int value; + + public Status(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + public static Status from(int value) { + return new Status( value ); + } + + @Override + public boolean equals(Object o) { + if ( this == o ) { + return true; + } + if ( o == null || getClass() != o.getClass() ) { + return false; + } + + final Status status = (Status) o; + return value == status.value; + } + + @Override + public int hashCode() { + return value; + } + + @Override + public String toString() { + return "Status{" + "value=" + value + '}'; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/generated/GeneratedNoOpUpdateTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/generated/GeneratedNoOpUpdateTest.java new file mode 100644 index 000000000000..bdd8489b9676 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/generated/GeneratedNoOpUpdateTest.java @@ -0,0 +1,182 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.mapping.generated; + +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import org.hibernate.annotations.CurrentTimestamp; + +import org.hibernate.testing.jdbc.SQLStatementInspector; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Marco Belladelli + */ +@DomainModel( annotatedClasses = { + GeneratedNoOpUpdateTest.Pizza.class, + GeneratedNoOpUpdateTest.Topping.class, +} ) +@SessionFactory(useCollectingStatementInspector = true) +@Jira( "https://hibernate.atlassian.net/browse/HHH-18484" ) +public class GeneratedNoOpUpdateTest { + @Test + public void testUpdate(SessionFactoryScope scope) { + final SQLStatementInspector inspector = scope.getCollectingStatementInspector(); + inspector.clear(); + + final ZonedDateTime updatedTime = scope.fromTransaction( session -> { + final Pizza pizza = session.find( Pizza.class, 1L ); + final ZonedDateTime initialTime = pizza.getLastUpdated(); + // Create a new topping + final Topping newTopping1 = new Topping(); + newTopping1.setName( "Cheese" ); + newTopping1.setPizza( pizza ); + // Let's mutate the existing list + pizza.getToppings().add( newTopping1 ); + session.flush(); + // pizza was not dirty so no update is executed + inspector.assertNoUpdate(); + assertThat( pizza.getLastUpdated() ).isEqualTo( initialTime ); + return pizza.getLastUpdated(); + } ); + + inspector.clear(); + scope.inTransaction( session -> { + // Now let's try adding a new topping via a new list + final Pizza pizza = session.find( Pizza.class, 1L ); + // Create a new topping + final Topping newTopping2 = new Topping(); + newTopping2.setName( "Mushroom" ); + newTopping2.setPizza( pizza ); + // This time, instead of mutating the existing list, we're creating a new list + pizza.setToppings( List.of( pizza.getToppings().get( 0 ), newTopping2 ) ); + session.flush(); + // pizza this time was dirty, but still no update is executed because + // only the unowned one-to-many association has changed + inspector.assertNoUpdate(); + assertThat( pizza.getLastUpdated() ).isEqualTo( updatedTime ); + } ); + + scope.inTransaction( session -> { + final Pizza pizza = session.find( Pizza.class, 1L ); + assertThat( pizza.getToppings() ).hasSize( 3 ) + .extracting( Topping::getName ) + .containsExactlyInAnyOrder( "Pepperoni", "Cheese", "Mushroom" ); + // This time we mutate the pizza to trigger a real update + pizza.setName( "Salamino e funghi" ); + session.flush(); + assertThat( inspector.getSqlQueries() ).anyMatch( sql -> sql.toLowerCase( Locale.ROOT ).contains( "update" ) ); + assertThat( pizza.getLastUpdated() ).isAfter( updatedTime ); + } ); + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final Pizza pizza = new Pizza( 1L, "Salamino" ); + session.persist( pizza ); + final Topping topping = new Topping(); + topping.setName( "Pepperoni" ); + topping.setPizza( pizza ); + pizza.getToppings().add( topping ); + } ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Entity( name = "Pizza" ) + static class Pizza { + @Id + private Long id; + + @OneToMany( mappedBy = "pizza", cascade = CascadeType.ALL ) + private List toppings = new ArrayList<>(); + + @CurrentTimestamp + private ZonedDateTime lastUpdated; + + private String name; + + public Pizza() { + } + + public Pizza(Long id, String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public List getToppings() { + return toppings; + } + + public void setToppings(final List toppings) { + this.toppings = toppings; + } + + public ZonedDateTime getLastUpdated() { + return lastUpdated; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + @Entity( name = "Topping" ) + static class Topping { + @Id + @GeneratedValue + private Long id; + + @ManyToOne + private Pizza pizza; + + private String name; + + public void setName(final String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setPizza(final Pizza pizza) { + this.pizza = pizza; + } + + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/generated/always/GeneratedAlwaysTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/generated/always/GeneratedAlwaysTest.java index ec6ac851b948..0fd10efb51d4 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/generated/always/GeneratedAlwaysTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/generated/always/GeneratedAlwaysTest.java @@ -6,6 +6,7 @@ import org.hibernate.annotations.GeneratedColumn; import org.hibernate.community.dialect.AltibaseDialect; import org.hibernate.dialect.DerbyDialect; +import org.hibernate.community.dialect.InformixDialect; import org.hibernate.dialect.H2Dialect; import org.hibernate.dialect.HSQLDialect; import org.hibernate.dialect.PostgreSQLDialect; @@ -33,6 +34,7 @@ @SkipForDialect(dialectClass = PostgreSQLDialect.class, majorVersion = 10, matchSubTypes = true) @SkipForDialect(dialectClass = PostgreSQLDialect.class, majorVersion = 11, matchSubTypes = true) // 'generated always' was added in 12 @SkipForDialect(dialectClass = AltibaseDialect.class, reason = "generated always is not supported in Altibase") +@SkipForDialect(dialectClass = InformixDialect.class) public class GeneratedAlwaysTest { @Test diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/identifier/CompositeIdGenerationTypeTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/identifier/CompositeIdGenerationTypeTest.java new file mode 100644 index 000000000000..a5e4d4099fb1 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/identifier/CompositeIdGenerationTypeTest.java @@ -0,0 +1,270 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.mapping.identifier; + +import java.util.UUID; + +import org.hibernate.annotations.UuidGenerator; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.IdClass; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +/** + * @author Marco Belladelli + */ +@DomainModel( annotatedClasses = { + CompositeIdGenerationTypeTest.SingleIdClass.class, + CompositeIdGenerationTypeTest.MultipleIdClass.class, + CompositeIdGenerationTypeTest.IdClassPK.class, + CompositeIdGenerationTypeTest.SingleEmbeddedId.class, + CompositeIdGenerationTypeTest.SingleEmbeddedPK.class, + CompositeIdGenerationTypeTest.MultipleEmbeddedId.class, + CompositeIdGenerationTypeTest.MultipleEmbeddedPK.class, +} ) +@SessionFactory +@Jira( "https://hibernate.atlassian.net/browse/HHH-18524" ) +public class CompositeIdGenerationTypeTest { + @Test + public void testSingleIdClass(SessionFactoryScope scope) { + scope.inTransaction( session -> session.persist( new SingleIdClass( 1L, "id_class_1" ) ) ); + scope.inSession( session -> { + final SingleIdClass result = session.createQuery( "from SingleIdClass", SingleIdClass.class ) + .getSingleResult(); + assertThat( result.getId() ).isEqualTo( 1L ); + assertDoesNotThrow( () -> UUID.fromString( result.getUuid() ) ); + } ); + } + + @Test + public void testMultipleIdClass(SessionFactoryScope scope) { + scope.inTransaction( session -> session.persist( new MultipleIdClass( "id_class_2" ) ) ); + scope.inSession( session -> { + final MultipleIdClass result = session.createQuery( "from MultipleIdClass", MultipleIdClass.class ) + .getSingleResult(); + assertThat( result.getId() ).isGreaterThan( 0 ); + assertDoesNotThrow( () -> UUID.fromString( result.getUuid() ) ); + } ); + } + + @Test + public void testSingleEmbeddedId(SessionFactoryScope scope) { + scope.inTransaction( session -> session.persist( new SingleEmbeddedId( + new SingleEmbeddedPK( 1L ), + "embedded_id_1" + ) ) ); + scope.inSession( session -> { + final SingleEmbeddedId result = session.createQuery( "from SingleEmbeddedId", SingleEmbeddedId.class ) + .getSingleResult(); + final SingleEmbeddedPK embeddedId = result.getEmbeddedId(); + assertThat( embeddedId.getId() ).isEqualTo( 1L ); + assertDoesNotThrow( () -> UUID.fromString( embeddedId.getUuid() ) ); + } ); + } + + @Test + public void testMultipleEmbeddedId(SessionFactoryScope scope) { + scope.inTransaction( session -> session.persist( new MultipleEmbeddedId( + new MultipleEmbeddedPK(), + "embedded_id_2" + ) ) ); + + scope.inTransaction( session -> { + final MultipleEmbeddedId result = session.createQuery( "from MultipleEmbeddedId", MultipleEmbeddedId.class ) + .getSingleResult(); + final MultipleEmbeddedPK embeddedId = result.getEmbeddedId(); + assertThat( embeddedId.getId() ).isGreaterThan( 0 ); + assertDoesNotThrow( () -> UUID.fromString( embeddedId.getUuid() ) ); + } ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Entity( name = "SingleIdClass" ) + @IdClass( IdClassPK.class ) + static class SingleIdClass { + @Id + private Long id; + + @Id + @UuidGenerator + private String uuid; + + private String name; + + public SingleIdClass() { + } + + public SingleIdClass(Long id, String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public String getUuid() { + return uuid; + } + + public String getName() { + return name; + } + } + + @Embeddable + static class IdClassPK { + private Long id; + private String uuid; + + public IdClassPK() { + } + + public IdClassPK(Long id, String uuid) { + this.id = id; + this.uuid = uuid; + } + } + + @Entity( name = "MultipleIdClass" ) + @IdClass( IdClassPK.class ) + static class MultipleIdClass { + @Id + @GeneratedValue + private Long id; + + @Id + @UuidGenerator + private String uuid; + + private String name; + + public MultipleIdClass() { + } + + public MultipleIdClass(String name) { + this.name = name; + } + + public Long getId() { + return id; + } + + public String getUuid() { + return uuid; + } + + public String getName() { + return name; + } + } + + @Entity( name = "SingleEmbeddedId" ) + static class SingleEmbeddedId { + @EmbeddedId + private SingleEmbeddedPK embeddedId; + + private String name; + + public SingleEmbeddedId() { + } + + public SingleEmbeddedId(SingleEmbeddedPK embeddedId, String name) { + this.embeddedId = embeddedId; + this.name = name; + } + + public SingleEmbeddedPK getEmbeddedId() { + return embeddedId; + } + + public String getName() { + return name; + } + } + + @Embeddable + static class SingleEmbeddedPK { + private Long id; + + @UuidGenerator + private String uuid; + + public SingleEmbeddedPK() { + } + + public SingleEmbeddedPK(Long id) { + this.id = id; + } + + public Long getId() { + return id; + } + + public String getUuid() { + return uuid; + } + } + + @Entity( name = "MultipleEmbeddedId" ) + static class MultipleEmbeddedId { + @EmbeddedId + private MultipleEmbeddedPK embeddedId; + + private String name; + + public MultipleEmbeddedId() { + } + + public MultipleEmbeddedId(MultipleEmbeddedPK embeddedId, String name) { + this.embeddedId = embeddedId; + this.name = name; + } + + public MultipleEmbeddedPK getEmbeddedId() { + return embeddedId; + } + + public String getName() { + return name; + } + } + + @Embeddable + static class MultipleEmbeddedPK { + @GeneratedValue + private Long id; + + @UuidGenerator + private String uuid; + + public Long getId() { + return id; + } + + public String getUuid() { + return uuid; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/javatime/GlobalJavaTimeJdbcTypeTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/javatime/GlobalJavaTimeJdbcTypeTests.java index 26060e9de7bf..5e1b6e95e51a 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/javatime/GlobalJavaTimeJdbcTypeTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/javatime/GlobalJavaTimeJdbcTypeTests.java @@ -44,7 +44,7 @@ import jakarta.persistence.Table; import static org.assertj.core.api.Assertions.assertThat; -import static org.hibernate.type.descriptor.DateTimeUtils.roundToDefaultPrecision; +import static org.hibernate.type.descriptor.DateTimeUtils.adjustToDefaultPrecision; /** * Tests for "direct" JDBC handling of {@linkplain java.time Java Time} types. @@ -84,7 +84,7 @@ private void checkAttribute( @Test void testInstant(SessionFactoryScope scope) { final Dialect dialect = scope.getSessionFactory().getJdbcServices().getDialect(); - final Instant start = roundToDefaultPrecision( Instant.EPOCH, dialect ); + final Instant start = adjustToDefaultPrecision( Instant.EPOCH, dialect ); scope.inTransaction( (session) -> { final EntityWithJavaTimeValues entity = new EntityWithJavaTimeValues(); @@ -114,7 +114,7 @@ void testInstant(SessionFactoryScope scope) { @Test void testLocalDateTime(SessionFactoryScope scope) { final Dialect dialect = scope.getSessionFactory().getJdbcServices().getDialect(); - final LocalDateTime start = roundToDefaultPrecision( LocalDateTime.now(), dialect ); + final LocalDateTime start = adjustToDefaultPrecision( LocalDateTime.now(), dialect ); scope.inTransaction( (session) -> { final EntityWithJavaTimeValues entity = new EntityWithJavaTimeValues(); @@ -144,7 +144,7 @@ void testLocalDateTime(SessionFactoryScope scope) { @Test void testLocalDate(SessionFactoryScope scope) { final Dialect dialect = scope.getSessionFactory().getJdbcServices().getDialect(); - final LocalDate startTime = roundToDefaultPrecision( LocalDate.now(), dialect ); + final LocalDate startTime = adjustToDefaultPrecision( LocalDate.now(), dialect ); scope.inTransaction( (session) -> { final EntityWithJavaTimeValues entity = new EntityWithJavaTimeValues(); @@ -177,7 +177,7 @@ void testLocalDate(SessionFactoryScope scope) { @SkipForDialect(dialectClass = AltibaseDialect.class, reason = "Altibase drivers truncate fractional seconds from the LocalTime") void testLocalTime(SessionFactoryScope scope) { final Dialect dialect = scope.getSessionFactory().getJdbcServices().getDialect(); - final LocalTime startTime = roundToDefaultPrecision( LocalTime.now(), dialect ); + final LocalTime startTime = adjustToDefaultPrecision( LocalTime.now(), dialect ); scope.inTransaction( (session) -> { final EntityWithJavaTimeValues entity = new EntityWithJavaTimeValues(); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/javatime/JavaTimeJdbcTypeTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/javatime/JavaTimeJdbcTypeTests.java index 91085418acbc..481d8f8f2517 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/javatime/JavaTimeJdbcTypeTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/javatime/JavaTimeJdbcTypeTests.java @@ -45,7 +45,7 @@ import jakarta.persistence.Table; import static org.assertj.core.api.Assertions.assertThat; -import static org.hibernate.type.descriptor.DateTimeUtils.roundToDefaultPrecision; +import static org.hibernate.type.descriptor.DateTimeUtils.adjustToDefaultPrecision; /** * Tests for "direct" JDBC handling of {@linkplain java.time Java Time} types. @@ -85,7 +85,7 @@ private void checkAttribute( @Test void testInstant(SessionFactoryScope scope) { final Dialect dialect = scope.getSessionFactory().getJdbcServices().getDialect(); - final Instant start = roundToDefaultPrecision( Instant.EPOCH, dialect ); + final Instant start = adjustToDefaultPrecision( Instant.EPOCH, dialect ); scope.inTransaction( (session) -> { final EntityWithJavaTimeValues entity = new EntityWithJavaTimeValues(); @@ -115,7 +115,7 @@ void testInstant(SessionFactoryScope scope) { @Test void testLocalDateTime(SessionFactoryScope scope) { final Dialect dialect = scope.getSessionFactory().getJdbcServices().getDialect(); - final LocalDateTime start = roundToDefaultPrecision( LocalDateTime.now(), dialect ); + final LocalDateTime start = adjustToDefaultPrecision( LocalDateTime.now(), dialect ); scope.inTransaction( (session) -> { final EntityWithJavaTimeValues entity = new EntityWithJavaTimeValues(); @@ -145,7 +145,7 @@ void testLocalDateTime(SessionFactoryScope scope) { @Test void testLocalDate(SessionFactoryScope scope) { final Dialect dialect = scope.getSessionFactory().getJdbcServices().getDialect(); - final LocalDate startTime = roundToDefaultPrecision( LocalDate.now(), dialect ); + final LocalDate startTime = adjustToDefaultPrecision( LocalDate.now(), dialect ); scope.inTransaction( (session) -> { final EntityWithJavaTimeValues entity = new EntityWithJavaTimeValues(); @@ -178,7 +178,7 @@ void testLocalDate(SessionFactoryScope scope) { @SkipForDialect(dialectClass = AltibaseDialect.class, reason = "Altibase drivers truncate fractional seconds from the LocalTime") void testLocalTime(SessionFactoryScope scope) { final Dialect dialect = scope.getSessionFactory().getJdbcServices().getDialect(); - final LocalTime startTime = roundToDefaultPrecision( LocalTime.now(), dialect ); + final LocalTime startTime = adjustToDefaultPrecision( LocalTime.now(), dialect ); scope.inTransaction( (session) -> { final EntityWithJavaTimeValues entity = new EntityWithJavaTimeValues(); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/merge/BidirectionalOneToManyMergeTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/merge/BidirectionalOneToManyMergeTest.java new file mode 100644 index 000000000000..d9261f6be747 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/merge/BidirectionalOneToManyMergeTest.java @@ -0,0 +1,178 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.merge; + +import org.hibernate.testing.orm.junit.JiraKey; +import org.junit.Before; +import org.junit.Test; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import java.util.ArrayList; +import java.util.List; + +import static org.hibernate.testing.transaction.TransactionUtil.doInJPA; + +/** + * @author Lisandro Fernandez (kelechul at gmail dot com) + */ +@JiraKey("HHH-13815") +public class BidirectionalOneToManyMergeTest extends org.hibernate.orm.test.jpa.BaseEntityManagerFunctionalTestCase { + + @Override + protected Class[] getAnnotatedClasses() { + return new Class[]{ + Post.class, + PostComment.class, + }; + } + + @Before + public void setUp() { + doInJPA(this::entityManagerFactory, entityManager -> { + entityManager.persist( + new Post("High-Performance Java Persistence").setId(1L) + ); + }); + } + + @Test + public void testMerge() { + doInJPA(this::entityManagerFactory, entityManager -> { + Post post = entityManager.find(Post.class, 1L); + post.addComment(new PostComment("This post rocks!", post)); + post.getComments().isEmpty(); + entityManager.merge(post); + }); + } + + @Entity + public static class Post { + + @Id + private Long id; + + private String title; + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + private List comments = new ArrayList<>(); + + public Post() { + } + + public Post(String title) { + this.title = title; + } + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public List getComments() { + return comments; + } + + private Post setComments(List comments) { + this.comments = comments; + return this; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + + return this; + } + + public Post removeComment(PostComment comment) { + comments.remove(comment); + comment.setPost(null); + + return this; + } + } + + @Entity + public static class PostComment { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String review; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id") + private Post post; + + public PostComment() { + } + + public PostComment(String review, Post post) { + this.review = review; + this.post = post; + } + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof PostComment)) return false; + return id != null && id.equals(((PostComment) o).getId()); + } + + @Override + public int hashCode() { + return 31; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/EmbeddableMetaModelTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/EmbeddableMetaModelTest.java index 7ae21c5c877a..3f64f5457ee2 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/EmbeddableMetaModelTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/EmbeddableMetaModelTest.java @@ -28,6 +28,8 @@ Person.class, Measurement.class, Height.class, + WeightClass.class, + Weight.class, } ) public class EmbeddableMetaModelTest { @Test @@ -62,4 +64,18 @@ public void test(EntityManagerFactoryScope scope) { assertNotNull( Measurement_.unit ); } ); } + + @Test + @Jira( "https://hibernate.atlassian.net/browse/HHH-18819" ) + public void testIdClass(EntityManagerFactoryScope scope) { + scope.inTransaction( entityManager -> { + final EmbeddableDomainType embeddable = (EmbeddableDomainType) entityManager.getMetamodel() + .embeddable( Weight.class ); + assertNotNull( embeddable.getSuperType() ); + assertEquals( MAPPED_SUPERCLASS, embeddable.getSuperType().getPersistenceType() ); + assertEquals( Measurement.class, embeddable.getSuperType().getJavaType() ); + assertNotNull( Weight_.weight ); + assertNotNull( Measurement_.unit ); + } ); + } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/Weight.java b/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/Weight.java new file mode 100644 index 000000000000..c5557314bbfc --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/Weight.java @@ -0,0 +1,15 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.metamodel; + +import jakarta.persistence.Embeddable; + +/** + * @author Marco Belladelli + */ +@Embeddable +public class Weight extends Measurement { + private float weight; +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/WeightClass.java b/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/WeightClass.java new file mode 100644 index 000000000000..18d313b405a1 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/WeightClass.java @@ -0,0 +1,24 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.metamodel; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.IdClass; + +/** + * @author Marco Belladelli + */ +@Entity +@IdClass(Weight.class) +public class WeightClass { + @Id + private String unit; + + @Id + private float weight; + + private String description; +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/generics/embeddable/AbstractValueObject.java b/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/generics/embeddable/AbstractValueObject.java new file mode 100644 index 000000000000..3b9e40980920 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/generics/embeddable/AbstractValueObject.java @@ -0,0 +1,34 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.metamodel.generics.embeddable; + +import java.io.Serializable; + +import jakarta.persistence.Column; +import jakarta.persistence.MappedSuperclass; + +@MappedSuperclass +public abstract class AbstractValueObject> implements Serializable, + Comparable> { + public static final String VALUE = "value"; + + @Column( name = "value_col" ) + private V value; + + protected AbstractValueObject() { + super(); + } + + protected AbstractValueObject(final V value) { + this.value = value; + } + + @Override + public int compareTo(final AbstractValueObject object) { + return value.compareTo( object.value ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/generics/embeddable/CreationDate.java b/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/generics/embeddable/CreationDate.java new file mode 100644 index 000000000000..21700a1cb159 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/generics/embeddable/CreationDate.java @@ -0,0 +1,22 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.metamodel.generics.embeddable; + +import java.util.Date; + +import jakarta.persistence.Embeddable; + +@Embeddable +public class CreationDate extends AbstractValueObject { + protected CreationDate() { + super(); + } + + public CreationDate(final Date value) { + super( value ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/generics/embeddable/GenericEmbeddableSuperclassMetamodelTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/generics/embeddable/GenericEmbeddableSuperclassMetamodelTest.java new file mode 100644 index 000000000000..474085add4cd --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/generics/embeddable/GenericEmbeddableSuperclassMetamodelTest.java @@ -0,0 +1,80 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.metamodel.generics.embeddable; + +import java.util.Date; + +import org.hibernate.SessionFactory; + +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.Jpa; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Tuple; +import jakarta.persistence.TypedQuery; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Expression; +import jakarta.persistence.criteria.Path; +import jakarta.persistence.criteria.Root; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Marco Belladelli + */ +@Jpa( annotatedClasses = { + Parent.class, + AbstractValueObject.class, + CreationDate.class, + SomeNumber.class, + SomeString.class, +} ) +@Jira( "https://hibernate.atlassian.net/browse/HHH-18490" ) +public class GenericEmbeddableSuperclassMetamodelTest { + @SuppressWarnings( { "rawtypes", "unchecked" } ) + @Test + public void test(EntityManagerFactoryScope scope) { + scope.inTransaction( entityManager -> { + final CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + final CriteriaQuery cq = cb.createTupleQuery(); + final Root from = cq.from( Parent.class ); + + final Path someStringPath = from.get( Parent_.someString ).get( SomeString_.value ); + final Path someNumberPath = from.get( Parent_.someNumber ).get( SomeNumber_.value ); + final Path timestampPath = from.get( Parent_.date ).get( CreationDate_.value ); + final Expression maxNumber = cb.max( someNumberPath ); + final Expression maxTimestamp = cb.function( "max", Date.class, timestampPath ); + cq.select( cb.tuple( someStringPath, maxTimestamp, maxNumber ) ); + cq.groupBy( someStringPath ); + + final TypedQuery query = entityManager.createQuery( cq ); + final Tuple result = query.getSingleResult(); + + assertThat( result.get( 0, String.class ) ).isEqualTo( "something" ); + assertThat( result.get( 1, Date.class ) ).isNotNull(); + assertThat( result.get( 2, Object.class ) ).isEqualTo( 42 ); + } ); + } + + @BeforeAll + public void setUp(EntityManagerFactoryScope scope) { + scope.inTransaction( entityManager -> entityManager.persist( new Parent( + new SomeString( "something" ), + new CreationDate( new Date() ), + new SomeNumber( 42 ) + ) ) ); + } + + @AfterAll + public void tearDown(EntityManagerFactoryScope scope) { + scope.getEntityManagerFactory().unwrap( SessionFactory.class ).getSchemaManager().truncateMappedObjects(); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/generics/embeddable/Parent.java b/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/generics/embeddable/Parent.java new file mode 100644 index 000000000000..17898c5b48a4 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/generics/embeddable/Parent.java @@ -0,0 +1,44 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.metamodel.generics.embeddable; + +import jakarta.persistence.AttributeOverride; +import jakarta.persistence.Column; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; + +import static org.hibernate.orm.test.metamodel.generics.embeddable.AbstractValueObject.VALUE; + +@Entity +public class Parent { + @Id + @GeneratedValue + private Long id; + + @Embedded + @AttributeOverride( name = VALUE, column = @Column( name = "some_string" ) ) + private SomeString someString; + + @Embedded + @AttributeOverride( name = VALUE, column = @Column( name = "some_date" ) ) + private CreationDate date; + + @Embedded + @AttributeOverride( name = VALUE, column = @Column( name = "some_number" ) ) + private SomeNumber someNumber; + + public Parent() { + } + + public Parent(final SomeString someString, final CreationDate date, final SomeNumber someNumber) { + this.someString = someString; + this.date = date; + this.someNumber = someNumber; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/generics/embeddable/SomeNumber.java b/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/generics/embeddable/SomeNumber.java new file mode 100644 index 000000000000..3f618556ed33 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/generics/embeddable/SomeNumber.java @@ -0,0 +1,19 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.metamodel.generics.embeddable; + +import jakarta.persistence.Embeddable; + +@Embeddable +public class SomeNumber extends AbstractValueObject { + protected SomeNumber() { + } + + public SomeNumber(final Integer value) { + super( value ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/generics/embeddable/SomeString.java b/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/generics/embeddable/SomeString.java new file mode 100644 index 000000000000..825a743d3801 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/generics/embeddable/SomeString.java @@ -0,0 +1,13 @@ +package org.hibernate.orm.test.metamodel.generics.embeddable; + +import jakarta.persistence.Embeddable; + +@Embeddable +public class SomeString extends AbstractValueObject { + protected SomeString() { + } + + public SomeString(final String value) { + super( value ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/onetoone/embeddedid/OneToOneJoinColumnsEmbeddedIdTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/onetoone/embeddedid/OneToOneJoinColumnsEmbeddedIdTest.java new file mode 100644 index 000000000000..7e0c2375ea98 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/onetoone/embeddedid/OneToOneJoinColumnsEmbeddedIdTest.java @@ -0,0 +1,195 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.onetoone.embeddedid; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinColumns; +import jakarta.persistence.OneToOne; +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@DomainModel( + annotatedClasses = { + OneToOneJoinColumnsEmbeddedIdTest.EntityA.class, + OneToOneJoinColumnsEmbeddedIdTest.EntityB.class, + } +) +@SessionFactory +@BytecodeEnhanced(runNotEnhancedAsWell = true) +@JiraKey("HHH-17838") +public class OneToOneJoinColumnsEmbeddedIdTest { + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + EntityAKey entityAKey = new EntityAKey( 1, "1" ); + EntityA entityA = new EntityA( entityAKey, "te1" ); + + EntityBKey entityBKey = new EntityBKey( 1, "1" ); + EntityB entityB = new EntityB( entityBKey, entityA ); + + session.persist( entityA ); + session.persist( entityB ); + } + ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Test + public void testFind(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + EntityAKey entityAKey = new EntityAKey( 1, "1" ); + EntityA entityA = session.find( EntityA.class, entityAKey ); + assertThat( entityA ).isNotNull(); + + EntityB entityB = entityA.getEntityB(); + assertThat( entityB ).isNotNull(); + + EntityBKey key = entityB.getEntityBKey(); + assertThat( key.id1 ).isEqualTo( 1 ); + assertThat( key.id2 ).isEqualTo( "1" ); + } + ); + } + + @Test + public void testFind2(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + EntityBKey entityBKey = new EntityBKey( 1, "1" ); + EntityB entityB = session.find( EntityB.class, entityBKey ); + assertThat( entityB ).isNotNull(); + + EntityA entityA = entityB.getEntityA(); + assertThat( entityA ).isNotNull(); + + EntityAKey entityAKey = entityA.getEntityAKey(); + assertThat( entityAKey.id1 ).isEqualTo( 1 ); + assertThat( entityAKey.id2 ).isEqualTo( "1" ); + + assertThat( entityA.getName() ).isEqualTo( "te1" ); + } + ); + } + + @Entity(name = "EntityA") + public static class EntityA { + + @EmbeddedId + private EntityAKey entityAKey; + + private String name; + + @OneToOne(mappedBy = "entityA", fetch = FetchType.LAZY) + private EntityB entityB; + + public EntityA() { + } + + public EntityA(EntityAKey key, String name) { + this.entityAKey = key; + this.name = name; + } + + public EntityAKey getEntityAKey() { + return entityAKey; + } + + public String getName() { + return name; + } + + public EntityB getEntityB() { + return entityB; + } + } + + @Embeddable + public static class EntityAKey { + + @Column(name = "id1") + private Integer id1; + + @Column(name = "id2") + private String id2; + + public EntityAKey() { + } + + public EntityAKey(Integer id1, String id2) { + this.id1 = id1; + this.id2 = id2; + } + } + + @Entity(name = "EntityB") + public static class EntityB { + @EmbeddedId + private EntityBKey entityBKey; + + @OneToOne(optional = false, fetch = FetchType.LAZY) + @JoinColumns({ + @JoinColumn(name = "id1", referencedColumnName = "id1", nullable = false, + insertable = false, updatable = false), + @JoinColumn(name = "id2", referencedColumnName = "id2", nullable = false, + insertable = false, updatable = false) + }) + private EntityA entityA; + + public EntityB() { + } + + public EntityB(EntityBKey key, EntityA testEntity) { + this.entityBKey = key; + this.entityA = testEntity; + testEntity.entityB = this; + } + + public EntityBKey getEntityBKey() { + return entityBKey; + } + + public EntityA getEntityA() { + return entityA; + } + } + + @Embeddable + public static class EntityBKey { + + @Column(name = "id1") + private Integer id1; + + @Column(name = "id2") + private String id2; + + public EntityBKey() { + } + + public EntityBKey(Integer documentType, String no) { + this.id1 = documentType; + this.id2 = no; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/ops/MergeExplicitInitialVersionTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/ops/MergeExplicitInitialVersionTest.java new file mode 100644 index 000000000000..cb49e2e274b4 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/ops/MergeExplicitInitialVersionTest.java @@ -0,0 +1,71 @@ +package org.hibernate.orm.test.ops; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Version; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.Jpa; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@Jpa(annotatedClasses = {MergeExplicitInitialVersionTest.E.class, + MergeExplicitInitialVersionTest.F.class, + MergeExplicitInitialVersionTest.G.class}) +public class MergeExplicitInitialVersionTest { + @Test public void testGeneratedId(EntityManagerFactoryScope scope) { + E e = new E(); + scope.inTransaction(s->s.persist(e)); + assertEquals(e.version, 1); + e.text = "hello"; + E e2 = scope.fromTransaction(s->s.merge(e)); + assertEquals(e2.version, 2); + } + @Test public void testAssignedId(EntityManagerFactoryScope scope) { + F f = new F(); + scope.inTransaction(s->s.persist(f)); + assertEquals(f.version, 1); + f.text = "hello"; + F f2 = scope.fromTransaction(s->s.merge(f)); + assertEquals(f2.version, 2); + } + @Test public void testNegativeVersion(EntityManagerFactoryScope scope) { + G g = new G(); + scope.inTransaction(s->s.persist(g)); + assertEquals(g.version, 0); + g.text = "hello"; + G g2 = scope.fromTransaction(s->s.merge(g)); + assertEquals(g2.version, 1); + } + + @Entity + static class E { + @Id + @GeneratedValue + long id; + @Version + int version = 1; + String text; + } + + @Entity + static class F { + @Id + long id = 5; + @Version + int version = 1; + String text; + } + + @Entity + static class G { + @Id + @GeneratedValue + long id; + @Version + int version = -1; + String text; + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/proxy/ProxyClassReuseTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/proxy/ProxyClassReuseTest.java new file mode 100644 index 000000000000..5166cd8f554e --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/proxy/ProxyClassReuseTest.java @@ -0,0 +1,193 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.proxy; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import org.hibernate.boot.MetadataSources; +import org.hibernate.boot.registry.BootstrapServiceRegistryBuilder; +import org.hibernate.boot.registry.StandardServiceRegistry; +import org.hibernate.boot.registry.StandardServiceRegistryBuilder; +import org.hibernate.boot.registry.classloading.internal.TcclLookupPrecedence; +import org.hibernate.bytecode.internal.bytebuddy.BytecodeProviderImpl; +import org.hibernate.bytecode.spi.ByteCodeHelper; +import org.hibernate.bytecode.spi.BytecodeProvider; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SessionImplementor; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.util.ServiceRegistryUtil; +import org.junit.jupiter.api.Test; + +import java.io.InputStream; +import java.util.Set; +import java.util.function.Function; + +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; + +@Jira("https://hibernate.atlassian.net/browse/HHH-14694") +public class ProxyClassReuseTest { + + @Test + void testReuse() { + Function> proxyGetter = sf -> { + try (SessionImplementor s = sf.openSession()) { + return s.getReference( MyEntity.class, "abc" ).getClass(); + } + }; + final BytecodeProvider bytecodeProvider = new BytecodeProviderImpl(); + Class proxyClass1 = withFactory( proxyGetter, bytecodeProvider, null ); + Class proxyClass2 = withFactory( proxyGetter, bytecodeProvider, null ); + assertSame( proxyClass1, proxyClass2 ); + } + + @Test + void testReuseWithDifferentFactories() { + Function> proxyGetter = sf -> { + try (SessionImplementor s = sf.openSession()) { + return s.getReference( MyEntity.class, "abc" ).getClass(); + } + }; + Class proxyClass1 = withFactory( proxyGetter, null, null ); + Class proxyClass2 = withFactory( proxyGetter, null, null ); + assertSame( proxyClass1, proxyClass2 ); + assertSame( proxyClass1.getClassLoader(), MyEntity.class.getClassLoader() ); + assertSame( proxyClass2.getClassLoader(), MyEntity.class.getClassLoader() ); + } + + @Test + void testNoReuse() { + Function> proxyGetter = sf -> { + try { + //noinspection unchecked + Function> getter = (Function>) Thread.currentThread() + .getContextClassLoader() + .loadClass( ProxyClassReuseTest.class.getName() + "$ProxyGetter" ) + .getConstructor() + .newInstance(); + return getter.apply( sf ); + } + catch (Exception ex) { + throw new RuntimeException( ex ); + } + }; + // Create two isolated class loaders that load the entity and proxy classes in isolation + Set isolatedClasses = Set.of( "org.hibernate.orm.test.proxy.*" ); + ClassLoader cl1 = new IsolatingClassLoader( isolatedClasses ); + ClassLoader cl2 = new IsolatingClassLoader( isolatedClasses ); + Class proxyClass1 = withFactory( proxyGetter, null, cl1 ); + Class proxyClass2 = withFactory( proxyGetter, null, cl2 ); + // The two proxy classes shall be defined on the respective isolated class loaders and hence be different + assertNotSame( proxyClass1, proxyClass2 ); + assertSame( proxyClass1.getClassLoader(), cl1 ); + assertSame( proxyClass2.getClassLoader(), cl2 ); + } + + T withFactory(Function consumer, BytecodeProvider bytecodeProvider, ClassLoader classLoader) { + final ClassLoader oldClassLoader = Thread.currentThread().getContextClassLoader(); + try { + if (classLoader != null) { + Thread.currentThread().setContextClassLoader( classLoader ); + } + final BootstrapServiceRegistryBuilder bsr = new BootstrapServiceRegistryBuilder(); + bsr.applyTcclLookupPrecedence( TcclLookupPrecedence.BEFORE ); + final StandardServiceRegistryBuilder builder = ServiceRegistryUtil.serviceRegistryBuilder(bsr.build()); + if ( bytecodeProvider != null ) { + builder.addService( BytecodeProvider.class, bytecodeProvider ); + } + final StandardServiceRegistry ssr = builder.build(); + + try (final SessionFactoryImplementor sf = (SessionFactoryImplementor) new MetadataSources( ssr ) + .addAnnotatedClassName( ProxyClassReuseTest.class.getName() + "$MyEntity" ) + .buildMetadata() + .getSessionFactoryBuilder() + .build()) { + return consumer.apply( sf ); + } + catch (Exception e) { + StandardServiceRegistryBuilder.destroy( ssr ); + throw e; + } + } + finally { + if (classLoader != null) { + Thread.currentThread().setContextClassLoader( oldClassLoader ); + } + } + } + + @Entity(name = "MyEntity") + public static class MyEntity { + @Id + String id; + String name; + } + + public static class ProxyGetter implements Function> { + @Override + public Class apply(SessionFactoryImplementor sf) { + try (SessionImplementor s = sf.openSession()) { + return s.getReference( MyEntity.class, "abc" ).getClass(); + } + } + } + + private static class IsolatingClassLoader extends ClassLoader { + + private final Set isolatedClasses; + + public IsolatingClassLoader(Set isolatedClasses) { + super( IsolatingClassLoader.class.getClassLoader() ); + this.isolatedClasses = isolatedClasses; + } + + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + synchronized (getClassLoadingLock(name)) { + Class c = findLoadedClass(name); + if (c == null) { + if (isIsolatedClass(name)) { + InputStream is = this.getResourceAsStream( name.replace( '.', '/' ) + ".class" ); + if ( is == null ) { + throw new ClassNotFoundException( name + " not found" ); + } + + try { + byte[] bytecode = ByteCodeHelper.readByteCode( is ); + return defineClass( name, bytecode, 0, bytecode.length ); + } + catch( Throwable t ) { + throw new ClassNotFoundException( name + " not found", t ); + } + } else { + // Parent first + c = super.loadClass(name, resolve); + } + } + return c; + } + } + + private boolean isIsolatedClass(String name) { + if (isolatedClasses != null) { + for (String isolated : isolatedClasses) { + if (isolated.endsWith(".*")) { + String isolatedPackage = isolated.substring(0, isolated.length() - 1); + String paramPackage = name.substring(0, name.lastIndexOf('.') + 1); + if (paramPackage.startsWith(isolatedPackage)) { + // Matching package + return true; + } + } else if (isolated.equals(name)) { + return true; + } + } + } + return false; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/proxy/concrete/ConcreteProxyToOneSecondLevelCacheTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/proxy/concrete/ConcreteProxyToOneSecondLevelCacheTest.java new file mode 100644 index 000000000000..e9380cc4bc66 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/proxy/concrete/ConcreteProxyToOneSecondLevelCacheTest.java @@ -0,0 +1,220 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.proxy.concrete; + +import jakarta.persistence.Cacheable; +import jakarta.persistence.DiscriminatorColumn; +import jakarta.persistence.DiscriminatorType; +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import org.hibernate.Hibernate; +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.hibernate.annotations.ConcreteProxy; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.stat.Statistics; +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel(annotatedClasses = { + ConcreteProxyToOneSecondLevelCacheTest.TestNode.class, + ConcreteProxyToOneSecondLevelCacheTest.TestCompositeNode.class +}) +@ServiceRegistry(settings = { + @Setting(name = AvailableSettings.GENERATE_STATISTICS, value = "true"), + @Setting(name = AvailableSettings.USE_SECOND_LEVEL_CACHE, value = "true") +}) +@SessionFactory +@BytecodeEnhanced(runNotEnhancedAsWell = true) +@Jira("https://hibernate.atlassian.net/browse/HHH-18872") +public class ConcreteProxyToOneSecondLevelCacheTest { + @Test + public void testToOneInCacheGetReference(SessionFactoryScope scope) { + final Statistics stats = scope.getSessionFactory().getStatistics(); + stats.clear(); + // load only the node with ID 1 into the 2LC + scope.inTransaction( session -> { + assertThat( session.find( TestCompositeNode.class, 1 ) ).isNotNull(); + assertThat( session.find( TestCompositeNode.class, 2 ) ).isNotNull(); + } ); + assertCacheStats( stats, 0, 2, 2 ); + scope.inSession( session -> { + final TestCompositeNode node1 = session.getReference( TestCompositeNode.class, 1 ); + assertThat( Hibernate.isInitialized( node1 ) ).isFalse(); + // this triggers node1 initialization, but should maintain laziness for parent + final TestNode parent = node1.getParent(); + assertThat( Hibernate.isInitialized( node1 ) ).isTrue(); + // node 1 will be loaded from cache + assertCacheStats( stats, 1, 2, 2 ); + assertParent( parent, stats, 2 ); + } ); + } + + @Test + public void testToOneNotInCacheGetReference(SessionFactoryScope scope) { + final Statistics stats = scope.getSessionFactory().getStatistics(); + stats.clear(); + // load only the node with ID 1 into the 2LC + scope.inTransaction( session -> assertThat( session.find( TestCompositeNode.class, 1 ) ).isNotNull() ); + assertCacheStats( stats, 0, 1, 1 ); + scope.inSession( session -> { + final TestCompositeNode node1 = session.getReference( TestCompositeNode.class, 1 ); + assertThat( Hibernate.isInitialized( node1 ) ).isFalse(); + // this triggers node1 initialization, but should maintain laziness for parent + final TestNode parent = node1.getParent(); + assertThat( Hibernate.isInitialized( node1 ) ).isTrue(); + // node 1 will be loaded from cache + assertCacheStats( stats, 1, 1, 1 ); + assertParent( parent, stats, 1 ); + } ); + } + + @Test + public void testToOneInCacheFind(SessionFactoryScope scope) { + final Statistics stats = scope.getSessionFactory().getStatistics(); + stats.clear(); + // load only the node with ID 1 into the 2LC + scope.inTransaction( session -> { + assertThat( session.find( TestCompositeNode.class, 1 ) ).isNotNull(); + assertThat( session.find( TestCompositeNode.class, 2 ) ).isNotNull(); + } ); + assertCacheStats( stats, 0, 2, 2 ); + scope.inSession( session -> { + final TestCompositeNode node1 = session.find( TestCompositeNode.class, 1 ); + assertThat( Hibernate.isInitialized( node1 ) ).isTrue(); + // node 1 will be loaded from cache + assertCacheStats( stats, 1, 2, 2 ); + assertParent( node1.getParent(), stats, 2 ); + } ); + } + + @Test + public void testToOneNotInCacheFind(SessionFactoryScope scope) { + final Statistics stats = scope.getSessionFactory().getStatistics(); + stats.clear(); + // load only the node with ID 1 into the 2LC + scope.inTransaction( session -> assertThat( session.find( TestCompositeNode.class, 1 ) ).isNotNull() ); + assertCacheStats( stats, 0, 1, 1 ); + scope.inSession( session -> { + final TestCompositeNode node1 = session.find( TestCompositeNode.class, 1 ); + assertThat( Hibernate.isInitialized( node1 ) ).isTrue(); + // node 1 will be loaded from cache + assertCacheStats( stats, 1, 1, 1 ); + assertParent( node1.getParent(), stats, 1 ); + } ); + } + + private static void assertParent(final TestNode parent, final Statistics stats, final long hits) { + assertThat( TestCompositeNode.class ).as( "Expecting parent proxy to be narrowed to concrete type" ) + .isAssignableFrom( parent.getClass() ); + final TestCompositeNode parentComposite = (TestCompositeNode) parent; + assertThat( Hibernate.isInitialized( parentComposite ) ).isFalse(); + assertThat( parentComposite.getName() ).isEqualTo( "parent_node" ); + assertThat( Hibernate.isInitialized( parentComposite ) ).isTrue(); + // node 2 will not be found in cache + assertCacheStats( stats, hits, 2, 2 ); + assertThat( parentComposite ).as( String.format( + "Expecting parent to be an instance of TestCompositeNode but was: [%s]", + parent.getClass() + ) ).isInstanceOf( TestCompositeNode.class ); + } + + private static void assertCacheStats(final Statistics stats, final long hits, final long misses, final long puts) { + assertThat( stats.getSecondLevelCacheHitCount() ).isEqualTo( hits ); + assertThat( stats.getSecondLevelCacheMissCount() ).isEqualTo( misses ); + assertThat( stats.getSecondLevelCachePutCount() ).isEqualTo( puts ); + } + + @BeforeEach + public void clearCache(SessionFactoryScope scope) { + scope.getSessionFactory().getCache().evictAllRegions(); + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final TestCompositeNode node1 = new TestCompositeNode( 1, "child_node" ); + final TestCompositeNode node2 = new TestCompositeNode( 2, "parent_node" ); + node1.setParent( node2 ); + session.persist( node1 ); + session.persist( node2 ); + } ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Entity(name = "TestNode") + @Cacheable + @ConcreteProxy + @Inheritance(strategy = InheritanceType.SINGLE_TABLE) + @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + @DiscriminatorColumn(discriminatorType = DiscriminatorType.STRING, name = "disc_col") + @DiscriminatorValue(value = "simple") + public static class TestNode { + @Id + private Integer id; + + private String name; + + public TestNode() { + } + + public TestNode(Integer id, String name) { + this.id = id; + this.name = name; + } + + public Integer getId() { + return id; + } + + public String getName() { + return name; + } + } + + @Entity + @DiscriminatorValue("composite") + public static class TestCompositeNode extends TestNode { + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "parent_id") + private TestNode parent; + + public TestCompositeNode() { + } + + public TestCompositeNode(Integer id, String name) { + super( id, name ); + } + + public TestNode getParent() { + return parent; + } + + public void setParent(TestNode parent) { + this.parent = parent; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/CriteriaSubqueryInPredicateTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/CriteriaSubqueryInPredicateTest.java new file mode 100644 index 000000000000..cf1863824207 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/CriteriaSubqueryInPredicateTest.java @@ -0,0 +1,84 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.query; + +import java.util.List; + +import org.hibernate.testing.orm.domain.gambit.BasicEntity; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.Root; +import jakarta.persistence.criteria.Subquery; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Marco Belladelli + */ +@DomainModel( annotatedClasses = BasicEntity.class ) +@SessionFactory +@Jira( "https://hibernate.atlassian.net/browse/HHH-18502" ) +public class CriteriaSubqueryInPredicateTest { + @Test + public void testInCollection(SessionFactoryScope scope) { + executeQuery( scope, 1, sub -> sub.in( List.of( "entity_1", "another_entity" ) ) ); + } + + @Test + public void testInArray(SessionFactoryScope scope) { + executeQuery( scope, 2, sub -> sub.in( "entity_2", "another_entity" ) ); + } + + @Test + public void testInLiteral(SessionFactoryScope scope) { + executeQuery( scope, 3, sub -> sub.in( "entity_3" ) ); + } + + private void executeQuery(SessionFactoryScope scope, Integer expectedId, InPredicateProducer producer) { + scope.inTransaction( session -> { + final CriteriaBuilder cb = session.getCriteriaBuilder(); + final CriteriaQuery cq = cb.createQuery( BasicEntity.class ); + final Root root = cq.from( BasicEntity.class ); + + final Subquery sub = cq.subquery( String.class ); + final Root subRoot = sub.from( BasicEntity.class ); + sub.select( subRoot.get( "data" ) ).where( cb.equal( subRoot.get( "id" ), root.get( "id" ) ) ); + + cq.select( root ).where( producer.accept( sub ) ); + + final List resultList = session.createQuery( cq ).getResultList(); + assertThat( resultList ).hasSize( 1 ).extracting( BasicEntity::getId ).containsOnlyOnce( expectedId ); + } ); + } + + private interface InPredicateProducer { + Predicate accept(Subquery sub); + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.persist( new BasicEntity( 1, "entity_1" ) ); + session.persist( new BasicEntity( 2, "entity_2" ) ); + session.persist( new BasicEntity( 3, "entity_3" ) ); + } ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/CteTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/CteTests.java index f52896c7742e..f4ed251a4a5f 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/CteTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/CteTests.java @@ -268,11 +268,11 @@ public void testInSubquery(SessionFactoryScope scope) { final QueryImplementor query = session.createQuery( "select c.name.first from Contact c where c.id in (" + - "with contacts as (" + + "with cte as (" + "select c.id id, c.name.first firstName from Contact c " + "where c.id in (1,2)" + ") " + - "select c.id from contacts c" + + "select c.id from cte c" + ")", String.class ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/LeftJoinNullnessPredicateQueryTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/LeftJoinNullnessPredicateQueryTest.java index 92b79bc67bdf..45635f3871f3 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/LeftJoinNullnessPredicateQueryTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/LeftJoinNullnessPredicateQueryTest.java @@ -9,6 +9,7 @@ import java.util.List; import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactoryScope; import org.junit.jupiter.api.AfterAll; @@ -29,7 +30,11 @@ @DomainModel( annotatedClasses = { LeftJoinNullnessPredicateQueryTest.Author.class, LeftJoinNullnessPredicateQueryTest.Book.class -} ) +}) +@Jira( "https://hibernate.atlassian.net/browse/HHH-16505" ) +@Jira( "https://hibernate.atlassian.net/browse/HHH-17379" ) +@Jira( "https://hibernate.atlassian.net/browse/HHH-17397" ) +@Jira( "https://hibernate.atlassian.net/browse/HHH-19116" ) public class LeftJoinNullnessPredicateQueryTest { @BeforeAll public void setUp(SessionFactoryScope scope) { @@ -80,6 +85,19 @@ public void testIsNotNull(SessionFactoryScope scope) { } ); } + @Test + public void testIsNullImplicit(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final List resultList = session.createQuery( + "select book from Book book " + + "where book.author is null", + Book.class + ).getResultList(); + assertThat( resultList ).hasSize( 1 ); + assertThat( resultList.get( 0 ).getTitle() ).isEqualTo( "Unknown Author" ); + } ); + } + @Test public void testDereferenceIsNull(SessionFactoryScope scope) { scope.inTransaction( session -> { @@ -108,6 +126,19 @@ public void testDereferenceIsNotNull(SessionFactoryScope scope) { } ); } + @Test + public void testFkImplicitIsNull(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final List resultList = session.createQuery( + "select book from Book book " + + "where fk(book.author) is null", + Book.class + ).getResultList(); + assertThat( resultList ).hasSize( 1 ); + assertThat( resultList.get( 0 ).getTitle() ).isEqualTo( "Unknown Author" ); + } ); + } + @Test public void testIsNotNullWithCondition(SessionFactoryScope scope) { scope.inTransaction( session -> { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/NativeQueryAndDiscriminatorTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/NativeQueryAndDiscriminatorTest.java new file mode 100644 index 000000000000..c745061fb328 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/NativeQueryAndDiscriminatorTest.java @@ -0,0 +1,112 @@ +package org.hibernate.orm.test.query; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Column; +import jakarta.persistence.DiscriminatorColumn; +import jakarta.persistence.DiscriminatorType; +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; +import jakarta.persistence.Table; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@DomainModel( + annotatedClasses = { + NativeQueryAndDiscriminatorTest.BaseEntity.class, + NativeQueryAndDiscriminatorTest.TestEntity.class + } +) +@SessionFactory +@JiraKey("HHH-18515") +public class NativeQueryAndDiscriminatorTest { + + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + TestEntity e = new TestEntity( 1l, "test", EntityDiscriminator.T ); + session.persist( e ); + } + ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.createMutationQuery( "delete TestEntity" ).executeUpdate(); + } + ); + } + + @Test + public void testNativeQuery(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + BaseEntity entity = session.createNativeQuery( + "select * from BASE_ENTITY where id = :id", + BaseEntity.class + ) + .setParameter( "id", 1l ) + .getSingleResult(); + assertThat( entity ).isNotNull(); + } + ); + } + + + @Entity(name = "BaseEntity") + @Table(name = "BASE_ENTITY") + @Inheritance(strategy = InheritanceType.SINGLE_TABLE) + @DiscriminatorColumn(name = "discriminator", discriminatorType = DiscriminatorType.CHAR) + @DiscriminatorValue("B") + public static class BaseEntity { + + @Id + private Long id; + + private String name; + + @Column(insertable = false, updatable = false) + @Enumerated(EnumType.STRING) + private EntityDiscriminator discriminator; + + public BaseEntity() { + } + + public BaseEntity(Long id, String name, EntityDiscriminator discriminator) { + this.id = id; + this.name = name; + this.discriminator = discriminator; + } + } + + @Entity(name = "TestEntity") + @DiscriminatorValue("T") + public static class TestEntity extends BaseEntity { + + public TestEntity() { + } + + public TestEntity(Long id, String name, EntityDiscriminator discriminator) { + super( id, name, discriminator ); + } + } + + enum EntityDiscriminator { + T; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/QueryTimeOutTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/QueryTimeOutTest.java index 69937a770b76..7cf09d2decec 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/QueryTimeOutTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/QueryTimeOutTest.java @@ -12,6 +12,7 @@ import java.util.Map; import org.hibernate.cfg.AvailableSettings; +import org.hibernate.community.dialect.InformixDialect; import org.hibernate.dialect.AbstractTransactSQLDialect; import org.hibernate.dialect.OracleDialect; import org.hibernate.dialect.SybaseDialect; @@ -80,6 +81,9 @@ else if ( DialectContext.getDialect() instanceof SybaseDialect ) { else if ( DialectContext.getDialect() instanceof AbstractTransactSQLDialect ) { baseQuery = "update ae1_0 set name=? from AnEntity ae1_0"; } + else if (DialectContext.getDialect() instanceof InformixDialect ) { + baseQuery = "update AnEntity set name=?"; + } else { baseQuery = "update AnEntity ae1_0 set name=?"; } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/SelectCaseWhenNullLiteralTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/SelectCaseWhenNullLiteralTest.java index 836e7df34a6e..330e804078c2 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/SelectCaseWhenNullLiteralTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/SelectCaseWhenNullLiteralTest.java @@ -4,6 +4,7 @@ import org.hibernate.testing.TestForIssue; import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactoryScope; import org.junit.jupiter.api.AfterEach; @@ -55,6 +56,30 @@ public void testSelectCaseWhenNullLiteral(SessionFactoryScope scope) { ); } + @Test + @JiraKey( "HHH-18556" ) + public void testSelectCaseWhenNullLiteralWithParameters(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + List result = session.createQuery( "select case when 1=1 then ?1 else null end from Person p" ) + .setParameter( 1, 2 ) + .list(); + assertThat( result.size(), is( 1 ) ); + assertThat( result.get( 0 ), is( 2 ) ); + } + ); + + scope.inTransaction( + session -> { + List result = session.createQuery( "select count(case when 1=1 then ?1 else null end) from Person p" ) + .setParameter( 1, 2 ) + .list(); + assertThat( result.size(), is( 1 ) ); + assertThat( result.get( 0 ), is( 1L ) ); + } + ); + } + @Entity(name = "Person") @Table(name = "PERSON_TABLE") public static class Person { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/SelectJoinedAssociationMultipleTimesTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/SelectJoinedAssociationMultipleTimesTest.java new file mode 100644 index 000000000000..ff163b37b292 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/SelectJoinedAssociationMultipleTimesTest.java @@ -0,0 +1,60 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.query; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + +@DomainModel(annotatedClasses = { + SelectJoinedAssociationMultipleTimesTest.Book.class +}) +@SessionFactory +public class SelectJoinedAssociationMultipleTimesTest { + + @Test + public void test(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + // Add a proxy first to trigger the error + session.getReference( Book.class, 1 ); + session.createSelectionQuery( "select b b1, b b2 from Book b", Object[].class ).getResultList(); + } + ); + } + + @BeforeEach + public void prepareTestData(SessionFactoryScope scope) { + scope.inTransaction( (session) -> session.persist( new Book( 1, "First book" ) ) ); + } + + @AfterEach + public void dropTestData(SessionFactoryScope scope) { + scope.inTransaction( (session) -> session.createMutationQuery( "delete Book" ).executeUpdate() ); + } + + @Entity( name = "Book") + public static class Book { + @Id + private Integer id; + private String name; + + public Book() { + } + + public Book(Integer id, String name) { + this.id = id; + this.name = name; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/SelectUnknownEnumLiteralTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/SelectUnknownEnumLiteralTest.java new file mode 100644 index 000000000000..5cb857db1935 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/SelectUnknownEnumLiteralTest.java @@ -0,0 +1,66 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.query; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Tuple; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@DomainModel(annotatedClasses = SelectUnknownEnumLiteralTest.Transaction.class) +@SessionFactory +public class SelectUnknownEnumLiteralTest { + + @BeforeAll + void setup(SessionFactoryScope scope) { + scope.inTransaction( em -> em.persist( new Transaction( 1L, "abc" ) ) ); + } + + @AfterAll + void clean(SessionFactoryScope scope) { + scope.inTransaction( em -> em.createMutationQuery( "delete from Tx" ).executeUpdate() ); + } + + @Test + void test(SessionFactoryScope scope) { + final List tuples = scope.fromSession( em -> + em.createQuery( + "SELECT org.hibernate.orm.test.query.SelectUnknownEnumLiteralTest$Type.TRANSACTION, e.id, e.reference FROM Tx e", + Tuple.class ).getResultList() ); + assertEquals( 1, tuples.size() ); + assertEquals( Type.TRANSACTION, tuples.get( 0 ).get( 0 ) ); + assertEquals( 1L, tuples.get( 0 ).get( 1 ) ); + } + + @Entity(name = "Tx") + static class Transaction { + @Id + Long id; + String reference; + + Transaction() { + } + + Transaction(Long id, String reference) { + this.id = id; + this.reference = reference; + } + } + + enum Type { + TRANSACTION, DIRECT_DEBIT_GROUP, DIRECT_DEBIT + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQuerySelectCaseWhenTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQuerySelectCaseWhenTest.java new file mode 100644 index 000000000000..424c95237c84 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQuerySelectCaseWhenTest.java @@ -0,0 +1,79 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.query; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + + +@DomainModel( + annotatedClasses = { + SubQuerySelectCaseWhenTest.TestEntity.class, + } +) +@SessionFactory +@JiraKey("HHH-18681") +public class SubQuerySelectCaseWhenTest { + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.persist( new TestEntity( "A" ) ); + session.persist( new TestEntity( "B" ) ); + session.persist( new TestEntity( "C" ) ); + } + ); + } + + @Test + public void testSelectCase(SessionFactoryScope scope) { + scope.inTransaction( session -> { + List result = session.createQuery( "select " + + " (select " + + " case " + + " when " + + " t.name = ?1 " + + " then 0 " + + " else 1 " + + " end)" + + " from TestEntity t order by t.name", Integer.class ) + .setParameter( 1, "A" ) + .list(); + assertThat( result.size() ).isEqualTo( 3 ); + assertThat( result.get( 0 ) ).isEqualTo( 0 ); + assertThat( result.get( 1 ) ).isEqualTo( 1 ); + assertThat( result.get( 2 ) ).isEqualTo( 1 ); + } ); + } + + @Entity(name = "TestEntity") + public class TestEntity { + + @Id + @GeneratedValue + private Long id; + + private String name; + + public TestEntity() { + } + + public TestEntity(String name) { + this.name = name; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/criteria/CountQueryTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/criteria/CountQueryTests.java index aa5215dfc51d..ad3a721c9a14 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/criteria/CountQueryTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/criteria/CountQueryTests.java @@ -88,6 +88,48 @@ public void testForHHH17967(SessionFactoryScope scope) { ); } + @Test + @JiraKey( "HHH-18850" ) + public void testForHHH18850(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + HibernateCriteriaBuilder cb = session.getCriteriaBuilder(); + JpaCriteriaQuery cq = cb.createQuery( Contract.class ); + cq.distinct( true ); + Root root = cq.from( Contract.class ); + cq.select( root ); + cq.orderBy( cb.asc( root.get( "customerName" ) ) ); + TypedQuery query = session.createQuery( cq.createCountQuery() ); + try { + // Leads to NPE on pre-6.5 versions + query.getSingleResult(); + } + catch (Exception e) { + fail( e ); + } + } + ); + + scope.inTransaction( + session -> { + HibernateCriteriaBuilder cb = session.getCriteriaBuilder(); + JpaCriteriaQuery cq = cb.createQuery( Contract.class ); + cq.distinct( false ); + Root root = cq.from( Contract.class ); + cq.select( root ); + cq.orderBy( cb.desc( root.get( "customerName" ) ) ); + TypedQuery query = session.createQuery( cq.createCountQuery() ); + try { + // Leads to NPE on pre-6.5 versions + query.getSingleResult(); + } + catch (Exception e) { + fail( e ); + } + } + ); + } + @Test @JiraKey("HHH-17410") public void testBasic(SessionFactoryScope scope) { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/criteria/CriteriaMutationQueryFkValuesTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/criteria/CriteriaMutationQueryFkValuesTest.java new file mode 100644 index 000000000000..aa5b6a2c22ed --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/criteria/CriteriaMutationQueryFkValuesTest.java @@ -0,0 +1,172 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.query.criteria; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import org.assertj.core.extractor.Extractors; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Marco Belladelli + */ +@DomainModel( annotatedClasses = { + CriteriaMutationQueryFkValuesTest.DemoEntity.class, + CriteriaMutationQueryFkValuesTest.A.class, + CriteriaMutationQueryFkValuesTest.B.class, + CriteriaMutationQueryFkValuesTest.C.class, +} ) +@SessionFactory( useCollectingStatementInspector = true ) +@Jira( "https://hibernate.atlassian.net/browse/HHH-18647" ) +public class CriteriaMutationQueryFkValuesTest { + @Test + public void testInsertValuesFkColumns(SessionFactoryScope scope) { + final var inspector = scope.getCollectingStatementInspector(); + inspector.clear(); + + scope.inTransaction( session -> { + final var cb = session.getCriteriaBuilder(); + final var criteriaInsert = cb.createCriteriaInsertValues( DemoEntity.class ); + criteriaInsert.setInsertionTargetPaths( + criteriaInsert.getTarget().get( "id" ), + // insert values into foreign key columns + criteriaInsert.getTarget().get( "a" ).get( "id" ), // a_id + criteriaInsert.getTarget().get( "b" ).get( "id" ), // b_id + criteriaInsert.getTarget().get( "c" ).get( "id" ) // c_id + ); + criteriaInsert.values( cb.values( + cb.value( 2L ), + cb.value( 1 ), + cb.value( 2 ), + cb.value( 3 ) + ) ); + final var count = session.createMutationQuery( criteriaInsert ).executeUpdate(); + assertThat( count ).isEqualTo( 1 ); + inspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "join", 0 ); + + assertThat( session.find( DemoEntity.class, 2L ) ).extracting( "a", "b", "c" ).doesNotContainNull(); + } ); + } + + @Test + public void testUpdateFkColumns(SessionFactoryScope scope) { + final var inspector = scope.getCollectingStatementInspector(); + inspector.clear(); + + scope.inTransaction( session -> { + final var cb = session.getCriteriaBuilder(); + final var criteriaUpdate = cb.createCriteriaUpdate( DemoEntity.class ); + criteriaUpdate.set( criteriaUpdate.getTarget().get( "a" ).get( "id" ), cb.value( 4 ) ); + criteriaUpdate.set( + criteriaUpdate.getTarget().get( "b" ).get( "id" ), + cb.nullLiteral( Integer.class ) + ); + criteriaUpdate.where( cb.equal( criteriaUpdate.getTarget().get( "id" ), 1L ) ); + final var count = session.createMutationQuery( criteriaUpdate ).executeUpdate(); + assertThat( count ).isEqualTo( 1 ); + inspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "join", 0 ); + + assertThat( session.find( DemoEntity.class, 1L ) ) + .extracting( "a", "b", "c" ) + .extracting( o -> o == null ? null : Extractors.byName( "id" ).apply( o ) ) + .contains( 4, null, 3 ); + } ); + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var a = new A( 1 ); + final var b = new B( 2 ); + final var c = new C( 3 ); + session.persist( new DemoEntity( 1L, a, b, c ) ); + session.persist( new A( 4 ) ); + } ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Entity( name = "DemoEntity" ) + static class DemoEntity { + @Id + private Long id; + + @ManyToOne( cascade = CascadeType.PERSIST ) + @JoinColumn( name = "a_id" ) + private A a; + + @ManyToOne( cascade = CascadeType.PERSIST ) + @JoinColumn( name = "b_id" ) + private B b; + + @ManyToOne( cascade = CascadeType.PERSIST ) + @JoinColumn( name = "c_id" ) + private C c; + + public DemoEntity() { + } + + public DemoEntity(Long id, A a, B b, C c) { + this.id = id; + this.a = a; + this.b = b; + this.c = c; + } + } + + @Entity( name = "AEntity" ) + static class A { + @Id + private Integer id; + + public A() { + } + + public A(Integer id) { + this.id = id; + } + } + + @Entity( name = "BEntity" ) + static class B { + @Id + private Integer id; + + public B() { + } + + public B(Integer id) { + this.id = id; + } + } + + @Entity( name = "CEntity" ) + static class C { + @Id + private Integer id; + + public C() { + } + + public C(Integer id) { + this.id = id; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/ExistsSubqueryForeignKeyTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/ExistsSubqueryForeignKeyTest.java new file mode 100644 index 000000000000..a5f86d55ebd8 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/ExistsSubqueryForeignKeyTest.java @@ -0,0 +1,112 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.query.hql; + +import org.hibernate.dialect.DerbyDialect; +import org.hibernate.dialect.HSQLDialect; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.SkipForDialect; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Tuple; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Marco Belladelli + */ +@DomainModel(annotatedClasses = { + ExistsSubqueryForeignKeyTest.Person.class, + ExistsSubqueryForeignKeyTest.Document.class, +}) +@SessionFactory +@SkipForDialect(dialectClass = HSQLDialect.class, reason = "HSQLDB doesn't like the case-when selection not being in the group-by") +@SkipForDialect(dialectClass = DerbyDialect.class, reason = "Derby doesn't like the case-when selection not being in the group-by") +@Jira( "https://hibernate.atlassian.net/browse/HHH-18816" ) +public class ExistsSubqueryForeignKeyTest { + @Test + public void testWhereClause(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final Long result = session.createQuery( + "select count(*) from Document d join d.owner o " + + "where exists(select p.id from Person p where p.id = o.id) group by o.id", + Long.class + ).getSingleResult(); + assertThat( result ).isEqualTo( 1L ); + } ); + } + + @Test + public void testSelectCaseWhen(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final Tuple result = session.createQuery( + "select case when exists(select p.id from Person p where p.id = o.id) then 1 else 0 end," + + "count(*) from Document d join d.owner o group by o.id", + Tuple.class + ).getSingleResult(); + assertThat( result.get( 0, Integer.class ) ).isEqualTo( 1 ); + assertThat( result.get( 1, Long.class ) ).isEqualTo( 1L ); + } ); + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final Person person1 = new Person( 1L, "person_1" ); + session.persist( person1 ); + session.persist( new Document( 1L, "doc_1", person1 ) ); + } ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Entity(name = "Person") + static class Person { + @Id + private Long id; + + private String name; + + public Person() { + } + + public Person(Long id, String name) { + this.id = id; + this.name = name; + } + } + + @Entity(name = "Document") + static class Document { + @Id + private Long id; + + private String title; + + @ManyToOne + private Person owner; + + public Document() { + } + + public Document(Long id, String title, Person owner) { + this.id = id; + this.title = title; + this.owner = owner; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/FunctionTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/FunctionTests.java index af1a148e83f2..92b4355d84ee 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/FunctionTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/FunctionTests.java @@ -7,8 +7,12 @@ package org.hibernate.orm.test.query.hql; import org.hamcrest.Matchers; + +import org.hibernate.HibernateException; +import org.hibernate.JDBCException; import org.hibernate.QueryException; import org.hibernate.community.dialect.AltibaseDialect; +import org.hibernate.community.dialect.InformixDialect; import org.hibernate.dialect.CockroachDialect; import org.hibernate.dialect.DB2Dialect; import org.hibernate.dialect.DerbyDialect; @@ -23,6 +27,8 @@ import org.hibernate.dialect.SybaseDialect; import org.hibernate.dialect.TiDBDialect; import org.hibernate.query.sqm.produce.function.FunctionArgumentException; +import org.hibernate.sql.exec.ExecutionException; + import org.hibernate.testing.TestForIssue; import org.hibernate.testing.orm.domain.StandardDomainModel; import org.hibernate.testing.orm.domain.gambit.EntityOfBasics; @@ -31,6 +37,7 @@ import org.hibernate.testing.orm.domain.gambit.SimpleEntity; import org.hibernate.testing.orm.junit.DialectFeatureChecks; import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; import org.hibernate.testing.orm.junit.JiraKey; import org.hibernate.testing.orm.junit.RequiresDialect; import org.hibernate.testing.orm.junit.RequiresDialectFeature; @@ -66,6 +73,7 @@ import static org.hibernate.testing.orm.domain.gambit.EntityOfBasics.Gender.FEMALE; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -599,6 +607,7 @@ public void testDateTruncFunction(SessionFactoryScope scope) { @Test @SkipForDialect(dialectClass = DerbyDialect.class, reason = "Derby doesn't support any form of date truncation") @SkipForDialect(dialectClass = OracleDialect.class, reason = "See HHH-16442, Oracle trunc() throws away the timezone") + @SkipForDialect(dialectClass = InformixDialect.class, reason = "Informix doesn't support any form of date truncation") public void testDateTruncWithOffsetFunction(SessionFactoryScope scope) { scope.inTransaction( session -> { @@ -1030,6 +1039,44 @@ public void testCastFunction(SessionFactoryScope scope) { ); } + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-18447") + public void testCastStringToBoolean(SessionFactoryScope scope) { + scope.inTransaction( session -> { + assertThat( session.createQuery("select cast('1' as Boolean)", Boolean.class).getSingleResult(), is(true) ); + assertThat( session.createQuery("select cast('0' as Boolean)", Boolean.class).getSingleResult(), is(false) ); + assertThat( session.createQuery("select cast('y' as Boolean)", Boolean.class).getSingleResult(), is(true) ); + assertThat( session.createQuery("select cast('n' as Boolean)", Boolean.class).getSingleResult(), is(false) ); + assertThat( session.createQuery("select cast('Y' as Boolean)", Boolean.class).getSingleResult(), is(true) ); + assertThat( session.createQuery("select cast('N' as Boolean)", Boolean.class).getSingleResult(), is(false) ); + assertThat( session.createQuery("select cast('t' as Boolean)", Boolean.class).getSingleResult(), is(true) ); + assertThat( session.createQuery("select cast('f' as Boolean)", Boolean.class).getSingleResult(), is(false) ); + assertThat( session.createQuery("select cast('T' as Boolean)", Boolean.class).getSingleResult(), is(true) ); + assertThat( session.createQuery("select cast('F' as Boolean)", Boolean.class).getSingleResult(), is(false) ); + assertThat( session.createQuery("select cast('true' as Boolean)", Boolean.class).getSingleResult(), is(true) ); + assertThat( session.createQuery("select cast('false' as Boolean)", Boolean.class).getSingleResult(), is(false) ); + assertThat( session.createQuery("select cast('TRUE' as Boolean)", Boolean.class).getSingleResult(), is(true) ); + assertThat( session.createQuery("select cast('FALSE' as Boolean)", Boolean.class).getSingleResult(), is(false) ); + }); + } + + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-18447") + public void testCastInvalidStringToBoolean(SessionFactoryScope scope) { + scope.inTransaction( session -> { + try { + session.createQuery( "select cast('bla' as Boolean)", Boolean.class ).getSingleResult(); + fail("Casting invalid boolean string should fail"); + } + catch ( HibernateException e ) { + // Expected + if ( !( e instanceof JDBCException || e instanceof ExecutionException ) ) { + throw e; + } + } + } ); + } + @Test @SkipForDialect(dialectClass = DB2Dialect.class, matchSubTypes = true) @SkipForDialect(dialectClass = DerbyDialect.class) @@ -1063,6 +1110,7 @@ public void testCastDoubleToString(SessionFactoryScope scope) { @SkipForDialect(dialectClass = OracleDialect.class, reason = "Oracle treats the cast value as a hexadecimal literal") @SkipForDialect(dialectClass = HSQLDialect.class, reason = "HSQL treats the cast value as a hexadecimal literal") @SkipForDialect(dialectClass = AltibaseDialect.class, reason = "Altibase doesn't support casting varchar to binary") + @SkipForDialect(dialectClass = InformixDialect.class, reason = "Informix doesn't support casting varchar to byte") public void testCastFunctionBinary(SessionFactoryScope scope) { scope.inTransaction( session -> { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/InsertConflictWithCriteriaCopyTreeEnabledTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/InsertConflictWithCriteriaCopyTreeEnabledTests.java new file mode 100644 index 000000000000..5df94e23a97d --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/InsertConflictWithCriteriaCopyTreeEnabledTests.java @@ -0,0 +1,92 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.query.hql; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Tuple; +import org.hibernate.cfg.QuerySettings; +import org.hibernate.query.criteria.HibernateCriteriaBuilder; +import org.hibernate.query.criteria.JpaCriteriaInsertSelect; +import org.hibernate.query.criteria.JpaCriteriaInsertValues; +import org.hibernate.query.criteria.JpaCriteriaQuery; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.Test; + + +@DomainModel( + annotatedClasses = { + InsertConflictWithCriteriaCopyTreeEnabledTests.TestEntity.class, + InsertConflictWithCriteriaCopyTreeEnabledTests.AnotherTestEntity.class, + } +) +@ServiceRegistry( + settings = {@Setting(name = QuerySettings.CRITERIA_COPY_TREE, value = "true")} +) +@SessionFactory +@JiraKey("HHH-19314") +public class InsertConflictWithCriteriaCopyTreeEnabledTests { + + @Test + void createCriteriaInsertValuesTest(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + HibernateCriteriaBuilder cb = session.getCriteriaBuilder(); + + JpaCriteriaInsertValues insertIntoItem = cb + .createCriteriaInsertValues( TestEntity.class ); + insertIntoItem.setInsertionTargetPaths( insertIntoItem.getTarget().get( "id" ) ); + insertIntoItem.values( cb.values( cb.value( 1L ) ) ); + insertIntoItem.onConflict().onConflictDoNothing(); + + session.createMutationQuery( insertIntoItem ).executeUpdate(); + } + ); + } + + @Test + void createCriteriaInsertSelectTest(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + HibernateCriteriaBuilder cb = session.getCriteriaBuilder(); + + JpaCriteriaInsertSelect insertIntoItem = cb + .createCriteriaInsertSelect( TestEntity.class ); + insertIntoItem.setInsertionTargetPaths( insertIntoItem.getTarget().get( "id" ) ); + + JpaCriteriaQuery cq = cb.createQuery( Tuple.class ); + cq.select( cb.tuple( cb.literal( 1 ) ) ); + cq.fetch( 1 ); + insertIntoItem.select( cq ); + insertIntoItem.onConflict().onConflictDoNothing(); + + session.createMutationQuery( insertIntoItem ).executeUpdate(); + } + ); + } + + @Entity(name = "TestEntity") + public static class TestEntity { + @Id + private Long id; + + private String name; + + } + + @Entity(name = "AnotherTestEntity") + public static class AnotherTestEntity { + @Id + private Long id; + + private String name; + + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/LiteralTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/LiteralTests.java index 2aef7e6ceddc..ef0a7ad92973 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/LiteralTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/LiteralTests.java @@ -13,6 +13,7 @@ import org.hibernate.testing.orm.junit.ServiceRegistry; import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.SkipForDialect; import org.junit.jupiter.api.Test; import java.math.BigDecimal; @@ -25,6 +26,7 @@ import java.time.LocalDateTime; import java.time.LocalTime; +import org.hibernate.community.dialect.InformixDialect; import org.hibernate.query.spi.QueryImplementor; import org.hibernate.type.descriptor.java.PrimitiveByteArrayJavaType; @@ -45,6 +47,7 @@ public class LiteralTests { @Test + @SkipForDialect(dialectClass = InformixDialect.class, reason = "Informix does not support binary literals") public void testBinaryLiteral(SessionFactoryScope scope) { scope.inTransaction( session -> { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/MultiValuedParameterTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/MultiValuedParameterTest.java index a083fbcd3a9c..569e6bf30020 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/MultiValuedParameterTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/MultiValuedParameterTest.java @@ -6,6 +6,7 @@ */ package org.hibernate.orm.test.query.hql; +import java.math.BigDecimal; import java.math.BigInteger; import java.time.LocalDate; import java.util.ArrayList; @@ -28,6 +29,7 @@ import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; /** * @author Andrea Boriero @@ -102,6 +104,20 @@ public void test() { } ); } + @Test + @Jira( "https://hibernate.atlassian.net/browse/HHH-18575" ) + void testMultiValuedBigDecimals() { + inTransaction( session -> { + assertEquals( + 1, + session.createQuery("SELECT 1 WHERE :value IN (:list)", Integer.class) + .setParameter( "value", BigDecimal.valueOf( 2.0)) + .setParameter("list", List.of(BigDecimal.valueOf(2.0), BigDecimal.valueOf(3.0))) + .getSingleResult() + ); + }); + } + @AfterAll public void cleanupData() { inTransaction( session -> { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/UnionAllSelectNullTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/UnionAllSelectNullTest.java new file mode 100644 index 000000000000..519be3f4c403 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/UnionAllSelectNullTest.java @@ -0,0 +1,140 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.query.hql; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Tuple; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@DomainModel( + annotatedClasses = { + UnionAllSelectNullTest.TestEntity.class, + UnionAllSelectNullTest.AnotherTestEntity.class + } +) +@SessionFactory +@JiraKey("HHH-18720") +class UnionAllSelectNullTest { + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.persist( new TestEntity( 1L, "a" ) ); + session.persist( new AnotherTestEntity( 2L, "b" ) ); + } + ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.createQuery( "delete TestEntity" ).executeUpdate(); + session.createQuery( "delete AnotherTestEntity" ).executeUpdate(); + } + ); + } + + @Test + void testSelect(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + List resultList = session.createQuery( + "SELECT te.id as id from TestEntity te" + + " union all SELECT null as id from AnotherTestEntity ate" + , Tuple.class ) + .getResultList(); + assertThat( resultList.size() ).isEqualTo( 2 ); + assertResultIsCorrect( resultList, 1L, null ); + } + ); + } + + @Test + void testSelect2(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + List resultList = session.createQuery( + "SELECT null as id from TestEntity te" + + " union all SELECT ate.id as id from AnotherTestEntity ate" + , Tuple.class ) + .getResultList(); + assertThat( resultList.size() ).isEqualTo( 2 ); + assertResultIsCorrect( resultList, null, 2L ); + } + ); + } + + @Test + void testSelect3(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + List resultList = session.createQuery( + "SELECT te.id as id from TestEntity te" + + " union all SELECT ate.id as id from AnotherTestEntity ate" + , Tuple.class ) + .getResultList(); + assertThat( resultList.size() ).isEqualTo( 2 ); + assertResultIsCorrect( resultList, 1L, 2L ); + } + ); + } + + private static void assertResultIsCorrect(List resultList, Long id1, Long id2) { + Set ids = new HashSet<>( 2 ); + ids.add( (Long) resultList.get( 0 ).get( "id" ) ); + ids.add( (Long) resultList.get( 1 ).get( "id" ) ); + assertThat( ids.contains( id1 ) ).as( "Result does not contain expected value:" + id1 ).isTrue(); + assertThat( ids.contains( id2 ) ).as( "Result does not contain expected value:" + id2 ).isTrue(); + } + + @Entity(name = "TestEntity") + public static class TestEntity { + + @Id + private Long id; + + private String name; + + public TestEntity() { + } + + public TestEntity(Long id, String name) { + this.id = id; + this.name = name; + } + } + + @Entity(name = "AnotherTestEntity") + public static class AnotherTestEntity { + + @Id + private Long id; + + private String name; + + public AnotherTestEntity() { + } + + public AnotherTestEntity(Long id, String name) { + this.id = id; + this.name = name; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/instantiation/InstantiationWithGenericsExpressionTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/instantiation/InstantiationWithGenericsExpressionTest.java new file mode 100644 index 000000000000..78592a687767 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/instantiation/InstantiationWithGenericsExpressionTest.java @@ -0,0 +1,224 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.query.hql.instantiation; + +import java.io.Serializable; + +import org.hibernate.annotations.Imported; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.MappedSuperclass; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Marco Belladelli + */ +@DomainModel(annotatedClasses = { + InstantiationWithGenericsExpressionTest.AbstractEntity.class, + InstantiationWithGenericsExpressionTest.ConcreteEntity.class, + InstantiationWithGenericsExpressionTest.ConstructorDto.class, + InstantiationWithGenericsExpressionTest.InjectionDto.class, +}) +@SessionFactory +@Jira("https://hibernate.atlassian.net/browse/HHH-18218") +public class InstantiationWithGenericsExpressionTest { + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final ConcreteEntity entity = new ConcreteEntity(); + entity.setId( 1 ); + entity.setGen( 1L ); + entity.setData( "entity_1" ); + session.persist( entity ); + } ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( session -> session.createMutationQuery( "delete from ConcreteEntity" ).executeUpdate() ); + } + + @Test + public void testConstructorBinaryExpression(SessionFactoryScope scope) { + scope.inTransaction( session -> assertThat( session.createQuery( + "select new ConstructorDto(e.gen+e.gen, e.data) from ConcreteEntity e", + ConstructorDto.class + ).getSingleResult() ).extracting( ConstructorDto::getGen, ConstructorDto::getData ) + .containsExactly( 2L, "entity_1" ) ); + } + + @Test + public void testImplicitConstructorBinaryExpression(SessionFactoryScope scope) { + scope.inTransaction( session -> assertThat( session.createQuery( + "select e.gen+e.gen, e.data from ConcreteEntity e", + ConstructorDto.class + ).getSingleResult() ).extracting( ConstructorDto::getGen, ConstructorDto::getData ) + .containsExactly( 2L, "entity_1" ) ); + } + + @Test + public void testInjectionBinaryExpression(SessionFactoryScope scope) { + scope.inTransaction( session -> assertThat( session.createQuery( + "select new InjectionDto(e.gen+e.gen as gen, e.data as data) from ConcreteEntity e", + InjectionDto.class + ).getSingleResult() ).extracting( InjectionDto::getGen, InjectionDto::getData ) + .containsExactly( 2L, "entity_1" ) ); + } + + @Test + public void testConstructorUnaryExpression(SessionFactoryScope scope) { + scope.inTransaction( session -> assertThat( session.createQuery( + "select new ConstructorDto(-e.gen, e.data) from ConcreteEntity e", + ConstructorDto.class + ).getSingleResult() ).extracting( ConstructorDto::getGen, ConstructorDto::getData ) + .containsExactly( -1L, "entity_1" ) ); + } + + @Test + public void testImplicitConstructorUnaryExpression(SessionFactoryScope scope) { + scope.inTransaction( session -> assertThat( session.createQuery( + "select -e.gen, e.data from ConcreteEntity e", + ConstructorDto.class + ).getSingleResult() ).extracting( ConstructorDto::getGen, ConstructorDto::getData ) + .containsExactly( -1L, "entity_1" ) ); + } + + @Test + public void testInjectionUnaryExpression(SessionFactoryScope scope) { + scope.inTransaction( session -> assertThat( session.createQuery( + "select new InjectionDto(-e.gen as gen, e.data as data) from ConcreteEntity e", + InjectionDto.class + ).getSingleResult() ).extracting( InjectionDto::getGen, InjectionDto::getData ) + .containsExactly( -1L, "entity_1" ) ); + } + + @Test + public void testConstructorFunction(SessionFactoryScope scope) { + scope.inTransaction( session -> { + assertThat( session.createQuery( + "select new ConstructorDto(abs(e.gen), e.data) from ConcreteEntity e", + ConstructorDto.class + ).getSingleResult() ).extracting( ConstructorDto::getGen, ConstructorDto::getData ) + .containsExactly( 1L, "entity_1" ); + } ); + } + + @Test + public void testImplicitFunction(SessionFactoryScope scope) { + scope.inTransaction( session -> { + assertThat( session.createQuery( + "select abs(e.gen), e.data from ConcreteEntity e", + ConstructorDto.class + ).getSingleResult() ).extracting( ConstructorDto::getGen, ConstructorDto::getData ) + .containsExactly( 1L, "entity_1" ); + } ); + } + + @Test + public void testInjectionFunction(SessionFactoryScope scope) { + scope.inTransaction( session -> { + assertThat( session.createQuery( + "select new InjectionDto(abs(e.gen) as gen, e.data as data) from ConcreteEntity e", + InjectionDto.class + ).getSingleResult() ).extracting( InjectionDto::getGen, InjectionDto::getData ) + .containsExactly( 1L, "entity_1" ); + } ); + } + + @MappedSuperclass + static abstract class AbstractEntity { + @Id + protected Integer id; + + protected K gen; + + public Integer getId() { + return id; + } + + public void setId(final Integer id) { + this.id = id; + } + + public K getGen() { + return gen; + } + + public void setGen(final K gen) { + this.gen = gen; + } + } + + @Entity(name = "ConcreteEntity") + static class ConcreteEntity extends AbstractEntity { + protected String data; + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } + } + + @Imported + public static class ConstructorDto { + private final Long gen; + + private final String data; + + public ConstructorDto(Long gen, String data) { + this.gen = gen; + this.data = data; + } + + public Long getGen() { + return gen; + } + + public String getData() { + return data; + } + } + + @Imported + public static class InjectionDto { + private long gen; + + private String data; + + public long getGen() { + return gen; + } + + public void setGen(final long gen) { + this.gen = gen; + } + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/set/UnionOfPartitionResultsTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/set/UnionOfPartitionResultsTest.java new file mode 100644 index 000000000000..ff9896246bb6 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/set/UnionOfPartitionResultsTest.java @@ -0,0 +1,215 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.query.hql.set; + +import java.time.LocalDate; + +import org.hibernate.query.Query; + +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; + +/** + * @author Jan Schatteman + */ +@DomainModel( + annotatedClasses = {UnionOfPartitionResultsTest.Apple.class, UnionOfPartitionResultsTest.Pie.class} +) +@SessionFactory +@JiraKey( "HHH-18069" ) +public class UnionOfPartitionResultsTest { + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsUnion.class) + public void testSubqueryWithUnion(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + String q = "SELECT id " + + "FROM " + + "( " + + "( SELECT id id, bakedPie bakedPie " + + "\tFROM Apple c " + + ") " + + "\tUNION ALL " + + "( SELECT id id, bakedPie bakedPie " + + "\tFROM Apple a " + + ") " + + ")"; + + Query query = session.createQuery( q ); + + query.list(); + } + ); + } + + @Test + public void testSubquery(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + String q = "SELECT id " + + "FROM " + + "( " + + "\tSELECT id id, bakedPie bakedPie " + + "\tFROM Apple c " + + ")"; + + Query query = session.createQuery( q ); + + query.list(); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsUnion.class) + public void testUnionQuery(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + String q = "( SELECT id id, bakedPie bakedPie " + + "\tFROM Apple c " + + ") " + + "\tUNION ALL " + + "( SELECT id id, bakedPie bakedPie " + + "\tFROM Apple c " + + ") "; + + Query query = session.createQuery( q ); + + query.list(); + } + ); + } + + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsUnion.class) + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportPartitionBy.class) + public void testUnionOfPartitionResults(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + String q = + "SELECT new CurrentApple(id, bakedPie.id, dir) " + + "FROM (" + + "(" + + "SELECT id id, bakedPie bakedPie, bakedOn bakedOn, MAX(bakedOn) OVER (PARTITION BY bakedPie.id) mbo, -1 dir " + + "FROM Apple c " + + "WHERE bakedPie.id IN (1,2,3,4) AND bakedOn <= :now" + + ") UNION ALL (" + + "SELECT id id, bakedPie bakedPie, bakedOn bakedOn, MIN(bakedOn) OVER (PARTITION BY bakedPie.id) mbo, 1 dir " + + "FROM Apple c " + + "WHERE bakedPie.id IN (1,2,3,4) AND bakedOn > :now" + + ")" + + ") " + + "WHERE bakedOn = mbo ORDER BY dir"; + + Query query = session.createQuery( q, CurrentApple.class ); + query.setParameter( "now", LocalDate.now()); + + query.list(); + } + ); + } + + + public static class CurrentApple { + private final int id; + private final int pieId; + private final int dir; + + public CurrentApple(int id, int pieId, int dir) { + this.id = id; + this.pieId = pieId; + this.dir = dir; + } + + public int getDir() { + return dir; + } + + public int getId() { + return id; + } + + public int getPieId() { + return pieId; + } + } + + @Entity(name = "Apple") + public static class Apple { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + private LocalDate bakedOn; + @ManyToOne + private Pie bakedPie; + + public Integer getId() { + return id; + } + + public Apple setId(Integer id) { + this.id = id; + return this; + } + + public LocalDate getBakedOn() { + return bakedOn; + } + + public Apple setBakedOn(LocalDate bakedOn) { + this.bakedOn = bakedOn; + return this; + } + + public Pie getBakedPie() { + return bakedPie; + } + + public Apple setBakedPie(Pie bakedPie) { + this.bakedPie = bakedPie; + return this; + } + } + + @Entity(name = "Pie") + public static class Pie { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + private String taste; + + public Integer getId() { + return id; + } + + public Pie setId(Integer id) { + this.id = id; + return this; + } + + public String getTaste() { + return taste; + } + + public Pie setTaste(String taste) { + this.taste = taste; + return this; + } + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/joinfetch/JoinFetchNestedAssociationsTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/joinfetch/JoinFetchNestedAssociationsTest.java new file mode 100644 index 000000000000..82699cf60641 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/joinfetch/JoinFetchNestedAssociationsTest.java @@ -0,0 +1,181 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.query.joinfetch; + +import java.util.HashSet; +import java.util.Set; + +import org.hibernate.Hibernate; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.MappedSuperclass; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Marco Belladelli + */ +@DomainModel( annotatedClasses = { + JoinFetchNestedAssociationsTest.AEntity.class, + JoinFetchNestedAssociationsTest.BaseBEntity.class, + JoinFetchNestedAssociationsTest.B1Entity.class, + JoinFetchNestedAssociationsTest.B2Entity.class, + JoinFetchNestedAssociationsTest.CEntity.class, +} ) +@SessionFactory +@Jira( "https://hibernate.atlassian.net/browse/HHH-18469" ) +public class JoinFetchNestedAssociationsTest { + @Test + public void test(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final AEntity result = session.createQuery( + "select a from AEntity a " + + "left join fetch a.b_entity b_join " + + "left join fetch b_join.c_entities c", + AEntity.class + ).getSingleResult(); + final B2Entity bEntity = result.getB_entity(); + assertThat( bEntity ).matches( Hibernate::isInitialized ); + assertThat( bEntity.getC_entities() ).matches( Hibernate::isInitialized ) + .hasSize( 1 ) + .allMatch( c -> c.getName().equals( "c_entity" ) ); + } ); + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + // B1 or B2 doesn't matter as they map to the same table + final B1Entity b = new B1Entity(); + b.setId( 1L ); + session.persist( b ); + session.flush(); + + final AEntity a = new AEntity(); + a.setB_entity_id( 1L ); + final CEntity c = new CEntity(); + c.setbEntityId( 1L ); + c.setName( "c_entity" ); + session.persist( a ); + session.persist( c ); + } ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Entity( name = "AEntity" ) + static class AEntity { + @Id + @GeneratedValue + private Long id; + + @OneToOne + @JoinColumn( name = "b_entity_id", updatable = false, insertable = false ) + private B2Entity b_entity; + + @Column( name = "b_entity_id" ) + private Long b_entity_id; + + public B2Entity getB_entity() { + return b_entity; + } + + public Long getB_entity_id() { + return b_entity_id; + } + + public void setB_entity_id(Long b_entity_id) { + this.b_entity_id = b_entity_id; + } + } + + @MappedSuperclass + static class BaseBEntity { + @Id + private Long id; + + @OneToMany( mappedBy = "b_entity" ) + private Set c_entities = new HashSet<>(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Set getC_entities() { + return c_entities; + } + } + + @Entity( name = "B1Entity" ) + @Table( name = "b_entities" ) + static class B1Entity extends BaseBEntity { + } + + @Entity( name = "B2Entity" ) + @Table( name = "b_entities" ) + static class B2Entity extends BaseBEntity { + } + + @Entity( name = "CEntity" ) + static class CEntity { + @Id + @GeneratedValue + private Long id; + + @Column( name = "b_id" ) + private Long bEntityId; + + @ManyToOne + @JoinColumn( name = "b_id", updatable = false, insertable = false ) + private B1Entity b_entity; + + private String name; + + public Long getbEntityId() { + return bEntityId; + } + + public void setbEntityId(Long bEntityId) { + this.bEntityId = bEntityId; + } + + public B1Entity getB_entity() { + return b_entity; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/sql/NativeQueryJoinTableTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/sql/NativeQueryJoinTableTest.java new file mode 100644 index 000000000000..ddece2eb3dd0 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/sql/NativeQueryJoinTableTest.java @@ -0,0 +1,205 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.query.sql; + +import java.util.HashSet; +import java.util.Set; + +import org.hibernate.query.NativeQuery; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinTable; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * @author Marco Belladelli + */ +@DomainModel( annotatedClasses = { + NativeQueryJoinTableTest.Shelf.class, NativeQueryJoinTableTest.Book.class, +} ) +@SessionFactory +@Jira( "https://hibernate.atlassian.net/browse/HHH-18494" ) +public class NativeQueryJoinTableTest { + private static final String SHELF_ID = "shelf1"; + private static final String FILE_ID = "file1"; + + @Test + public void testTypedQuery(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final NativeQuery query = session.createNativeQuery( + "select book.*, book_1_.shelfid from BOOK_T book, SHELF_BOOK book_1_ where book.fileid = book_1_.fileid", + Book.class + ); + final Book retrievedBook = query.getSingleResult(); + assertEquals( FILE_ID, retrievedBook.getFileId() ); + assertEquals( "Birdwatchers Guide to Dodos", retrievedBook.getTitle() ); + assertEquals( "nonfiction", retrievedBook.getShelf().getArea() ); + assertEquals( 3, retrievedBook.getShelf().getShelfNumber() ); + assertEquals( SHELF_ID, retrievedBook.getShelf().getShelfid() ); + assertEquals( 5, retrievedBook.getShelf().getPosition() ); + } ); + } + + @Test + public void testAddEntity(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final NativeQuery query = session.createNativeQuery( + "select {book.*}, book_1_.shelfid from BOOK_T book, SHELF_BOOK book_1_ where book.fileid = book_1_.fileid" + ); + query.addEntity( "book", Book.class ); + final Book retrievedBook = (Book) query.getSingleResult(); + assertEquals( FILE_ID, retrievedBook.getFileId() ); + assertEquals( "Birdwatchers Guide to Dodos", retrievedBook.getTitle() ); + assertEquals( "nonfiction", retrievedBook.getShelf().getArea() ); + assertEquals( 3, retrievedBook.getShelf().getShelfNumber() ); + assertEquals( SHELF_ID, retrievedBook.getShelf().getShelfid() ); + assertEquals( 5, retrievedBook.getShelf().getPosition() ); + } ); + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final Shelf shelf = new Shelf(); + shelf.setShelfid( SHELF_ID ); + shelf.setArea( "nonfiction" ); + shelf.setPosition( Integer.valueOf( 5 ) ); + shelf.setShelfNumber( Integer.valueOf( 3 ) ); + shelf.setBooks( new HashSet<>() ); + session.persist( shelf ); + final Book book = new Book( FILE_ID ); + book.setTitle( "Birdwatchers Guide to Dodos" ); + book.setShelf( shelf ); + session.persist( book ); + } ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Entity( name = "Shelf" ) + @Table( name = "SHELF" ) + public static class Shelf { + @Id + private String shelfid; + + private String area; + + private Integer shelfNumber; + + private Integer position; + + @OneToMany + @JoinTable( name = "SHELF_BOOK", joinColumns = @JoinColumn( name = "shelfid" ), inverseJoinColumns = @JoinColumn( name = "fileid" ) ) + private Set books; + + public Shelf() { + } + + public String getShelfid() { + return shelfid; + } + + public void setShelfid(String shelfid) { + this.shelfid = shelfid; + } + + public String getArea() { + return area; + } + + public void setArea(String area) { + this.area = area; + } + + public Integer getShelfNumber() { + return shelfNumber; + } + + public void setShelfNumber(Integer shelfNumber) { + this.shelfNumber = shelfNumber; + } + + public Integer getPosition() { + return position; + } + + public void setPosition(Integer position) { + this.position = position; + } + + public Set getBooks() { + return books; + } + + public void setBooks(Set books) { + this.books = books; + } + } + + ; + + @Entity( name = "Book" ) + @Table( name = "BOOK_T" ) + public static class Book { + @Id + private String fileid; + + private String title; + + @ManyToOne( optional = false, fetch = FetchType.EAGER ) + @JoinTable( name = "SHELF_BOOK" ) + @JoinColumn( name = "shelfid" ) + private Shelf shelf; + + public Book() { + } + + public Book(final String fileid) { + this.fileid = fileid; + } + + public String getFileId() { + return fileid; + } + + public void setFileId(final String fileid) { + this.fileid = fileid; + } + + public String getTitle() { + return title; + } + + public void setTitle(final String title) { + this.title = title; + } + + public Shelf getShelf() { + return shelf; + } + + public void setShelf(Shelf shelf) { + this.shelf = shelf; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/sql/NativeQueryResultBuilderTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/sql/NativeQueryResultBuilderTests.java index b04e132ed4ea..147af62497c7 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/sql/NativeQueryResultBuilderTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/sql/NativeQueryResultBuilderTests.java @@ -20,6 +20,7 @@ import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.ModelPart; import org.hibernate.metamodel.mapping.internal.BasicAttributeMapping; +import org.hibernate.testing.orm.domain.gambit.BasicEntity; import org.hibernate.type.descriptor.converter.spi.BasicValueConverter; import org.hibernate.type.descriptor.converter.spi.JpaAttributeConverter; import org.hibernate.query.sql.spi.NativeQueryImplementor; @@ -28,6 +29,7 @@ import org.hibernate.testing.orm.domain.StandardDomainModel; import org.hibernate.testing.orm.domain.gambit.EntityOfBasics; import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactoryScope; import org.junit.jupiter.api.AfterEach; @@ -279,6 +281,45 @@ public void testConvertedAttributeBasedBuilder(SessionFactoryScope scope) { ); } + @Test + @JiraKey("HHH-18629") + public void testNativeQueryWithResultClass(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + final String sql = "select data, id from BasicEntity"; + final NativeQueryImplementor query = session.createNativeQuery( sql, BasicEntity.class ); + + final List results = query.list(); + assertThat( results.size(), is( 1 ) ); + + final BasicEntity result = (BasicEntity) results.get( 0 ); + + assertThat( result.getData(), is( STRING_VALUE ) ); + assertThat( result.getId(), is( 1 ) ); + } + ); + } + + @Test + @JiraKey("HHH-18629") + public void testNativeQueryWithResultClassAndPlaceholders(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + final String sql = "select {be.*} from BasicEntity be"; + final NativeQueryImplementor query = session.createNativeQuery( sql, BasicEntity.class ); + query.addEntity( "be", BasicEntity.class ); + + final List results = query.list(); + assertThat( results.size(), is( 1 ) ); + + final BasicEntity result = (BasicEntity) results.get( 0 ); + + assertThat( result.getData(), is( STRING_VALUE ) ); + assertThat( result.getId(), is( 1 ) ); + } + ); + } + @BeforeAll public void verifyModel(SessionFactoryScope scope) { final EntityMappingType entityDescriptor = scope.getSessionFactory() @@ -317,13 +358,16 @@ public void prepareData(SessionFactoryScope scope) throws MalformedURLException entityOfBasics.setTheInstant( Instant.EPOCH ); session.persist( entityOfBasics ); + + session.persist( new BasicEntity( 1, STRING_VALUE ) ); } ); scope.inTransaction( session -> { - final EntityOfBasics entity = session.get( EntityOfBasics.class, 1 ); - assertThat( entity, notNullValue() ); + assertThat( session.get( EntityOfBasics.class, 1 ), notNullValue() ); + + assertThat( session.get( BasicEntity.class, 1 ), notNullValue() ); } ); } @@ -331,7 +375,10 @@ public void prepareData(SessionFactoryScope scope) throws MalformedURLException @AfterEach public void cleanUpData(SessionFactoryScope scope) { scope.inTransaction( - session -> session.createQuery( "delete EntityOfBasics" ).executeUpdate() + session -> { + session.createQuery( "delete EntityOfBasics" ).executeUpdate(); + session.createQuery( "delete BasicEntity" ).executeUpdate(); + } ); } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/querycache/EntityWithCollectionReloadCacheTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/querycache/EntityWithCollectionReloadCacheTest.java new file mode 100644 index 000000000000..a2af4ac5dd19 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/querycache/EntityWithCollectionReloadCacheTest.java @@ -0,0 +1,444 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.querycache; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +import org.hibernate.Hibernate; +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Tuple; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Marco Belladelli + */ +@DomainModel( annotatedClasses = { + EntityWithCollectionReloadCacheTest.Course.class, + EntityWithCollectionReloadCacheTest.Subject.class, + EntityWithCollectionReloadCacheTest.Demand.class, + EntityWithCollectionReloadCacheTest.Student.class, + EntityWithCollectionReloadCacheTest.Major.class, + EntityWithCollectionReloadCacheTest.StudentMajor.class, +} ) +@SessionFactory +@Jira( "https://hibernate.atlassian.net/browse/HHH-18478" ) +public class EntityWithCollectionReloadCacheTest { + @Test + public void test(SessionFactoryScope scope) { + scope.inTransaction( session -> { + for ( String subject : new String[] { "MATH", "BIOL", "CS" } ) { + final List resultList = session.createQuery( + "select d.id, d.course, s " + + "from Demand d inner join d.student s left join fetch s.majors " + + "where d.course.subject.name = :subject", + Tuple.class + ) + .setParameter( "subject", subject ) + .setCacheable( true ) + .getResultList(); + assertThat( resultList ).hasSize( subject.equals( "MATH" ) ? 5 : 4 ).allSatisfy( tuple -> { + assertThat( tuple.get( 1, Course.class ).getSubject().getName() ).isEqualTo( subject ); + assertThat( tuple.get( 2, Student.class ).getMajors() ).matches( Hibernate::isInitialized ); + } ); + } + } ); + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + // Create two majors + final Major m1 = new Major(); + m1.setName( "Biology" ); + session.persist( m1 ); + final Major m2 = new Major(); + m2.setName( "Computer Science" ); + session.persist( m2 ); + + // Create three students + final Student s1 = new Student(); + s1.setName( "Andrew" ); + final StudentMajor sm1 = new StudentMajor(); + sm1.setStudent( s1 ); + sm1.setMajor( m1 ); + sm1.setClassification( "01" ); + s1.addToMajors( sm1 ); + session.persist( s1 ); + + final Student s2 = new Student(); + s2.setName( "Brian" ); + final StudentMajor sm2 = new StudentMajor(); + sm2.setStudent( s2 ); + sm2.setMajor( m1 ); + sm2.setClassification( "02" ); + s2.addToMajors( sm2 ); + session.persist( s2 ); + + final Student s3 = new Student(); + s3.setName( "Charlie" ); + final StudentMajor sm3 = new StudentMajor(); + sm3.setStudent( s3 ); + sm3.setMajor( m1 ); + sm3.setClassification( "01" ); + s3.addToMajors( sm3 ); + final StudentMajor sm4 = new StudentMajor(); + sm4.setStudent( s3 ); + sm4.setMajor( m2 ); + sm4.setClassification( "02" ); + s3.addToMajors( sm4 ); + session.persist( s3 ); + + // Create two subjects + final Subject math = new Subject(); + math.setName( "MATH" ); + session.persist( math ); + final Subject biology = new Subject(); + biology.setName( "BIOL" ); + session.persist( biology ); + final Subject cs = new Subject(); + cs.setName( "CS" ); + session.persist( cs ); + + // Create a few courses + final Course c1 = new Course(); + c1.setSubject( math ); + c1.setNumber( "101" ); + session.persist( c1 ); + final Course c2 = new Course(); + c2.setSubject( math ); + c2.setNumber( "201" ); + session.persist( c2 ); + final Course c3 = new Course(); + c3.setSubject( biology ); + c3.setNumber( "101" ); + session.persist( c3 ); + final Course c4 = new Course(); + c4.setSubject( biology ); + c4.setNumber( "201" ); + session.persist( c4 ); + final Course c5 = new Course(); + c5.setSubject( cs ); + c5.setNumber( "101" ); + session.persist( c5 ); + final Course c6 = new Course(); + c6.setSubject( cs ); + c6.setNumber( "201" ); + session.persist( c6 ); + + // Create some course demands + final Demand d1 = new Demand(); + d1.setCourse( c1 ); + d1.setStudent( s1 ); + session.persist( d1 ); + final Demand d2 = new Demand(); + d2.setCourse( c1 ); + d2.setStudent( s2 ); + session.persist( d2 ); + final Demand d3 = new Demand(); + d3.setCourse( c2 ); + d3.setStudent( s2 ); + session.persist( d3 ); + final Demand d4 = new Demand(); + d4.setCourse( c2 ); + d4.setStudent( s3 ); + session.persist( d4 ); + final Demand d5 = new Demand(); + d5.setCourse( c3 ); + d5.setStudent( s1 ); + session.persist( d5 ); + final Demand d6 = new Demand(); + d6.setCourse( c3 ); + d6.setStudent( s3 ); + session.persist( d6 ); + final Demand d7 = new Demand(); + d7.setCourse( c4 ); + d7.setStudent( s1 ); + session.persist( d7 ); + final Demand d8 = new Demand(); + d8.setCourse( c5 ); + d8.setStudent( s2 ); + session.persist( d8 ); + final Demand d9 = new Demand(); + d9.setCourse( c6 ); + d9.setStudent( s2 ); + session.persist( d9 ); + final Demand d0 = new Demand(); + d0.setCourse( c6 ); + d0.setStudent( s3 ); + session.persist( d0 ); + } ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Entity( name = "Course" ) + static class Course { + private UUID id; + private String number; + private Subject subject; + + @Id + @GeneratedValue + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + @Column( name = "number_col" ) + public String getNumber() { + return number; + } + + public void setNumber(String number) { + this.number = number; + } + + @ManyToOne + @JoinColumn( name = "subject_id" ) + public Subject getSubject() { + return subject; + } + + public void setSubject(Subject subject) { + this.subject = subject; + } + + @Override + public String toString() { + return getSubject() + " " + getNumber(); + } + } + + @Entity( name = "Subject" ) + static class Subject { + private UUID id; + private String name; + + @Id + @GeneratedValue + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } + } + + @Entity( name = "Demand" ) + static class Demand { + private UUID id; + private Student student; + private Course course; + + @Id + @GeneratedValue + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + @ManyToOne + @JoinColumn( name = "student_id" ) + public Student getStudent() { + return student; + } + + public void setStudent(Student student) { + this.student = student; + } + + @ManyToOne + @JoinColumn( name = "course_id" ) + public Course getCourse() { + return course; + } + + public void setCourse(Course course) { + this.course = course; + } + + @Override + public String toString() { + return getStudent() + " for " + getCourse(); + } + } + + @Entity( name = "Student" ) + static class Student { + private UUID id; + private String name; + private Set majors; + + @Id + @GeneratedValue + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @OneToMany( fetch = FetchType.LAZY, mappedBy = "student", cascade = { CascadeType.ALL } ) + @Cache( usage = CacheConcurrencyStrategy.READ_WRITE ) + public Set getMajors() { + return majors; + } + + public void setMajors(Set majors) { + this.majors = majors; + } + + public void addToMajors(StudentMajor major) { + if ( this.majors == null ) { + this.majors = new HashSet(); + } + this.majors.add( major ); + } + + @Override + public String toString() { + return getName() + " " + getMajors(); + } + } + + @Entity( name = "StudentMajor" ) + static class StudentMajor { + private UUID id; + private Student student; + private Major major; + private String classification; + + @Id + @GeneratedValue + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public String getClassification() { + return classification; + } + + public void setClassification(String classification) { + this.classification = classification; + } + + @ManyToOne + @JoinColumn( name = "student_id" ) + public Student getStudent() { + return student; + } + + public void setStudent(Student student) { + this.student = student; + } + + @ManyToOne + @JoinColumn( name = "major_id" ) + public Major getMajor() { + return major; + } + + public void setMajor(Major major) { + this.major = major; + } + + @Override + public String toString() { + return getMajor().getName() + " " + getClassification(); + } + } + + @Entity( name = "Major" ) + static class Major { + private UUID id; + + @Column( name = "name_col" ) + private String name; + + @Id + @GeneratedValue + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/querycache/QueryCacheNullValueTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/querycache/QueryCacheNullValueTest.java new file mode 100644 index 000000000000..a20b3e3a12b7 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/querycache/QueryCacheNullValueTest.java @@ -0,0 +1,77 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.querycache; + +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.stat.spi.StatisticsImplementor; + +import org.hibernate.testing.orm.domain.gambit.BasicEntity; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Marco Belladelli + */ +@DomainModel( annotatedClasses = { + BasicEntity.class +} ) +@SessionFactory( generateStatistics = true ) +@ServiceRegistry( settings = { + @Setting( name = AvailableSettings.USE_QUERY_CACHE, value = "true" ), + @Setting( name = AvailableSettings.USE_SECOND_LEVEL_CACHE, value = "true" ) +} ) +@Jira( "https://hibernate.atlassian.net/browse/HHH-18439" ) +public class QueryCacheNullValueTest { + @Test + public void testNullProperty(SessionFactoryScope scope) { + executeQuery( scope, "select data from BasicEntity" ); + } + + @Test + public void testNullLiteral(SessionFactoryScope scope) { + executeQuery( scope, "select null from BasicEntity" ); + } + + private static void executeQuery(SessionFactoryScope scope, String hql) { + scope.getSessionFactory().getCache().evictQueryRegions(); + final StatisticsImplementor statistics = scope.getSessionFactory().getStatistics(); + statistics.clear(); + + for ( int i = 0; i < 2; i++ ) { + final int hitCount = i; + scope.inTransaction( session -> { + assertThat( session.createQuery( hql, String.class ) + .setCacheable( true ) + .getSingleResult() ).isNull(); + // 0 hits, 1 miss, 1 put + assertThat( statistics.getQueryCacheHitCount() ).isEqualTo( hitCount ); + assertThat( statistics.getQueryCacheMissCount() ).isEqualTo( 1 ); + assertThat( statistics.getQueryCachePutCount() ).isEqualTo( 1 ); + session.clear(); + } ); + } + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> session.persist( new BasicEntity( 1, null ) ) ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/schemaupdate/SchemaMigratorHaltOnErrorTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/schemaupdate/SchemaMigratorHaltOnErrorTest.java index 5607fe94d5f6..1338da0ed814 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/schemaupdate/SchemaMigratorHaltOnErrorTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/schemaupdate/SchemaMigratorHaltOnErrorTest.java @@ -17,6 +17,7 @@ import org.hibernate.cfg.AvailableSettings; import org.hibernate.community.dialect.AltibaseDialect; import org.hibernate.community.dialect.FirebirdDialect; +import org.hibernate.community.dialect.InformixDialect; import org.hibernate.dialect.DB2Dialect; import org.hibernate.dialect.DerbyDialect; import org.hibernate.orm.test.jpa.BaseEntityManagerFunctionalTestCase; @@ -31,6 +32,7 @@ @SkipForDialect(value = DerbyDialect.class, comment = "Derby is far more resistant to the reserved keyword usage.") @SkipForDialect(value = FirebirdDialect.class, comment = "FirebirdDialect has autoQuoteKeywords enabled, so it is far more resistant to the reserved keyword usage.") @SkipForDialect(value = AltibaseDialect.class, comment = "AltibaseDialect has autoQuoteKeywords enabled, so it is far more resistant to the reserved keyword usage.") +@SkipForDialect(value = InformixDialect.class, comment = "Informix is far more resistant to the reserved keyword usage.") public class SchemaMigratorHaltOnErrorTest extends BaseEntityManagerFunctionalTestCase { @Override diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/schemaupdate/SchemaUpdateHaltOnErrorTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/schemaupdate/SchemaUpdateHaltOnErrorTest.java index 0c5183f120db..3552251c96ed 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/schemaupdate/SchemaUpdateHaltOnErrorTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/schemaupdate/SchemaUpdateHaltOnErrorTest.java @@ -22,6 +22,7 @@ import org.hibernate.boot.spi.MetadataImplementor; import org.hibernate.community.dialect.AltibaseDialect; import org.hibernate.community.dialect.FirebirdDialect; +import org.hibernate.community.dialect.InformixDialect; import org.hibernate.dialect.DB2Dialect; import org.hibernate.testing.SkipForDialect; import org.hibernate.testing.junit4.CustomRunner; @@ -44,6 +45,7 @@ @SkipForDialect(value = DerbyDialect.class, comment = "Derby is far more resistant to the reserved keyword usage.") @SkipForDialect(value = FirebirdDialect.class, comment = "FirebirdDialect has autoQuoteKeywords enabled, so it is far more resistant to the reserved keyword usage.") @SkipForDialect(value = AltibaseDialect.class, comment = "AltibaseDialect has autoQuoteKeywords enabled, so it is far more resistant to the reserved keyword usage.") +@SkipForDialect(value = InformixDialect.class, comment = "Informix is far more resistant to the reserved keyword usage.") @RunWith(CustomRunner.class) public class SchemaUpdateHaltOnErrorTest { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/secondarytable/HHH18813Test.java b/hibernate-core/src/test/java/org/hibernate/orm/test/secondarytable/HHH18813Test.java new file mode 100644 index 000000000000..35490c1d43e5 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/secondarytable/HHH18813Test.java @@ -0,0 +1,122 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.secondarytable; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; +import jakarta.persistence.SecondaryTable; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + + +/** + * Test for a Bugfix described in HHH-18813. + * The CteUpdateHandler generated an Insert-Query, + * which contained columns that do not exist in the target table. + * + * @author Peter Bambazek + */ +@JiraKey(value = "HHH-18813") +@DomainModel( + annotatedClasses = {HHH18813Test.SecondaryTableEntitySub.class, HHH18813Test.SecondaryTableEntityBase.class}) +@SessionFactory +class HHH18813Test { + + @Test + void hhh18813Test(SessionFactoryScope scope) { + + // prepare + scope.inTransaction( session -> { + SecondaryTableEntitySub entitySub = new SecondaryTableEntitySub(); + entitySub.setB( 111L ); + entitySub.setC( 222L ); + session.persist( entitySub ); + } ); + + // asset before + scope.inTransaction( session -> { + SecondaryTableEntitySub entitySub = session.createQuery( + "select s from SecondaryTableEntitySub s", + SecondaryTableEntitySub.class ).getSingleResult(); + assertNotNull( entitySub ); + assertEquals( 111L, entitySub.getB() ); + assertEquals( 222L, entitySub.getC() ); + } ); + + // update + scope.inTransaction( session -> { + session.createMutationQuery( "update SecondaryTableEntitySub e set e.b=:b, e.c=:c" ) + .setParameter( "b", 333L ) + .setParameter( "c", 444L ) + .executeUpdate(); + } ); + + // asset after + scope.inTransaction( session -> { + SecondaryTableEntitySub entitySub = session.createQuery( "select s from SecondaryTableEntitySub s", + SecondaryTableEntitySub.class ).getSingleResult(); + assertNotNull( entitySub ); + assertEquals( 333L, entitySub.getB() ); + assertEquals( 444L, entitySub.getC() ); + } ); + } + + @Entity(name = "SecondaryTableEntitySub") + @Inheritance(strategy = InheritanceType.JOINED) + @SecondaryTable(name = "test") + public static class SecondaryTableEntitySub extends SecondaryTableEntityBase { + + @Column + private Long b; + + @Column(table = "test") + private Long c; + + public Long getB() { + return b; + } + + public void setB(Long b) { + this.b = b; + } + + public Long getC() { + return c; + } + + public void setC(Long c) { + this.c = c; + } + } + + @Entity + @Inheritance(strategy = InheritanceType.JOINED) + public static class SecondaryTableEntityBase { + + @Id + @GeneratedValue + private Long id; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/ToOneTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/ToOneTests.java index a2367e71ffad..280c81a6cbb6 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/ToOneTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/ToOneTests.java @@ -111,6 +111,23 @@ void basicSelectedTest(SessionFactoryScope scope) { } ); } + @Test + void fkAccessTest(SessionFactoryScope scope) { + final SQLStatementInspector sqlInspector = scope.getCollectingStatementInspector(); + sqlInspector.clear(); + + scope.inTransaction( (session) -> { + final Integer issue2Reporter = session.createQuery( "select i.reporter.id from Issue i where i.id = 2", Integer.class ).getSingleResultOrNull(); + assertThat( issue2Reporter ).isNull(); + + assertThat( sqlInspector.getSqlQueries() ).hasSize( 1 ); + assertThat( sqlInspector.getSqlQueries().get( 0 ) ).contains( " join " ); + assertThat( sqlInspector.getSqlQueries().get( 0 ) ).contains( ".reporter_fk" ); + assertThat( sqlInspector.getSqlQueries().get( 0 ) ).containsAnyOf( ".active='Y'", ".active=N'Y'" ); + assertThat( sqlInspector.getSqlQueries().get( 0 ) ).containsOnlyOnce( "active" ); + } ); + } + @Entity(name="Issue") @Table(name="issues") public static class Issue { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/TemplateTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/TemplateTest.java index 87a0cb1a7de1..96c3f0253e72 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/TemplateTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/TemplateTest.java @@ -7,13 +7,9 @@ package org.hibernate.orm.test.sql; -import org.hibernate.dialect.Dialect; import org.hibernate.engine.spi.SessionFactoryImplementor; -import org.hibernate.query.sqm.function.SqmFunctionRegistry; import org.hibernate.sql.Template; -import org.hibernate.type.spi.TypeConfiguration; -import org.hibernate.testing.orm.domain.StandardDomainModel; import org.hibernate.testing.orm.junit.DomainModel; import org.hibernate.testing.orm.junit.JiraKey; import org.hibernate.testing.orm.junit.SessionFactory; @@ -23,23 +19,33 @@ import static org.junit.jupiter.api.Assertions.assertEquals; @SessionFactory -@DomainModel(standardModels = StandardDomainModel.GAMBIT) +@DomainModel public class TemplateTest { @Test @JiraKey("HHH-18256") public void templateLiterals(SessionFactoryScope scope) { - assertWhereStringTemplate( "N'a'", scope.getSessionFactory() ); - assertWhereStringTemplate( "X'a'", scope.getSessionFactory() ); - assertWhereStringTemplate( "BX'a'", scope.getSessionFactory() ); - assertWhereStringTemplate( "VARBYTE'a'", scope.getSessionFactory() ); - assertWhereStringTemplate( "bytea 'a'", scope.getSessionFactory() ); - assertWhereStringTemplate( "bytea 'a'", scope.getSessionFactory() ); - assertWhereStringTemplate( "date 'a'", scope.getSessionFactory() ); - assertWhereStringTemplate( "time 'a'", scope.getSessionFactory() ); - assertWhereStringTemplate( "timestamp 'a'", scope.getSessionFactory() ); - assertWhereStringTemplate( "timestamp with time zone 'a'", scope.getSessionFactory() ); - assertWhereStringTemplate( "time with time zone 'a'", scope.getSessionFactory() ); + SessionFactoryImplementor factory = scope.getSessionFactory(); + assertWhereStringTemplate( "N'a'", factory ); + assertWhereStringTemplate( "X'a'", factory ); + assertWhereStringTemplate( "BX'a'", factory); + assertWhereStringTemplate( "VARBYTE'a'", factory ); + assertWhereStringTemplate( "bytea 'a'", factory ); + assertWhereStringTemplate( "bytea 'a'", factory ); + assertWhereStringTemplate( "date 'a'", factory ); + assertWhereStringTemplate( "time 'a'", factory ); + assertWhereStringTemplate( "timestamp 'a'", factory ); + assertWhereStringTemplate( "timestamp with time zone 'a'", factory ); + assertWhereStringTemplate( "time with time zone 'a'", factory ); + assertWhereStringTemplate( "date", "$PlaceHolder$.date", factory ); + assertWhereStringTemplate( "time", "$PlaceHolder$.time", factory ); + assertWhereStringTemplate( "zone", "$PlaceHolder$.zone", factory ); + assertWhereStringTemplate("select date from thetable", + "select $PlaceHolder$.date from thetable", factory ); + assertWhereStringTemplate("select date '2000-12-1' from thetable", + "select date '2000-12-1' from thetable", factory ); + assertWhereStringTemplate("where date between date '2000-12-1' and date '2002-12-2'", + "where $PlaceHolder$.date between date '2000-12-1' and date '2002-12-2'", factory ); } private static void assertWhereStringTemplate(String sql, SessionFactoryImplementor sf) { @@ -52,4 +58,14 @@ private static void assertWhereStringTemplate(String sql, SessionFactoryImplemen assertEquals( sql, template ); } + private static void assertWhereStringTemplate(String sql, String result, SessionFactoryImplementor sf) { + final String template = Template.renderWhereStringTemplate( + sql, + sf.getJdbcServices().getDialect(), + sf.getTypeConfiguration(), + sf.getQueryEngine().getSqmFunctionRegistry() + ); + assertEquals( result, template ); + } + } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/temporal/FractionalSecondsTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/temporal/FractionalSecondsTests.java index 7fe9ae3ff946..3bb0bd086e6b 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/temporal/FractionalSecondsTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/temporal/FractionalSecondsTests.java @@ -16,6 +16,7 @@ import org.hibernate.annotations.FractionalSeconds; import org.hibernate.boot.spi.MetadataImplementor; import org.hibernate.community.dialect.AltibaseDialect; +import org.hibernate.community.dialect.InformixDialect; import org.hibernate.dialect.CockroachDialect; import org.hibernate.dialect.DerbyDialect; import org.hibernate.dialect.Dialect; @@ -121,6 +122,7 @@ void testUsage(SessionFactoryScope scope) { @SkipForDialect(dialectClass = HANADialect.class, reason = "HANA does not support specifying a precision on timestamps") @SkipForDialect(dialectClass = SybaseDialect.class, reason = "Because... Sybase...", matchSubTypes = true) @SkipForDialect(dialectClass = AltibaseDialect.class, reason = "Altibase does not support specifying a precision on timestamps") + @SkipForDialect(dialectClass = InformixDialect.class, reason = "Informix only supports precision from 1 to 5") void testUsage0(SessionFactoryScope scope) { final Instant start = Instant.now(); @@ -172,6 +174,7 @@ void testUsage3(SessionFactoryScope scope) { @SkipForDialect(dialectClass = PostgreSQLDialect.class, reason = "PostgreSQL only supports precision <= 6", matchSubTypes = true) @SkipForDialect(dialectClass = CockroachDialect.class, reason = "CockroachDB only supports precision <= 6") @SkipForDialect(dialectClass = HANADialect.class, reason = "HANA does not support specifying a precision on timestamps") + @SkipForDialect(dialectClass = InformixDialect.class, reason = "Informix only supports precision from 1 to 5") void testUsage9(SessionFactoryScope scope) { final Instant start = Instant.now(); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/temporal/JavaTimeFractionalSecondsTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/temporal/JavaTimeFractionalSecondsTests.java index b84fbda1ea41..0e40a61fc1ed 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/temporal/JavaTimeFractionalSecondsTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/temporal/JavaTimeFractionalSecondsTests.java @@ -17,6 +17,7 @@ import org.hibernate.annotations.JdbcTypeCode; import org.hibernate.boot.spi.MetadataImplementor; import org.hibernate.community.dialect.AltibaseDialect; +import org.hibernate.community.dialect.InformixDialect; import org.hibernate.dialect.CockroachDialect; import org.hibernate.dialect.DerbyDialect; import org.hibernate.dialect.Dialect; @@ -123,6 +124,7 @@ void testUsage(SessionFactoryScope scope) { @SkipForDialect(dialectClass = HANADialect.class, reason = "HANA does not support specifying a precision on timestamps") @SkipForDialect(dialectClass = SybaseDialect.class, reason = "Because... Sybase...", matchSubTypes = true) @SkipForDialect(dialectClass = AltibaseDialect.class, reason = "Altibase does not support specifying a precision on timestamps") + @SkipForDialect(dialectClass = InformixDialect.class, reason = "Informix only supports precision from 1 to 5") void testUsage0(SessionFactoryScope scope) { final Instant start = Instant.now(); @@ -174,6 +176,7 @@ void testUsage3(SessionFactoryScope scope) { @SkipForDialect(dialectClass = PostgreSQLDialect.class, reason = "PostgreSQL only supports precision <= 6", matchSubTypes = true) @SkipForDialect(dialectClass = CockroachDialect.class, reason = "CockroachDB only supports precision <= 6") @SkipForDialect(dialectClass = HANADialect.class, reason = "HANA does not support specifying a precision on timestamps") + @SkipForDialect(dialectClass = InformixDialect.class, reason = "Informix only supports precision from 1 to 5") void testUsage9(SessionFactoryScope scope) { final Instant start = Instant.now(); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/tenantid/TenantIdToOneBidirectionalTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/tenantid/TenantIdToOneBidirectionalTest.java new file mode 100644 index 000000000000..b48881eef31c --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/tenantid/TenantIdToOneBidirectionalTest.java @@ -0,0 +1,165 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.tenantid; + +import jakarta.persistence.Column; +import org.hibernate.annotations.TenantId; +import org.hibernate.boot.SessionFactoryBuilder; +import org.hibernate.boot.spi.MetadataImplementor; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.context.spi.CurrentTenantIdentifierResolver; +import org.hibernate.engine.spi.SessionFactoryImplementor; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryProducer; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Marco Belladelli + */ +@DomainModel( annotatedClasses = { + TenantIdToOneBidirectionalTest.RootEntity.class, + TenantIdToOneBidirectionalTest.ChildEntity.class, +} ) +@SessionFactory +@ServiceRegistry( settings = @Setting( name = AvailableSettings.HBM2DDL_AUTO, value = "create-drop" ) ) +@Jira( "https://hibernate.atlassian.net/browse/HHH-18617" ) +public class TenantIdToOneBidirectionalTest implements SessionFactoryProducer { + private static String currentTenant; + + @Test + public void testExistingRoot(SessionFactoryScope scope) { + currentTenant = "tenant_1"; + scope.inTransaction( session -> { + final var child = session.find( ChildEntity.class, 1L ); + assertThat( child.getRoot() ).isNotNull().extracting( RootEntity::getChild ).isSameAs( child ); + } ); + } + + @Test + public void testRemovedRoot(SessionFactoryScope scope) { + currentTenant = "tenant_2"; + scope.inTransaction( session -> { + final var child = session.find( ChildEntity.class, 2L ); + assertThat( child.getRoot() ).isNull(); + } ); + } + + @Test + public void testNoRoot(SessionFactoryScope scope) { + currentTenant = "tenant_3"; + scope.inTransaction( session -> { + final var child = session.find( ChildEntity.class, 3L ); + assertThat( child.getRoot() ).isNull(); + } ); + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + currentTenant = "tenant_1"; + scope.inTransaction( session -> { + final var withChild = new RootEntity( 1L ); + withChild.setChild( new ChildEntity( 1L ) ); + session.persist( withChild ); + } ); + currentTenant = "tenant_2"; + scope.inTransaction( session -> { + final var deletedRoot = new RootEntity( 2L ); + final var child = new ChildEntity( 2L ); + deletedRoot.setChild( child ); + session.persist( deletedRoot ); + + session.flush(); + session.clear(); + session.remove( deletedRoot ); + } ); + currentTenant = "tenant_3"; + scope.inTransaction( session -> session.persist( new ChildEntity( 3L ) ) ); + } + + @Override + public SessionFactoryImplementor produceSessionFactory(MetadataImplementor model) { + final SessionFactoryBuilder sfb = model.getSessionFactoryBuilder(); + sfb.applyCurrentTenantIdentifierResolver( new CurrentTenantIdentifierResolver() { + @Override + public String resolveCurrentTenantIdentifier() { + return currentTenant; + } + + @Override + public boolean validateExistingCurrentSessions() { + return false; + } + } ); + return (SessionFactoryImplementor) sfb.build(); + } + + @Entity( name = "RootEntity" ) + static class RootEntity { + @Id + private Long id; + + @TenantId + @Column(name="tenant_col") + private String tenant; + + @OneToOne( cascade = CascadeType.PERSIST ) + @JoinColumn + private ChildEntity child; + + public RootEntity() { + } + + public RootEntity(Long id) { + this.id = id; + } + + public ChildEntity getChild() { + return child; + } + + public void setChild(ChildEntity child) { + this.child = child; + } + } + + @Entity( name = "ChildEntity" ) + static class ChildEntity { + @Id + private Long id; + + @TenantId + @Column(name="tenant_col") + private String tenant; + + @OneToOne( mappedBy = "child" ) + private RootEntity root; + + public ChildEntity() { + } + + public ChildEntity(Long id) { + this.id = id; + } + + public RootEntity getRoot() { + return root; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/AutoZonedTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/AutoZonedTest.java index 421fd7deb3b5..9a88ffee9321 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/AutoZonedTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/AutoZonedTest.java @@ -54,12 +54,12 @@ public class AutoZonedTest { Zoned z = s.find(Zoned.class, id); final Dialect dialect = scope.getSessionFactory().getJdbcServices().getDialect(); assertEquals( - DateTimeUtils.roundToDefaultPrecision( nowZoned.toInstant(), dialect ), - DateTimeUtils.roundToDefaultPrecision( z.zonedDateTime.toInstant(), dialect ) + DateTimeUtils.adjustToDefaultPrecision( nowZoned.toInstant(), dialect ), + DateTimeUtils.adjustToDefaultPrecision( z.zonedDateTime.toInstant(), dialect ) ); assertEquals( - DateTimeUtils.roundToDefaultPrecision( nowOffset.toInstant(), dialect ), - DateTimeUtils.roundToDefaultPrecision( z.offsetDateTime.toInstant(), dialect ) + DateTimeUtils.adjustToDefaultPrecision( nowOffset.toInstant(), dialect ), + DateTimeUtils.adjustToDefaultPrecision( z.offsetDateTime.toInstant(), dialect ) ); assertEquals( nowZoned.toOffsetDateTime().getOffset(), z.zonedDateTime.toOffsetDateTime().getOffset() ); assertEquals( nowOffset.getOffset(), z.offsetDateTime.getOffset() ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/ColumnZonedTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/ColumnZonedTest.java index dbbcb3a62eca..245b6f2d997f 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/ColumnZonedTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/ColumnZonedTest.java @@ -54,12 +54,12 @@ public class ColumnZonedTest { Zoned z = s.find(Zoned.class, id); final Dialect dialect = scope.getSessionFactory().getJdbcServices().getDialect(); assertEquals( - DateTimeUtils.roundToDefaultPrecision( nowZoned.toInstant(), dialect ), - DateTimeUtils.roundToDefaultPrecision( z.zonedDateTime.toInstant(), dialect ) + DateTimeUtils.adjustToDefaultPrecision( nowZoned.toInstant(), dialect ), + DateTimeUtils.adjustToDefaultPrecision( z.zonedDateTime.toInstant(), dialect ) ); assertEquals( - DateTimeUtils.roundToDefaultPrecision( nowOffset.toInstant(), dialect ), - DateTimeUtils.roundToDefaultPrecision( z.offsetDateTime.toInstant(), dialect ) + DateTimeUtils.adjustToDefaultPrecision( nowOffset.toInstant(), dialect ), + DateTimeUtils.adjustToDefaultPrecision( z.offsetDateTime.toInstant(), dialect ) ); assertEquals( nowZoned.toOffsetDateTime().getOffset(), z.zonedDateTime.toOffsetDateTime().getOffset() ); assertEquals( nowOffset.getOffset(), z.offsetDateTime.getOffset() ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/DefaultZonedTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/DefaultZonedTest.java index 0bb8a1f3b220..37361b7353d1 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/DefaultZonedTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/DefaultZonedTest.java @@ -51,12 +51,12 @@ public class DefaultZonedTest { Zoned z = s.find(Zoned.class, id); final Dialect dialect = scope.getSessionFactory().getJdbcServices().getDialect(); assertEquals( - DateTimeUtils.roundToDefaultPrecision( nowZoned.toInstant(), dialect ), - DateTimeUtils.roundToDefaultPrecision( z.zonedDateTime.toInstant(), dialect ) + DateTimeUtils.adjustToDefaultPrecision( nowZoned.toInstant(), dialect ), + DateTimeUtils.adjustToDefaultPrecision( z.zonedDateTime.toInstant(), dialect ) ); assertEquals( - DateTimeUtils.roundToDefaultPrecision( nowOffset.toInstant(), dialect ), - DateTimeUtils.roundToDefaultPrecision( z.offsetDateTime.toInstant(), dialect ) + DateTimeUtils.adjustToDefaultPrecision( nowOffset.toInstant(), dialect ), + DateTimeUtils.adjustToDefaultPrecision( z.offsetDateTime.toInstant(), dialect ) ); if ( dialect.getTimeZoneSupport() == TimeZoneSupport.NATIVE ) { assertEquals( nowZoned.toOffsetDateTime().getOffset(), z.zonedDateTime.toOffsetDateTime().getOffset() ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/JDBCTimeZoneZonedTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/JDBCTimeZoneZonedTest.java index f77bc9ced6a0..aeb27acc9e11 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/JDBCTimeZoneZonedTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/JDBCTimeZoneZonedTest.java @@ -58,12 +58,12 @@ public class JDBCTimeZoneZonedTest { ZoneOffset systemOffset = systemZone.getRules().getOffset( Instant.now() ); final Dialect dialect = scope.getSessionFactory().getJdbcServices().getDialect(); assertEquals( - DateTimeUtils.roundToDefaultPrecision( nowZoned.toInstant(), dialect ), - DateTimeUtils.roundToDefaultPrecision( z.zonedDateTime.toInstant(), dialect ) + DateTimeUtils.adjustToDefaultPrecision( nowZoned.toInstant(), dialect ), + DateTimeUtils.adjustToDefaultPrecision( z.zonedDateTime.toInstant(), dialect ) ); assertEquals( - DateTimeUtils.roundToDefaultPrecision( nowOffset.toInstant(), dialect ), - DateTimeUtils.roundToDefaultPrecision( z.offsetDateTime.toInstant(), dialect ) + DateTimeUtils.adjustToDefaultPrecision( nowOffset.toInstant(), dialect ), + DateTimeUtils.adjustToDefaultPrecision( z.offsetDateTime.toInstant(), dialect ) ); assertEquals( systemZone, z.zonedDateTime.getZone() ); assertEquals( systemOffset, z.offsetDateTime.getOffset() ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/PassThruZonedTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/PassThruZonedTest.java index 8404eb25e475..440e066af4b8 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/PassThruZonedTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/PassThruZonedTest.java @@ -57,12 +57,12 @@ public class PassThruZonedTest { ZoneOffset systemOffset = systemZone.getRules().getOffset( Instant.now() ); final Dialect dialect = scope.getSessionFactory().getJdbcServices().getDialect(); assertEquals( - DateTimeUtils.roundToDefaultPrecision( nowZoned.toInstant(), dialect ), - DateTimeUtils.roundToDefaultPrecision( z.zonedDateTime.toInstant(), dialect ) + DateTimeUtils.adjustToDefaultPrecision( nowZoned.toInstant(), dialect ), + DateTimeUtils.adjustToDefaultPrecision( z.zonedDateTime.toInstant(), dialect ) ); assertEquals( - DateTimeUtils.roundToDefaultPrecision( nowOffset.toInstant(), dialect ), - DateTimeUtils.roundToDefaultPrecision( z.offsetDateTime.toInstant(), dialect ) + DateTimeUtils.adjustToDefaultPrecision( nowOffset.toInstant(), dialect ), + DateTimeUtils.adjustToDefaultPrecision( z.offsetDateTime.toInstant(), dialect ) ); assertEquals( systemZone, z.zonedDateTime.getZone() ); assertEquals( systemOffset, z.offsetDateTime.getOffset() ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/UTCNormalizedInstantTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/UTCNormalizedInstantTest.java index dae2242aa801..fb6000ad13b3 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/UTCNormalizedInstantTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/UTCNormalizedInstantTest.java @@ -46,12 +46,12 @@ public class UTCNormalizedInstantTest { final Zoned z = s.find(Zoned.class, id); final Dialect dialect = scope.getSessionFactory().getJdbcServices().getDialect(); assertEquals( - DateTimeUtils.roundToDefaultPrecision( z.utcInstant, dialect ), - DateTimeUtils.roundToDefaultPrecision( instant, dialect ) + DateTimeUtils.adjustToDefaultPrecision( z.utcInstant, dialect ), + DateTimeUtils.adjustToDefaultPrecision( instant, dialect ) ); assertEquals( - DateTimeUtils.roundToDefaultPrecision( z.localInstant, dialect ), - DateTimeUtils.roundToDefaultPrecision( instant, dialect ) + DateTimeUtils.adjustToDefaultPrecision( z.localInstant, dialect ), + DateTimeUtils.adjustToDefaultPrecision( instant, dialect ) ); }); } @@ -80,12 +80,12 @@ public class UTCNormalizedInstantTest { final Zoned z = s.find(Zoned.class, id); final Dialect dialect = scope.getSessionFactory().getJdbcServices().getDialect(); assertEquals( - DateTimeUtils.roundToDefaultPrecision( z.utcInstant, dialect ), - DateTimeUtils.roundToDefaultPrecision( instant, dialect ) + DateTimeUtils.adjustToDefaultPrecision( z.utcInstant, dialect ), + DateTimeUtils.adjustToDefaultPrecision( instant, dialect ) ); assertEquals( - DateTimeUtils.roundToDefaultPrecision( z.localInstant, dialect ), - DateTimeUtils.roundToDefaultPrecision( instant, dialect ) + DateTimeUtils.adjustToDefaultPrecision( z.localInstant, dialect ), + DateTimeUtils.adjustToDefaultPrecision( instant, dialect ) ); }); } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/UTCNormalizedZonedTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/UTCNormalizedZonedTest.java index cbab481e0f1d..92c3865749ea 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/UTCNormalizedZonedTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/UTCNormalizedZonedTest.java @@ -54,12 +54,12 @@ public class UTCNormalizedZonedTest { Zoned z = s.find(Zoned.class, id); final Dialect dialect = scope.getSessionFactory().getJdbcServices().getDialect(); assertEquals( - DateTimeUtils.roundToDefaultPrecision( nowZoned.toInstant(), dialect ), - DateTimeUtils.roundToDefaultPrecision( z.zonedDateTime.toInstant(), dialect ) + DateTimeUtils.adjustToDefaultPrecision( nowZoned.toInstant(), dialect ), + DateTimeUtils.adjustToDefaultPrecision( z.zonedDateTime.toInstant(), dialect ) ); assertEquals( - DateTimeUtils.roundToDefaultPrecision( nowOffset.toInstant(), dialect ), - DateTimeUtils.roundToDefaultPrecision( z.offsetDateTime.toInstant(), dialect ) + DateTimeUtils.adjustToDefaultPrecision( nowOffset.toInstant(), dialect ), + DateTimeUtils.adjustToDefaultPrecision( z.offsetDateTime.toInstant(), dialect ) ); assertEquals( ZoneId.of("Z"), z.zonedDateTime.getZone() ); assertEquals( ZoneOffset.ofHours(0), z.offsetDateTime.getOffset() ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/tool/schema/DefaultImportFileExecutionTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/tool/schema/DefaultImportFileExecutionTest.java index ed486db44300..dcf17761f81f 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/tool/schema/DefaultImportFileExecutionTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/tool/schema/DefaultImportFileExecutionTest.java @@ -3,8 +3,12 @@ import java.io.File; import java.io.FileWriter; import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URI; import java.net.URL; +import java.net.URLClassLoader; import java.nio.file.Files; +import java.nio.file.Path; import java.util.ArrayList; import java.util.EnumSet; import java.util.HashMap; @@ -13,9 +17,10 @@ import org.hibernate.boot.Metadata; import org.hibernate.boot.MetadataSources; +import org.hibernate.boot.registry.BootstrapServiceRegistryBuilder; import org.hibernate.boot.registry.StandardServiceRegistry; -import org.hibernate.boot.registry.StandardServiceRegistryBuilder; import org.hibernate.boot.registry.classloading.spi.ClassLoaderService; +import org.hibernate.bytecode.enhance.spi.Enhancer; import org.hibernate.cfg.AvailableSettings; import org.hibernate.tool.schema.SourceType; import org.hibernate.tool.schema.TargetType; @@ -30,6 +35,7 @@ import org.hibernate.tool.schema.spi.SourceDescriptor; import org.hibernate.tool.schema.spi.TargetDescriptor; +import org.hibernate.testing.orm.junit.Jira; import org.hibernate.testing.util.ServiceRegistryUtil; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -42,8 +48,8 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.hibernate.tool.schema.internal.SchemaCreatorImpl.DEFAULT_IMPORT_FILE; +@Jira( "https://hibernate.atlassian.net/browse/HHH-15717" ) public class DefaultImportFileExecutionTest { - private File defaultImportFile; private StandardServiceRegistry serviceRegistry; private static final String COMMAND = "INSERT INTO TEST_ENTITY (id, name) values (1,'name')"; @@ -52,7 +58,9 @@ public class DefaultImportFileExecutionTest { @BeforeEach public void setUp() throws Exception { defaultImportFile = createDefaultImportFile( "import.sql" ); - serviceRegistry = ServiceRegistryUtil.serviceRegistry(); + serviceRegistry = ServiceRegistryUtil.serviceRegistryBuilder( + new BootstrapServiceRegistryBuilder().applyClassLoader( toClassLoader( defaultImportFile.getParentFile() ) ).build() + ).build(); } @AfterEach @@ -99,11 +107,9 @@ private void createSchema(TargetDescriptorImpl targetDescriptor) { ); } - private static File createDefaultImportFile(String fileName) throws Exception { - URL myUrl = Thread.currentThread().getContextClassLoader().getResource( "hibernate.properties" ); - String path = myUrl.getPath().replace( "hibernate.properties", fileName ); - final File file = new File( path ); - file.createNewFile(); + private static File createDefaultImportFile(@SuppressWarnings( "SameParameterValue" ) String fileName) throws Exception { + final Path tmp = Files.createTempDirectory( "default_import" ); + final File file = new File( tmp.toString() + File.separator + fileName ); try (final FileWriter myWriter = new FileWriter( file )) { myWriter.write( COMMAND ); @@ -112,6 +118,17 @@ private static File createDefaultImportFile(String fileName) throws Exception { return file; } + private static ClassLoader toClassLoader(File classesDir) { + final URI classesDirUri = classesDir.toURI(); + try { + final URL url = classesDirUri.toURL(); + return new URLClassLoader( new URL[] { url }, Enhancer.class.getClassLoader() ); + } + catch (MalformedURLException e) { + throw new RuntimeException( "Unable to resolve classpath entry to URL : " + classesDir.getAbsolutePath(), e ); + } + } + private Metadata buildMappings(StandardServiceRegistry registry) { return new MetadataSources( registry ) .addAnnotatedClass( TestEntity.class ) diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/tool/schema/SkipDefaultImportFileExecutionTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/tool/schema/SkipDefaultImportFileExecutionTest.java new file mode 100644 index 000000000000..ba384e57e62d --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/tool/schema/SkipDefaultImportFileExecutionTest.java @@ -0,0 +1,243 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.tool.schema; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.hibernate.boot.Metadata; +import org.hibernate.boot.MetadataSources; +import org.hibernate.boot.registry.BootstrapServiceRegistryBuilder; +import org.hibernate.boot.registry.StandardServiceRegistry; +import org.hibernate.boot.registry.classloading.spi.ClassLoaderService; +import org.hibernate.bytecode.enhance.spi.Enhancer; +import org.hibernate.testing.util.ServiceRegistryUtil; +import org.hibernate.tool.schema.SourceType; +import org.hibernate.tool.schema.TargetType; +import org.hibernate.tool.schema.internal.SchemaCreatorImpl; +import org.hibernate.tool.schema.spi.CommandAcceptanceException; +import org.hibernate.tool.schema.spi.ContributableMatcher; +import org.hibernate.tool.schema.spi.ExceptionHandler; +import org.hibernate.tool.schema.spi.ExecutionOptions; +import org.hibernate.tool.schema.spi.ScriptSourceInput; +import org.hibernate.tool.schema.spi.ScriptTargetOutput; +import org.hibernate.tool.schema.spi.SourceDescriptor; +import org.hibernate.tool.schema.spi.TargetDescriptor; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.hibernate.cfg.SchemaToolingSettings.HBM2DDL_SKIP_DEFAULT_IMPORT_FILE; +import static org.hibernate.tool.schema.internal.SchemaCreatorImpl.DEFAULT_IMPORT_FILE; + +public class SkipDefaultImportFileExecutionTest { + private File defaultImportFile; + private StandardServiceRegistry serviceRegistry; + private static final String COMMAND = "INSERT INTO TEST_ENTITY (id, name) values (1,'name')"; + + + @BeforeEach + public void setUp() throws Exception { + defaultImportFile = createDefaultImportFile( "import.sql" ); + serviceRegistry = ServiceRegistryUtil.serviceRegistryBuilder( + new BootstrapServiceRegistryBuilder().applyClassLoader( + toClassLoader( defaultImportFile.getParentFile() ) ).build() + ).build(); + } + + @AfterEach + public void tearDown() { + serviceRegistry.close(); + if ( defaultImportFile.exists() ) { + try { + Files.delete( defaultImportFile.toPath() ); + } + catch (IOException e) { + throw new RuntimeException( e ); + } + } + } + + @Test + public void testImportScriptIsNotExecutedWhitSkipDefaultImportFileSetting() { + assertThat( serviceRegistry.getService( ClassLoaderService.class ) + .locateResource( DEFAULT_IMPORT_FILE ) ).isNotNull(); + + TargetDescriptorImpl targetDescriptor = TargetDescriptorImpl.INSTANCE; + + createSchema( targetDescriptor ); + + TestScriptTargetOutput targetOutput = (TestScriptTargetOutput) targetDescriptor.getScriptTargetOutput(); + + assertNoInsertCommandsFromDeaultImportFileHasBeenExecuted( targetOutput ); + } + + private static void assertNoInsertCommandsFromDeaultImportFileHasBeenExecuted(TestScriptTargetOutput targetOutput) { + assertThat( targetOutput.getInsertCommands().size() ).isEqualTo( 0 ) + .as( "Despite setting hibernate.hbm2ddl.skip_default_import_file to true the default import.sql file insert command hs been executed" ); + } + + private void createSchema(TargetDescriptorImpl targetDescriptor) { + + final Metadata mappings = buildMappings( serviceRegistry ); + + final SchemaCreatorImpl schemaCreator = new SchemaCreatorImpl( serviceRegistry ); + + final Map options = new HashMap<>(); + options.put( HBM2DDL_SKIP_DEFAULT_IMPORT_FILE, "true" ); + schemaCreator.doCreation( + mappings, + new ExecutionOptionsTestImpl( options ), + ContributableMatcher.ALL, + SourceDescriptorImpl.INSTANCE, + targetDescriptor + ); + } + + private static File createDefaultImportFile(@SuppressWarnings("SameParameterValue") String fileName) + throws Exception { + final Path tmp = Files.createTempDirectory( "default_import" ); + final File file = new File( tmp.toString() + File.separator + fileName ); + + try (final FileWriter myWriter = new FileWriter( file )) { + myWriter.write( COMMAND ); + } + + return file; + } + + private static ClassLoader toClassLoader(File classesDir) { + final URI classesDirUri = classesDir.toURI(); + try { + final URL url = classesDirUri.toURL(); + return new URLClassLoader( new URL[] {url}, Enhancer.class.getClassLoader() ); + } + catch (MalformedURLException e) { + throw new RuntimeException( "Unable to resolve classpath entry to URL : " + classesDir.getAbsolutePath(), + e ); + } + } + + private Metadata buildMappings(StandardServiceRegistry registry) { + return new MetadataSources( registry ) + .addAnnotatedClass( TestEntity.class ) + .buildMetadata(); + } + + @Entity(name = "TestEntity") + @Table(name = "TEST_ENTITY") + public static class TestEntity { + @Id + private long id; + + private String name; + } + + public class ExecutionOptionsTestImpl implements ExecutionOptions, ExceptionHandler { + Map configValues; + + public ExecutionOptionsTestImpl(Map configValues) { + this.configValues = configValues; + } + + @Override + public Map getConfigurationValues() { + return configValues; + } + + @Override + public boolean shouldManageNamespaces() { + return true; + } + + @Override + public ExceptionHandler getExceptionHandler() { + return this; + } + + @Override + public void handleException(CommandAcceptanceException exception) { + throw exception; + } + } + + private static class SourceDescriptorImpl implements SourceDescriptor { + /** + * Singleton access + */ + public static final SourceDescriptorImpl INSTANCE = new SourceDescriptorImpl(); + + @Override + public SourceType getSourceType() { + return SourceType.METADATA; + } + + @Override + public ScriptSourceInput getScriptSourceInput() { + return null; + } + } + + private static class TargetDescriptorImpl implements TargetDescriptor { + /** + * Singleton access + */ + public static final TargetDescriptorImpl INSTANCE = new TargetDescriptorImpl(); + + @Override + public EnumSet getTargetTypes() { + return EnumSet.of( TargetType.SCRIPT ); + } + + @Override + public ScriptTargetOutput getScriptTargetOutput() { + return TestScriptTargetOutput.INSTANCE; + } + } + + private static class TestScriptTargetOutput implements ScriptTargetOutput { + + public static final TestScriptTargetOutput INSTANCE = new TestScriptTargetOutput(); + + List insertCommands = new ArrayList<>(); + + @Override + public void prepare() { + + } + + @Override + public void accept(String command) { + if ( command.toLowerCase().startsWith( "insert" ) ) { + insertCommands.add( command ); + } + } + + @Override + public void release() { + + } + + public List getInsertCommands() { + return insertCommands; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/type/UUIDTypeConverterTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/type/UUIDTypeConverterTest.java index 6502be63b0de..ea36dfe72f4d 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/type/UUIDTypeConverterTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/type/UUIDTypeConverterTest.java @@ -11,10 +11,13 @@ import java.util.List; import java.util.UUID; +import org.hibernate.community.dialect.InformixDialect; + import org.hibernate.testing.TestForIssue; import org.hibernate.testing.orm.junit.DomainModel; import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.SkipForDialect; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; @@ -38,6 +41,7 @@ ) @SessionFactory @TestForIssue(jiraKey = "HHH-15417") +@SkipForDialect(dialectClass = InformixDialect.class, reason = "Informix does not support unique / primary constraints on binary columns") public class UUIDTypeConverterTest { @AfterEach diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/type/contributor/usertype/hhh18787/CustomData.java b/hibernate-core/src/test/java/org/hibernate/orm/test/type/contributor/usertype/hhh18787/CustomData.java new file mode 100644 index 000000000000..278a8e3d9201 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/type/contributor/usertype/hhh18787/CustomData.java @@ -0,0 +1,36 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.type.contributor.usertype.hhh18787; + +/** + * Simple object holding some properties + */ +public class CustomData { + private String text; + private Long number; + + public CustomData(String text, Long number) { + this.text = text; + this.number = number; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + public Long getNumber() { + return number; + } + + public void setNumber(Long number) { + this.number = number; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/type/contributor/usertype/hhh18787/CustomDataType.java b/hibernate-core/src/test/java/org/hibernate/orm/test/type/contributor/usertype/hhh18787/CustomDataType.java new file mode 100644 index 000000000000..2fb91707f38a --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/type/contributor/usertype/hhh18787/CustomDataType.java @@ -0,0 +1,98 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.type.contributor.usertype.hhh18787; + +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.type.SqlTypes; +import org.hibernate.usertype.UserType; + +import java.io.Serializable; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.util.Arrays; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Custom type implementing {@link UserType} so CustomData[] can be converted. + */ +public class CustomDataType implements UserType { + + public static final CustomDataType INSTANCE = new CustomDataType(); + + @Override + public int getSqlType() { + return SqlTypes.VARCHAR; + } + + @Override + public Class returnedClass() { + return CustomData[].class; + } + + @Override + public boolean equals(CustomData[] x, CustomData[] y) { + return Arrays.equals(x, y); + } + + @Override + public int hashCode(CustomData[] x) { + return Arrays.hashCode(x); + } + + @Override + public CustomData[] nullSafeGet(ResultSet rs, int position, SharedSessionContractImplementor session, + Object owner) throws SQLException { + + final var customDataStr = rs.getString(position); + return rs.wasNull() ? new CustomData[0] : parseDataFromString(customDataStr); + } + + @Override + public void nullSafeSet(PreparedStatement st, CustomData[] value, int index, + SharedSessionContractImplementor session) throws SQLException { + + if (value == null || value.length == 0) { + st.setNull(index, Types.VARCHAR); + } + else { + final var str = + Stream.of(value).map(u -> String.format("%s|%s", u.getText(), u.getNumber())).collect(Collectors.joining(",")); + + st.setString(index, str); + } + } + + @Override + public CustomData[] deepCopy(CustomData[] value) { + return Arrays.copyOf(value, value.length); + } + + @Override + public boolean isMutable() { + return true; + } + + @Override + public Serializable disassemble(CustomData[] value) { + return deepCopy(value); + } + + @Override + public CustomData[] assemble(Serializable cached, Object owner) { + return deepCopy((CustomData[]) cached); + } + + private CustomData[] parseDataFromString(String value) { + return Arrays.stream(value.split(",")).map(singleValue -> { + final var params = singleValue.split("\\|"); + return new CustomData(params[0], Long.parseLong(params[1])); + }).toArray(CustomData[]::new); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/type/contributor/usertype/hhh18787/SomeEntity.java b/hibernate-core/src/test/java/org/hibernate/orm/test/type/contributor/usertype/hhh18787/SomeEntity.java new file mode 100644 index 000000000000..33d9e006dcc3 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/type/contributor/usertype/hhh18787/SomeEntity.java @@ -0,0 +1,50 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.type.contributor.usertype.hhh18787; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +/** + * Some entity, important is the property customData. + */ +@Entity +@Table(name = "whatever") +public class SomeEntity { + @Id + @GeneratedValue + private Long id; + + @Column + private CustomData[] customData; + + public SomeEntity() { + } + + public SomeEntity(CustomData[] customData) { + this.customData = customData; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public CustomData[] getCustomData() { + return customData; + } + + public void setCustomData(CustomData[] custom) { + this.customData = custom; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/type/contributor/usertype/hhh18787/TypesContributor.java b/hibernate-core/src/test/java/org/hibernate/orm/test/type/contributor/usertype/hhh18787/TypesContributor.java new file mode 100644 index 000000000000..dfbe50e90300 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/type/contributor/usertype/hhh18787/TypesContributor.java @@ -0,0 +1,22 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.type.contributor.usertype.hhh18787; + +import org.hibernate.boot.model.TypeContributions; +import org.hibernate.boot.model.TypeContributor; +import org.hibernate.service.ServiceRegistry; + +/** + * Registering custom user type {@link CustomDataType}. + */ +public class TypesContributor implements TypeContributor { + + @Override + public void contribute(TypeContributions typeContributions, ServiceRegistry serviceRegistry) { + typeContributions.contributeType(CustomDataType.INSTANCE); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/type/contributor/usertype/hhh18787/UserTypeNotRecognisedTestCase.java b/hibernate-core/src/test/java/org/hibernate/orm/test/type/contributor/usertype/hhh18787/UserTypeNotRecognisedTestCase.java new file mode 100644 index 000000000000..364909cbc17a --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/type/contributor/usertype/hhh18787/UserTypeNotRecognisedTestCase.java @@ -0,0 +1,59 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.type.contributor.usertype.hhh18787; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@DomainModel( + annotatedClasses = SomeEntity.class, + typeContributors = TypesContributor.class +) +@SessionFactory +@JiraKey( "HHH-18787" ) +class UserTypeNotRecognisedTestCase { + + @Test + void customUserTypeWithTypeContributorRegistrationTest(SessionFactoryScope scope) { + final var data = new CustomData( "whatever", 1L ); + scope.inTransaction( em -> { + // creating some data, flushing and clearing context + em.merge( new SomeEntity( new CustomData[] {data} ) ); + } ); + + scope.inSession( em -> { + // getting the data + final var query = em.createQuery( "select s from SomeEntity s where id is not null", SomeEntity.class ); + final var resultList = query.getResultList(); + + // single result should be present + assertNotNull( resultList ); + assertEquals( 1, resultList.size() ); + + // the entity shouldn't be null + final var entity = resultList.get( 0 ); + assertNotNull( entity ); + + // custom data array shouldn't be null and there should be single object present + final var customData = entity.getCustomData(); + assertNotNull( customData ); + assertEquals( 1, customData.length ); + + // custom data object shouldn't be null and all fields should be set with correct values + final var singleCustomData = customData[0]; + assertNotNull( singleCustomData ); + assertEquals( data.getText(), singleCustomData.getText() ); + assertEquals( data.getNumber(), singleCustomData.getNumber() ); + } ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/persister/entity/JoinedSubclassEntityPersisterTest.java b/hibernate-core/src/test/java/org/hibernate/persister/entity/JoinedSubclassEntityPersisterTest.java new file mode 100644 index 000000000000..2556fee9bcfc --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/persister/entity/JoinedSubclassEntityPersisterTest.java @@ -0,0 +1,74 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.persister.entity; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.Table; +import org.hibernate.metamodel.MappingMetamodel; +import org.hibernate.metamodel.model.domain.internal.JpaMetamodelImpl; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.Jpa; +import org.junit.jupiter.api.Test; + +import static jakarta.persistence.InheritanceType.JOINED; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * @author Vincent Bouthinon + */ +@Jpa( + annotatedClasses = { + JoinedSubclassEntityPersisterTest.Animal.class, + JoinedSubclassEntityPersisterTest.Dog.class, + } +) +@JiraKey("HHH-18703") +class JoinedSubclassEntityPersisterTest { + + @Test + void the_table_name_must_match_the_attribute_s_column(EntityManagerFactoryScope scope) { + scope.inTransaction( + entityManager -> { + JpaMetamodelImpl metamodel = (JpaMetamodelImpl) entityManager.getMetamodel(); + MappingMetamodel mappingMetamodel = metamodel.getMappingMetamodel(); + AbstractEntityPersister entityDescriptor = (AbstractEntityPersister) mappingMetamodel.getEntityDescriptor( Dog.class ); + String table = entityDescriptor.getTableName( entityDescriptor.determineTableNumberForColumn( "name" ) ); + assertEquals( "TANIMAL", table ); + } + ); + } + + @Entity + @Inheritance(strategy = JOINED) + @Table(name = "TANIMAL") + public static class Animal { + + @Id + @GeneratedValue + public Integer id; + + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + @Entity + @Table(name = "TDOG") + public static class Dog extends Animal { + + } +} diff --git a/hibernate-core/src/test/java17/org/hibernate/orm/test/records/RecordEmbeddedPropertyNamesTest.java b/hibernate-core/src/test/java17/org/hibernate/orm/test/records/RecordEmbeddedPropertyNamesTest.java new file mode 100644 index 000000000000..62dbf9e3b4ec --- /dev/null +++ b/hibernate-core/src/test/java17/org/hibernate/orm/test/records/RecordEmbeddedPropertyNamesTest.java @@ -0,0 +1,152 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.records; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Embeddable; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Marco Belladelli + */ +@DomainModel( annotatedClasses = { + RecordEmbeddedPropertyNamesTest.Fee.class, + RecordEmbeddedPropertyNamesTest.Getaway.class, + RecordEmbeddedPropertyNamesTest.Vacation.class, + RecordEmbeddedPropertyNamesTest.TestFee.class, + RecordEmbeddedPropertyNamesTest.TestGetaway.class, + RecordEmbeddedPropertyNamesTest.TestVacation.class, +} ) +@SessionFactory +@Jira( "https://hibernate.atlassian.net/browse/HHH-18445" ) +public class RecordEmbeddedPropertyNamesTest { + @Test + public void testFee(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final TestFee result = session.find( TestFee.class, 1L ); + assertThat( result.getFee().issuedA() ).isFalse(); + assertThat( result.getFee().issuedB() ).isTrue(); + } ); + } + + @Test + public void testGetaway(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final TestGetaway result = session.find( TestGetaway.class, 1L ); + assertThat( result.getGetaway().getawayA() ).isEqualTo( "A" ); + assertThat( result.getGetaway().getawayB() ).isEqualTo( "B" ); + } ); + } + + @Test + public void testVacation(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final TestVacation result = session.find( TestVacation.class, 1L ); + assertThat( result.getVacation().amount() ).isEqualTo( 7 ); + assertThat( result.getVacation().issued() ).isTrue(); + } ); + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.persist( new TestFee( 1L, new Fee( true, false ) ) ); + session.persist( new TestGetaway( 1L, new Getaway( "B", "A" ) ) ); + session.persist( new TestVacation( 1L, new Vacation( true, 7 ) ) ); + } ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Embeddable + public record Fee(Boolean issuedB, Boolean issuedA) { + } + + @Entity( name = "TestFee" ) + static class TestFee { + @Id + private Long id; + @Embedded + private Fee fee; + + public TestFee() { + } + + public TestFee(Long id, Fee fee) { + this.id = id; + this.fee = fee; + } + + public Fee getFee() { + return fee; + } + } + + @Embeddable + record Getaway(String getawayB, String getawayA) { + } + + @Entity( name = "TestGetaway" ) + static class TestGetaway { + @Id + private Long id; + + @Embedded + private Getaway getaway; + + public TestGetaway() { + } + + public TestGetaway(Long id, Getaway getaway) { + this.id = id; + this.getaway = getaway; + } + + public Getaway getGetaway() { + return getaway; + } + } + + @Embeddable + record Vacation(Boolean issued, Integer amount) { + } + + @Entity( name = "TestVacation" ) + static class TestVacation { + @Id + private Long id; + + @Embedded + private Vacation vacation; + + public TestVacation() { + } + + public TestVacation(Long id, Vacation vacation) { + this.id = id; + this.vacation = vacation; + } + + public Vacation getVacation() { + return vacation; + } + } +} diff --git a/hibernate-core/src/test/resources/org/hibernate/orm/test/columndiscriminator/orm.xml b/hibernate-core/src/test/resources/org/hibernate/orm/test/columndiscriminator/orm.xml new file mode 100644 index 000000000000..02e59602ee07 --- /dev/null +++ b/hibernate-core/src/test/resources/org/hibernate/orm/test/columndiscriminator/orm.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/hibernate-core/src/test/resources/org/hibernate/orm/test/dynamicmap/artist.hbm.xml b/hibernate-core/src/test/resources/org/hibernate/orm/test/dynamicmap/artist.hbm.xml new file mode 100644 index 000000000000..590155b72dfc --- /dev/null +++ b/hibernate-core/src/test/resources/org/hibernate/orm/test/dynamicmap/artist.hbm.xml @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/boot/internal/EnversIntegrator.java b/hibernate-envers/src/main/java/org/hibernate/envers/boot/internal/EnversIntegrator.java index 97de0810ef4e..be4e140a1ccc 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/boot/internal/EnversIntegrator.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/boot/internal/EnversIntegrator.java @@ -20,6 +20,7 @@ import org.hibernate.envers.event.spi.EnversPreCollectionRemoveEventListenerImpl; import org.hibernate.envers.event.spi.EnversPreCollectionUpdateEventListenerImpl; import org.hibernate.envers.event.spi.EnversPreUpdateEventListenerImpl; +import org.hibernate.envers.internal.tools.ReflectionTools; import org.hibernate.event.service.spi.EventListenerRegistry; import org.hibernate.event.spi.EventType; import org.hibernate.integrator.spi.Integrator; @@ -119,6 +120,6 @@ public void integrate( @Override public void disintegrate(SessionFactoryImplementor sessionFactory, SessionFactoryServiceRegistry serviceRegistry) { - // nothing to do + ReflectionTools.reset(); } } diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/event/spi/EnversPostUpdateEventListenerImpl.java b/hibernate-envers/src/main/java/org/hibernate/envers/event/spi/EnversPostUpdateEventListenerImpl.java index 4c01fa70991b..edbb37264224 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/event/spi/EnversPostUpdateEventListenerImpl.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/event/spi/EnversPostUpdateEventListenerImpl.java @@ -6,6 +6,9 @@ */ package org.hibernate.envers.event.spi; +import org.hibernate.bytecode.enhance.spi.LazyPropertyInitializer; +import org.hibernate.bytecode.enhance.spi.interceptor.LazyAttributeLoadingInterceptor; +import org.hibernate.bytecode.spi.BytecodeEnhancementMetadata; import org.hibernate.envers.boot.internal.EnversService; import org.hibernate.envers.internal.synchronization.AuditProcess; import org.hibernate.envers.internal.synchronization.work.AuditWorkUnit; @@ -73,12 +76,30 @@ private Object[] postUpdateDBState(PostUpdateEvent event) { final Object[] newDbState = event.getState().clone(); if ( event.getOldState() != null ) { final EntityPersister entityPersister = event.getPersister(); + final Object entity = event.getEntity(); + final BytecodeEnhancementMetadata instrumentationMetadata = entityPersister.getInstrumentationMetadata(); + final LazyAttributeLoadingInterceptor lazyAttributeLoadingInterceptor; + if ( instrumentationMetadata.isEnhancedForLazyLoading() ) { + lazyAttributeLoadingInterceptor = instrumentationMetadata.extractInterceptor( entity ); + } + else { + lazyAttributeLoadingInterceptor = null; + } for ( int i = 0; i < entityPersister.getPropertyNames().length; ++i ) { if ( !entityPersister.getPropertyUpdateability()[i] ) { // Assuming that PostUpdateEvent#getOldState() returns database state of the record before modification. // Otherwise, we would have to execute SQL query to be sure of @Column(updatable = false) column value. newDbState[i] = event.getOldState()[i]; } + // Properties that have not been initialized need to be fetched in order to bind their value in the + // AUDIT insert statement. + if ( newDbState[i] == LazyPropertyInitializer.UNFETCHED_PROPERTY ) { + assert lazyAttributeLoadingInterceptor != null : "Entity:`" + entityPersister.getEntityName() + "` with uninitialized property:` " + entityPersister.getPropertyNames()[i] + "` hasn't an associated LazyAttributeLoadingInterceptor"; + event.getOldState()[i] = newDbState[i] = lazyAttributeLoadingInterceptor.fetchAttribute( + entity, + entityPersister.getPropertyNames()[i] + ); + } } } return newDbState; diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/relation/BasicCollectionMapper.java b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/relation/BasicCollectionMapper.java index d84bb582334d..d3ede1e2fae8 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/relation/BasicCollectionMapper.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/relation/BasicCollectionMapper.java @@ -200,7 +200,7 @@ private boolean isCollectionElementSame( // Currently the tuple is { owner_id, entity_id, rev } and so having this special // treatment is critical to avoid HHH-13080. // - if ( elementType.isEntityType() && !revisionTypeInId ) { + if ( elementType instanceof EntityType && !revisionTypeInId ) { // This is a short-circuit to check for reference equality only. // There is no need to delegate to the identifier if the objects are reference equal. diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/internal/synchronization/work/AbstractAuditWorkUnit.java b/hibernate-envers/src/main/java/org/hibernate/envers/internal/synchronization/work/AbstractAuditWorkUnit.java index c5688add9cfe..0d9f0951d3d8 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/internal/synchronization/work/AbstractAuditWorkUnit.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/internal/synchronization/work/AbstractAuditWorkUnit.java @@ -58,6 +58,8 @@ protected void fillDataWithId(Map data, Object revision) { data.put( configuration.getRevisionTypePropertyName(), revisionType ); data.put( configuration.getOriginalIdPropertyName(), originalId ); + // The $type$ property holds the name of the (versions) entity + data.put( "$type$", configuration.getAuditEntityName( entityName ) ); } @Override diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/internal/tools/ReflectionTools.java b/hibernate-envers/src/main/java/org/hibernate/envers/internal/tools/ReflectionTools.java index 0c67f2faecb1..0f9dbb4858e6 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/internal/tools/ReflectionTools.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/internal/tools/ReflectionTools.java @@ -186,4 +186,9 @@ public static Class loadClass(String name, ClassLoaderService classLoader throw new ClassLoadingException( "Unable to load class [" + name + "]", e ); } } + + public static void reset() { + SETTER_CACHE.clear(); + GETTER_CACHE.clear(); + } } diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/strategy/internal/ValidityAuditStrategy.java b/hibernate-envers/src/main/java/org/hibernate/envers/strategy/internal/ValidityAuditStrategy.java index a2beb9f79d45..4703d54b4f31 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/strategy/internal/ValidityAuditStrategy.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/strategy/internal/ValidityAuditStrategy.java @@ -433,7 +433,7 @@ private boolean isNonIdentifierWhereConditionsRequired(String entityName, String final Type propertyType = session.getSessionFactory() .getMappingMetamodel() .getEntityDescriptor( entityName ).getPropertyType( propertyName ); - if ( propertyType.isCollectionType() ) { + if ( propertyType instanceof CollectionType ) { final CollectionType collectionType = (CollectionType) propertyType; final Type collectionElementType = collectionType.getElementType( session.getSessionFactory() ); if ( collectionElementType instanceof ComponentType ) { diff --git a/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/blob/BasicBlobTest.java b/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/blob/BasicBlobTest.java new file mode 100644 index 000000000000..e0cfc56a6cff --- /dev/null +++ b/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/blob/BasicBlobTest.java @@ -0,0 +1,157 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.envers.integration.blob; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import org.hamcrest.Matchers; +import org.hibernate.dialect.PostgreSQLDialect; +import org.hibernate.dialect.SQLServerDialect; +import org.hibernate.dialect.SybaseDialect; +import org.hibernate.engine.jdbc.BlobProxy; +import org.hibernate.envers.Audited; +import org.hibernate.orm.test.envers.BaseEnversJPAFunctionalTestCase; +import org.hibernate.orm.test.envers.Priority; +import org.hibernate.testing.SkipForDialect; +import org.junit.Test; + +import java.io.BufferedInputStream; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.sql.Blob; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hibernate.testing.transaction.TransactionUtil.doInJPA; +import static org.junit.Assert.fail; + +/** + * @author Chris Cranford + */ +public class BasicBlobTest extends BaseEnversJPAFunctionalTestCase { + + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] {Asset.class}; + } + + @Test + @Priority(10) + public void testGenerateProxyNoStream() { + final Path path = Path.of( Thread.currentThread().getContextClassLoader() + .getResource( "org/hibernate/orm/test/envers/integration/blob/blob.txt" ).getPath() ); + doInJPA( this::entityManagerFactory, entityManager -> { + final Asset asset = new Asset(); + asset.setFileName( "blob.txt" ); + + try (final InputStream stream = new BufferedInputStream( Files.newInputStream( path ) )) { + assertThat( stream.markSupported(), Matchers.is( true ) ); + + // We use the method readAllBytes instead of passing the raw stream to the proxy + // since this is the only guaranteed way that will work across all dialects in a + // deterministic way. Postgres and Sybase will automatically close the stream + // after the blob has been written by the driver, which prevents Envers from + // then writing the contents of the stream to the audit table. + // + // If the driver and dialect are known not to close the input stream after the + // contents have been written by the driver, then it's safe to pass the stream + // here instead and the stream will be automatically marked and reset so that + // Envers can serialize the data after Hibernate has done so. Dialects like + // H2, MySQL, Oracle, SQL Server work this way. + // + // + Blob blob = BlobProxy.generateProxy( stream.readAllBytes() ); + + asset.setData( blob ); + entityManager.persist( asset ); + } + catch (Exception e) { + e.printStackTrace(); + fail( "Failed to persist the entity" ); + } + } ); + + } + + @Test + @Priority(10) + @SkipForDialect(value = PostgreSQLDialect.class, + comment = "The driver closes the stream, so it cannot be reused by envers") + @SkipForDialect(value = SQLServerDialect.class, + comment = "The driver closes the stream, so it cannot be reused by envers") + @SkipForDialect(value = SybaseDialect.class, + comment = "The driver closes the stream, so it cannot be reused by envers") + public void testGenerateProxyStream() { + final Path path = Path.of( Thread.currentThread().getContextClassLoader() + .getResource( "org/hibernate/orm/test/envers/integration/blob/blob.txt" ).getPath() ); + + try (final InputStream stream = new BufferedInputStream( Files.newInputStream( path ) )) { + doInJPA( this::entityManagerFactory, entityManager -> { + final Asset asset = new Asset(); + asset.setFileName( "blob.txt" ); + + assertThat( stream.markSupported(), Matchers.is( true ) ); + + // We use the method readAllBytes instead of passing the raw stream to the proxy + // since this is the only guaranteed way that will work across all dialects in a + // deterministic way. Postgres and Sybase will automatically close the stream + // after the blob has been written by the driver, which prevents Envers from + // then writing the contents of the stream to the audit table. + // + // If the driver and dialect are known not to close the input stream after the + // contents have been written by the driver, then it's safe to pass the stream + // here instead and the stream will be automatically marked and reset so that + // Envers can serialize the data after Hibernate has done so. Dialects like + // H2, MySQL, Oracle, SQL Server work this way. + // + // + Blob blob = BlobProxy.generateProxy( stream, 1431 ); + + asset.setData( blob ); + entityManager.persist( asset ); + } ); + } + catch (Exception e) { + e.printStackTrace(); + fail( "Failed to persist the entity" ); + } + } + + @Audited + @Entity(name = "Asset") + public static class Asset { + @Id + @GeneratedValue + private Integer id; + private String fileName; + private Blob data; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getFileName() { + return fileName; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } + + public Blob getData() { + return data; + } + + public void setData(Blob data) { + this.data = data; + } + } + +} diff --git a/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/lazy/LazyFieldsTest.java b/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/lazy/LazyFieldsTest.java new file mode 100644 index 000000000000..df4efcb12421 --- /dev/null +++ b/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/lazy/LazyFieldsTest.java @@ -0,0 +1,100 @@ +package org.hibernate.orm.test.envers.integration.lazy; + +import org.hibernate.annotations.LazyGroup; +import org.hibernate.envers.Audited; + +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Basic; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; + +@JiraKey("") +@BytecodeEnhanced +@DomainModel( + annotatedClasses = { + LazyFieldsTest.TestEntity.class, + } +) +@SessionFactory +public class LazyFieldsTest { + + private static final Long ID = 1L; + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + TestEntity testEntity = new TestEntity( ID, "test data", "lazyString", "group A" ); + session.persist( testEntity ); + } + ); + } + + @Test + public void testUpdate(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + TestEntity testEntity = session.find( TestEntity.class, ID ); + testEntity.setData( "modified test data" ); + } + ); + } + + @Entity(name = "TestEntity") + public static class TestEntity { + @Id + private Long id; + + @Audited + @Basic(optional = false) + private String data; + + @Audited + @Basic(fetch = FetchType.LAZY) + private String lazyString; + + @Audited + @Basic(fetch = FetchType.LAZY) + @LazyGroup("a") + private String lazyStringGroupA; + + public TestEntity() { + } + + public TestEntity(Long id, String data, String lazyString, String anotherLazyString) { + this.id = id; + this.data = data; + this.lazyString = lazyString; + this.lazyStringGroupA = anotherLazyString; + } + + public Long getId() { + return id; + } + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } + + public String getLazyString() { + return lazyString; + } + + public String getLazyStringGroupA() { + return lazyStringGroupA; + } + } + +} diff --git a/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/manytoone/bidirectional/ManyToOneCustomRevisionListenerTest.java b/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/manytoone/bidirectional/ManyToOneCustomRevisionListenerTest.java new file mode 100644 index 000000000000..460f0f64dbe9 --- /dev/null +++ b/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/manytoone/bidirectional/ManyToOneCustomRevisionListenerTest.java @@ -0,0 +1,204 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.envers.integration.manytoone.bidirectional; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityManager; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import org.hibernate.envers.AuditReader; +import org.hibernate.envers.Audited; +import org.hibernate.envers.EntityTrackingRevisionListener; +import org.hibernate.envers.RevisionEntity; +import org.hibernate.envers.RevisionListener; +import org.hibernate.envers.RevisionNumber; +import org.hibernate.envers.RevisionTimestamp; +import org.hibernate.envers.RevisionType; +import org.hibernate.orm.test.envers.BaseEnversJPAFunctionalTestCase; +import org.hibernate.orm.test.envers.Priority; +import org.hibernate.testing.orm.junit.Jira; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@Jira( "https://hibernate.atlassian.net/browse/HHH-17652" ) +public class ManyToOneCustomRevisionListenerTest extends BaseEnversJPAFunctionalTestCase { + private static final ThreadLocal auditReader = ThreadLocal.withInitial( () -> null ); + + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] { + Document.class, + DocumentAuthorEmployee.class, + Employee.class, + CustomRevisionEntity.class, + }; + } + + @Test + @Priority(10) + public void initData() { + // store in thread-local to use it in custom revision listener + auditReader.set( getAuditReader() ); + + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + + final Employee bilbo = new Employee( "Bilbo Baggins" ); + em.persist( bilbo ); + final Employee frodo = new Employee( "Frodo Baggins" ); + em.persist( frodo ); + + em.getTransaction().commit(); + + em.getTransaction().begin(); + + final Document document = new Document( "The Hobbit" ); + document.getAuthors().add( new DocumentAuthorEmployee( 1L, document, bilbo ) ); + document.getAuthors().add( new DocumentAuthorEmployee( 2L, document, frodo ) ); + em.persist( document ); + + em.getTransaction().commit(); + } + + @Test + public void testDocumentAuthorEmployeeRevisions() { + final AuditReader reader = getAuditReader(); + assertLastRevision( reader, 1L, "Bilbo Baggins" ); + assertLastRevision( reader, 2L, "Frodo Baggins" ); + getEntityManager().close(); + } + + private static void assertLastRevision(AuditReader reader, Long id, String employee) { + final List revisions = reader.getRevisions( DocumentAuthorEmployee.class, id ); + final Number revisionNumber = revisions.get( revisions.size() - 1 ); + final DocumentAuthorEmployee result = reader.find( DocumentAuthorEmployee.class, id, revisionNumber ); + assertThat( result.getEmployee().getName() ).isEqualTo( employee ); + assertThat( result.getDocument().getTitle() ).isEqualTo( "The Hobbit" ); + } + + @Audited(withModifiedFlag = true) + @Entity(name = "Document") + static class Document { + @Id + @GeneratedValue + private Long id; + + private String title; + + @OneToMany(mappedBy = "document", cascade = CascadeType.ALL) + private List authors = new ArrayList<>(); + + public Document() { + } + + public Document(String title) { + this.title = title; + } + + public List getAuthors() { + return authors; + } + + public String getTitle() { + return title; + } + } + + @Audited(withModifiedFlag = true) + @Entity(name = "DocumentAuthorEmployee") + static class DocumentAuthorEmployee { + @Id + private Long id; + + @ManyToOne + @JoinColumn(name = "document_id") + private Document document; + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "employee_id") + private Employee employee; + + public DocumentAuthorEmployee() { + } + + public DocumentAuthorEmployee(Long id, Document document, Employee employee) { + this.id = id; + this.document = document; + this.employee = employee; + } + + public Long getId() { + return id; + } + + public Document getDocument() { + return document; + } + + public Employee getEmployee() { + return employee; + } + } + + @Audited(withModifiedFlag = true) + @Entity(name = "Employee") + static class Employee { + @Id + @GeneratedValue + private Long id; + + private String name; + + public Employee() { + } + + public Employee(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + @Entity(name = "CustomRevisionEntity") + @RevisionEntity(CustomRevisionListener.class) + static class CustomRevisionEntity { + @Id + @GeneratedValue + @RevisionNumber + private int id; + + @RevisionTimestamp + private long timestamp; + } + + static class CustomRevisionListener implements RevisionListener, EntityTrackingRevisionListener { + @SuppressWarnings({"unchecked", "rawtypes"}) + @Override + public void entityChanged(Class entityClass, String entityName, Object entityId, RevisionType revisionType, Object revisionEntity) { + final AuditReader reader = auditReader.get(); + final List revisions = reader.getRevisions( entityClass, entityId ); + final Number revisionNumber = revisions.get( revisions.size() - 1 ); + + // This is what triggered the NPE + final Object obj = reader.find( entityClass, entityId, revisionNumber ); + assertThat( obj ).isNotNull(); + } + + @Override + public void newRevision(Object revisionEntity) { + } + } +} diff --git a/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/metamodel/RevisionEntitiesMetamodelTest.java b/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/metamodel/RevisionEntitiesMetamodelTest.java new file mode 100644 index 000000000000..78504c8bd1c7 --- /dev/null +++ b/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/metamodel/RevisionEntitiesMetamodelTest.java @@ -0,0 +1,99 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.envers.integration.metamodel; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.boot.MetadataSources; +import org.hibernate.boot.registry.StandardServiceRegistryBuilder; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.envers.Audited; +import org.hibernate.envers.configuration.EnversSettings; +import org.hibernate.internal.HEMLogging; +import org.hibernate.metamodel.internal.MetadataContext; +import org.hibernate.testing.logger.LogInspectionHelper; +import org.hibernate.testing.logger.TriggerOnPrefixLogListener; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.util.ServiceRegistryUtil; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +import java.time.Instant; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Marco Belladelli + */ +@Jira( "https://hibernate.atlassian.net/browse/HHH-17612" ) +@TestInstance( TestInstance.Lifecycle.PER_CLASS ) +public class RevisionEntitiesMetamodelTest { + private TriggerOnPrefixLogListener trigger; + + @BeforeAll + public void setUp() { + trigger = new TriggerOnPrefixLogListener( "HHH015007: Illegal argument on static metamodel field injection" ); + LogInspectionHelper.registerListener( trigger, HEMLogging.messageLogger( MetadataContext.class ) ); + } + + @Test + public void testDefaultRevisionEntity() { + try (final SessionFactoryImplementor ignored = buildSessionFactory( false, true )) { + assertThat( trigger.wasTriggered() ).isFalse(); + } + } + + @Test + public void testSequenceIdRevisionEntity() { + try (final SessionFactoryImplementor ignored = buildSessionFactory( false, false )) { + assertThat( trigger.wasTriggered() ).isFalse(); + } + } + + @Test + public void testDefaultTrackingModifiedEntitiesRevisionEntity() { + try (final SessionFactoryImplementor ignored = buildSessionFactory( true, true )) { + assertThat( trigger.wasTriggered() ).isFalse(); + } + } + + @Test + public void testSequenceIdTrackingModifiedEntitiesRevisionEntity() { + try (final SessionFactoryImplementor ignored = buildSessionFactory( true, false )) { + assertThat( trigger.wasTriggered() ).isFalse(); + } + } + + @SuppressWarnings( "resource" ) + private static SessionFactoryImplementor buildSessionFactory(boolean trackEntities, boolean nativeId) { + final StandardServiceRegistryBuilder registryBuilder = ServiceRegistryUtil.serviceRegistryBuilder(); + registryBuilder.applySetting( EnversSettings.TRACK_ENTITIES_CHANGED_IN_REVISION, trackEntities ); + registryBuilder.applySetting( EnversSettings.USE_REVISION_ENTITY_WITH_NATIVE_ID, nativeId ); + return new MetadataSources( registryBuilder.build() ) + .addAnnotatedClasses( Customer.class ) + .buildMetadata() + .buildSessionFactory() + .unwrap( SessionFactoryImplementor.class ); + } + + @Audited + @Entity( name = "Customer" ) + @SuppressWarnings( "unused" ) + public static class Customer { + @Id + private Long id; + + private String firstName; + + private String lastName; + + @Column( name = "created_on" ) + @CreationTimestamp + private Instant createdOn; + } +} diff --git a/hibernate-envers/src/test/resources/org/hibernate/orm/test/envers/integration/blob/blob.txt b/hibernate-envers/src/test/resources/org/hibernate/orm/test/envers/integration/blob/blob.txt new file mode 100644 index 000000000000..7a1354040f95 --- /dev/null +++ b/hibernate-envers/src/test/resources/org/hibernate/orm/test/envers/integration/blob/blob.txt @@ -0,0 +1,31 @@ + + + + + + + create-drop + + false + false + + org.hibernate.dialect.H2Dialect + jdbc:h2:mem:envers + org.h2.Driver + sa + + + + + + + + + + + diff --git a/hibernate-hikaricp/src/main/java/org/hibernate/hikaricp/internal/HikariCPConnectionProvider.java b/hibernate-hikaricp/src/main/java/org/hibernate/hikaricp/internal/HikariCPConnectionProvider.java index 725976271bec..c9859499a97d 100644 --- a/hibernate-hikaricp/src/main/java/org/hibernate/hikaricp/internal/HikariCPConnectionProvider.java +++ b/hibernate-hikaricp/src/main/java/org/hibernate/hikaricp/internal/HikariCPConnectionProvider.java @@ -8,9 +8,10 @@ package org.hibernate.hikaricp.internal; import java.sql.Connection; +import java.sql.DatabaseMetaData; import java.sql.SQLException; -import java.util.Map; import javax.sql.DataSource; +import java.util.Map; import org.hibernate.HibernateException; import org.hibernate.dialect.Dialect; @@ -18,6 +19,7 @@ import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider; import org.hibernate.engine.jdbc.connections.spi.DatabaseConnectionInfo; import org.hibernate.internal.log.ConnectionInfoLogger; +import org.hibernate.internal.util.StringHelper; import org.hibernate.service.UnknownUnwrapTypeException; import org.hibernate.service.spi.Configurable; import org.hibernate.service.spi.Stoppable; @@ -25,6 +27,8 @@ import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; +import static org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.allowJdbcMetadataAccess; + /** * HikariCP Connection provider for Hibernate. * @@ -34,6 +38,7 @@ public class HikariCPConnectionProvider implements ConnectionProvider, Configurable, Stoppable { private static final long serialVersionUID = -9131625057941275711L; + private boolean isMetadataAccessAllowed = true; /** * HikariCP configuration. @@ -52,6 +57,8 @@ public class HikariCPConnectionProvider implements ConnectionProvider, Configura @Override public void configure(Map props) throws HibernateException { try { + isMetadataAccessAllowed = allowJdbcMetadataAccess( props ); + ConnectionInfoLogger.INSTANCE.configureConnectionPool( "HikariCP" ); hcfg = HikariConfigurationUtil.loadConfiguration( props ); @@ -86,7 +93,9 @@ public boolean supportsAggressiveRelease() { public DatabaseConnectionInfo getDatabaseConnectionInfo(Dialect dialect) { return new DatabaseConnectionInfoImpl( hcfg.getJdbcUrl(), - hcfg.getDriverClassName(), + // Attempt to resolve the driver name from the dialect, in case it wasn't explicitly set and access to + // the database metadata is allowed + !StringHelper.isBlank( hcfg.getDriverClassName() ) ? hcfg.getDriverClassName() : extractDriverNameFromMetadata(), dialect.getVersion(), Boolean.toString( hcfg.isAutoCommit() ), hcfg.getTransactionIsolation(), @@ -95,6 +104,19 @@ public DatabaseConnectionInfo getDatabaseConnectionInfo(Dialect dialect) { ); } + private String extractDriverNameFromMetadata() { + if (isMetadataAccessAllowed) { + try ( Connection conn = getConnection() ) { + DatabaseMetaData dbmd = conn.getMetaData(); + return dbmd.getDriverName(); + } + catch (SQLException e) { + // Do nothing + } + } + return null; + } + @Override public boolean isUnwrappableAs(Class unwrapType) { return ConnectionProvider.class.equals( unwrapType ) diff --git a/hibernate-jfr/src/main/java/org/hibernate/event/jfr/internal/JfrEventManager.java b/hibernate-jfr/src/main/java/org/hibernate/event/jfr/internal/JfrEventManager.java index 992846428c07..d39ac9642ea8 100644 --- a/hibernate-jfr/src/main/java/org/hibernate/event/jfr/internal/JfrEventManager.java +++ b/hibernate-jfr/src/main/java/org/hibernate/event/jfr/internal/JfrEventManager.java @@ -56,10 +56,10 @@ public SessionOpenEvent beginSessionOpenEvent() { @Override public void completeSessionOpenEvent( - HibernateMonitoringEvent event, + HibernateMonitoringEvent monitoringEvent, SharedSessionContractImplementor session) { - if ( event != null ) { - final SessionOpenEvent sessionOpenEvent = (SessionOpenEvent) event; + if ( monitoringEvent != null ) { + final SessionOpenEvent sessionOpenEvent = (SessionOpenEvent) monitoringEvent; sessionOpenEvent.end(); if ( sessionOpenEvent.shouldCommit() ) { sessionOpenEvent.sessionIdentifier = getSessionIdentifier( session ); @@ -82,10 +82,10 @@ public SessionClosedEvent beginSessionClosedEvent() { @Override public void completeSessionClosedEvent( - HibernateMonitoringEvent event, + HibernateMonitoringEvent monitoringEvent, SharedSessionContractImplementor session) { - if ( event != null ) { - final SessionClosedEvent sessionClosedEvent = (SessionClosedEvent) event; + if ( monitoringEvent != null ) { + final SessionClosedEvent sessionClosedEvent = (SessionClosedEvent) monitoringEvent; sessionClosedEvent.end(); if ( sessionClosedEvent.shouldCommit() ) { sessionClosedEvent.sessionIdentifier = getSessionIdentifier( session ); @@ -108,11 +108,11 @@ public JdbcConnectionAcquisitionEvent beginJdbcConnectionAcquisitionEvent() { @Override public void completeJdbcConnectionAcquisitionEvent( - HibernateMonitoringEvent event, + HibernateMonitoringEvent monitoringEvent, SharedSessionContractImplementor session, Object tenantId) { - if ( event != null ) { - final JdbcConnectionAcquisitionEvent jdbcConnectionAcquisitionEvent = (JdbcConnectionAcquisitionEvent) event; + if ( monitoringEvent != null ) { + final JdbcConnectionAcquisitionEvent jdbcConnectionAcquisitionEvent = (JdbcConnectionAcquisitionEvent) monitoringEvent; jdbcConnectionAcquisitionEvent.end(); if ( jdbcConnectionAcquisitionEvent.shouldCommit() ) { jdbcConnectionAcquisitionEvent.sessionIdentifier = getSessionIdentifier( session ); @@ -138,11 +138,11 @@ public JdbcConnectionReleaseEvent beginJdbcConnectionReleaseEvent() { @Override public void completeJdbcConnectionReleaseEvent( - HibernateMonitoringEvent event, + HibernateMonitoringEvent monitoringEvent, SharedSessionContractImplementor session, Object tenantId) { - if ( event != null ) { - final JdbcConnectionReleaseEvent jdbcConnectionReleaseEvent = (JdbcConnectionReleaseEvent) event; + if ( monitoringEvent != null ) { + final JdbcConnectionReleaseEvent jdbcConnectionReleaseEvent = (JdbcConnectionReleaseEvent) monitoringEvent; jdbcConnectionReleaseEvent.end(); if ( jdbcConnectionReleaseEvent.shouldCommit() ) { jdbcConnectionReleaseEvent.sessionIdentifier = getSessionIdentifier( session ); @@ -168,10 +168,10 @@ public JdbcPreparedStatementCreationEvent beginJdbcPreparedStatementCreationEven @Override public void completeJdbcPreparedStatementCreationEvent( - HibernateMonitoringEvent event, + HibernateMonitoringEvent monitoringEvent, String preparedStatementSql) { - if ( event != null ) { - final JdbcPreparedStatementCreationEvent jdbcPreparedStatementCreation = (JdbcPreparedStatementCreationEvent) event; + if ( monitoringEvent != null ) { + final JdbcPreparedStatementCreationEvent jdbcPreparedStatementCreation = (JdbcPreparedStatementCreationEvent) monitoringEvent; jdbcPreparedStatementCreation.end(); if ( jdbcPreparedStatementCreation.shouldCommit() ) { jdbcPreparedStatementCreation.sql = preparedStatementSql; @@ -194,10 +194,10 @@ public JdbcPreparedStatementExecutionEvent beginJdbcPreparedStatementExecutionEv @Override public void completeJdbcPreparedStatementExecutionEvent( - HibernateMonitoringEvent event, + HibernateMonitoringEvent monitoringEvent, String preparedStatementSql) { - if ( event != null ) { - final JdbcPreparedStatementExecutionEvent jdbcPreparedStatementExecutionEvent = (JdbcPreparedStatementExecutionEvent) event; + if ( monitoringEvent != null ) { + final JdbcPreparedStatementExecutionEvent jdbcPreparedStatementExecutionEvent = (JdbcPreparedStatementExecutionEvent) monitoringEvent; jdbcPreparedStatementExecutionEvent.end(); if ( jdbcPreparedStatementExecutionEvent.shouldCommit() ) { jdbcPreparedStatementExecutionEvent.sql = preparedStatementSql; @@ -220,10 +220,10 @@ public JdbcBatchExecutionEvent beginJdbcBatchExecutionEvent() { @Override public void completeJdbcBatchExecutionEvent( - HibernateMonitoringEvent event, + HibernateMonitoringEvent monitoringEvent, String statementSql) { - if ( event != null ) { - final JdbcBatchExecutionEvent jdbcBatchExecutionEvent = (JdbcBatchExecutionEvent) event; + if ( monitoringEvent != null ) { + final JdbcBatchExecutionEvent jdbcBatchExecutionEvent = (JdbcBatchExecutionEvent) monitoringEvent; jdbcBatchExecutionEvent.end(); if ( jdbcBatchExecutionEvent.shouldCommit() ) { jdbcBatchExecutionEvent.sql = statementSql; @@ -246,13 +246,13 @@ public HibernateMonitoringEvent beginCachePutEvent() { @Override public void completeCachePutEvent( - HibernateMonitoringEvent event, + HibernateMonitoringEvent monitoringEvent, SharedSessionContractImplementor session, Region region, boolean cacheContentChanged, CacheActionDescription description) { - if ( event != null ) { - final CachePutEvent cachePutEvent = (CachePutEvent) event; + if ( monitoringEvent != null ) { + final CachePutEvent cachePutEvent = (CachePutEvent) monitoringEvent; cachePutEvent.end(); if ( cachePutEvent.shouldCommit() ) { cachePutEvent.sessionIdentifier = getSessionIdentifier( session ); @@ -266,14 +266,14 @@ public void completeCachePutEvent( @Override public void completeCachePutEvent( - HibernateMonitoringEvent event, + HibernateMonitoringEvent monitoringEvent, SharedSessionContractImplementor session, CachedDomainDataAccess cachedDomainDataAccess, EntityPersister persister, boolean cacheContentChanged, CacheActionDescription description) { completeCachePutEvent( - event, + monitoringEvent, session, cachedDomainDataAccess, persister, @@ -285,15 +285,15 @@ public void completeCachePutEvent( @Override public void completeCachePutEvent( - HibernateMonitoringEvent event, + HibernateMonitoringEvent monitoringEvent, SharedSessionContractImplementor session, CachedDomainDataAccess cachedDomainDataAccess, EntityPersister persister, boolean cacheContentChanged, boolean isNatualId, CacheActionDescription description) { - if ( event != null ) { - final CachePutEvent cachePutEvent = (CachePutEvent) event; + if ( monitoringEvent != null ) { + final CachePutEvent cachePutEvent = (CachePutEvent) monitoringEvent; cachePutEvent.end(); if ( cachePutEvent.shouldCommit() ) { cachePutEvent.sessionIdentifier = getSessionIdentifier( session ); @@ -309,14 +309,14 @@ public void completeCachePutEvent( @Override public void completeCachePutEvent( - HibernateMonitoringEvent event, + HibernateMonitoringEvent monitoringEvent, SharedSessionContractImplementor session, CachedDomainDataAccess cachedDomainDataAccess, CollectionPersister persister, boolean cacheContentChanged, CacheActionDescription description) { - if ( event != null ) { - final CachePutEvent cachePutEvent = (CachePutEvent) event; + if ( monitoringEvent != null ) { + final CachePutEvent cachePutEvent = (CachePutEvent) monitoringEvent; cachePutEvent.end(); if ( cachePutEvent.shouldCommit() ) { cachePutEvent.sessionIdentifier = getSessionIdentifier( session ); @@ -343,12 +343,12 @@ public HibernateMonitoringEvent beginCacheGetEvent() { @Override public void completeCacheGetEvent( - HibernateMonitoringEvent event, + HibernateMonitoringEvent monitoringEvent, SharedSessionContractImplementor session, Region region, boolean hit) { - if ( event != null ) { - final CacheGetEvent cacheGetEvent = (CacheGetEvent) event; + if ( monitoringEvent != null ) { + final CacheGetEvent cacheGetEvent = (CacheGetEvent) monitoringEvent; cacheGetEvent.end(); if ( cacheGetEvent.shouldCommit() ) { cacheGetEvent.sessionIdentifier = getSessionIdentifier( session ); @@ -361,14 +361,14 @@ public void completeCacheGetEvent( @Override public void completeCacheGetEvent( - HibernateMonitoringEvent event, + HibernateMonitoringEvent monitoringEvent, SharedSessionContractImplementor session, Region region, EntityPersister persister, boolean isNaturalKey, boolean hit) { - if ( event != null ) { - final CacheGetEvent cacheGetEvent = (CacheGetEvent) event; + if ( monitoringEvent != null ) { + final CacheGetEvent cacheGetEvent = (CacheGetEvent) monitoringEvent; cacheGetEvent.end(); if ( cacheGetEvent.shouldCommit() ) { cacheGetEvent.sessionIdentifier = getSessionIdentifier( session ); @@ -383,13 +383,13 @@ public void completeCacheGetEvent( @Override public void completeCacheGetEvent( - HibernateMonitoringEvent event, + HibernateMonitoringEvent monitoringEvent, SharedSessionContractImplementor session, Region region, CollectionPersister persister, boolean hit) { - if ( event != null ) { - final CacheGetEvent cacheGetEvent = (CacheGetEvent) event; + if ( monitoringEvent != null ) { + final CacheGetEvent cacheGetEvent = (CacheGetEvent) monitoringEvent; cacheGetEvent.end(); if ( cacheGetEvent.shouldCommit() ) { cacheGetEvent.sessionIdentifier = getSessionIdentifier( session ); @@ -422,18 +422,18 @@ public void completeFlushEvent( @Override public void completeFlushEvent( - HibernateMonitoringEvent hibernateMonitoringEvent, - org.hibernate.event.spi.FlushEvent event, + HibernateMonitoringEvent monitoringEvent, + org.hibernate.event.spi.FlushEvent hibernateFlushEvent, boolean autoFlush) { - if ( hibernateMonitoringEvent != null ) { - final FlushEvent flushEvent = (FlushEvent) hibernateMonitoringEvent; - flushEvent.end(); - if ( flushEvent.shouldCommit() ) { - flushEvent.sessionIdentifier = getSessionIdentifier( event.getSession() ); - flushEvent.numberOfEntitiesProcessed = event.getNumberOfEntitiesProcessed(); - flushEvent.numberOfCollectionsProcessed = event.getNumberOfCollectionsProcessed(); - flushEvent.isAutoFlush = autoFlush; - flushEvent.commit(); + if ( monitoringEvent != null ) { + final FlushEvent jfrFlushEvent = (FlushEvent) monitoringEvent; + jfrFlushEvent.end(); + if ( jfrFlushEvent.shouldCommit() ) { + jfrFlushEvent.sessionIdentifier = getSessionIdentifier( hibernateFlushEvent.getSession() ); + jfrFlushEvent.numberOfEntitiesProcessed = hibernateFlushEvent.getNumberOfEntitiesProcessed(); + jfrFlushEvent.numberOfCollectionsProcessed = hibernateFlushEvent.getNumberOfCollectionsProcessed(); + jfrFlushEvent.isAutoFlush = autoFlush; + jfrFlushEvent.commit(); } } } @@ -452,17 +452,17 @@ public PartialFlushEvent beginPartialFlushEvent() { @Override public void completePartialFlushEvent( - HibernateMonitoringEvent hibernateMonitoringEvent, - AutoFlushEvent event) { - if ( event != null ) { - final PartialFlushEvent flushEvent = (PartialFlushEvent) hibernateMonitoringEvent; - flushEvent.end(); - if ( flushEvent.shouldCommit() ) { - flushEvent.sessionIdentifier = getSessionIdentifier( event.getSession() ); - flushEvent.numberOfEntitiesProcessed = event.getNumberOfEntitiesProcessed(); - flushEvent.numberOfCollectionsProcessed = event.getNumberOfCollectionsProcessed(); - flushEvent.isAutoFlush = true; - flushEvent.commit(); + HibernateMonitoringEvent monitoringEvent, + AutoFlushEvent hibernateAutoFlushEvent) { + if ( monitoringEvent != null) { + final PartialFlushEvent jfrPartialFlushEvent = (PartialFlushEvent) monitoringEvent; + jfrPartialFlushEvent.end(); + if ( jfrPartialFlushEvent.shouldCommit() ) { + jfrPartialFlushEvent.sessionIdentifier = getSessionIdentifier( hibernateAutoFlushEvent.getSession() ); + jfrPartialFlushEvent.numberOfEntitiesProcessed = hibernateAutoFlushEvent.getNumberOfEntitiesProcessed(); + jfrPartialFlushEvent.numberOfCollectionsProcessed = hibernateAutoFlushEvent.getNumberOfCollectionsProcessed(); + jfrPartialFlushEvent.isAutoFlush = true; + jfrPartialFlushEvent.commit(); } } } @@ -481,13 +481,13 @@ public DirtyCalculationEvent beginDirtyCalculationEvent() { @Override public void completeDirtyCalculationEvent( - HibernateMonitoringEvent event, + HibernateMonitoringEvent monitoringEvent, SharedSessionContractImplementor session, EntityPersister persister, EntityEntry entry, int[] dirtyProperties) { - if ( event != null ) { - final DirtyCalculationEvent dirtyCalculationEvent = (DirtyCalculationEvent) event; + if ( monitoringEvent != null ) { + final DirtyCalculationEvent dirtyCalculationEvent = (DirtyCalculationEvent) monitoringEvent; dirtyCalculationEvent.end(); if ( dirtyCalculationEvent.shouldCommit() ) { dirtyCalculationEvent.sessionIdentifier = getSessionIdentifier( session ); @@ -513,10 +513,10 @@ public PrePartialFlushEvent beginPrePartialFlush() { @Override public void completePrePartialFlush( - HibernateMonitoringEvent event, + HibernateMonitoringEvent monitoringEvent, SharedSessionContractImplementor session) { - if ( event != null ) { - final PrePartialFlushEvent prePartialFlushEvent = (PrePartialFlushEvent) event; + if ( monitoringEvent != null ) { + final PrePartialFlushEvent prePartialFlushEvent = (PrePartialFlushEvent) monitoringEvent; prePartialFlushEvent.end(); if ( prePartialFlushEvent.shouldCommit() ) { prePartialFlushEvent.sessionIdentifier = getSessionIdentifier( session ); diff --git a/hibernate-jfr/src/test/java/org/hibernate/event/jfr/flush/AutoFlushJfrDisabledTests.java b/hibernate-jfr/src/test/java/org/hibernate/event/jfr/flush/AutoFlushJfrDisabledTests.java new file mode 100644 index 000000000000..6ccc5bcd01f2 --- /dev/null +++ b/hibernate-jfr/src/test/java/org/hibernate/event/jfr/flush/AutoFlushJfrDisabledTests.java @@ -0,0 +1,60 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.event.jfr.flush; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + + +@DomainModel(annotatedClasses = { + AutoFlushJfrDisabledTests.TestEntity.class, +}) +@SessionFactory +@JiraKey(value = "HHH-18770") +public class AutoFlushJfrDisabledTests { + + /** + * Execute a query (and a flush) with the jfr module on the classpath but jfr disabled + */ + @Test + public void testFlushEventWithPartialFlushEventDisabled(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + TestEntity entity = new TestEntity( 1, "name_1" ); + session.persist( entity ); + session.createQuery( "select t from TestEntity t" ).list(); + + session.remove( entity ); + } + ); + } + + @Entity(name = "TestEntity") + public static class TestEntity { + @Id + private Integer id; + + private String name; + + public TestEntity() { + } + + public TestEntity(Integer id, String name) { + this.id = id; + this.name = name; + } + + public void setName(String name) { + this.name = name; + } + } + +} diff --git a/hibernate-platform/hibernate-platform.gradle b/hibernate-platform/hibernate-platform.gradle index 46cd490f6f57..323819dc6cec 100644 --- a/hibernate-platform/hibernate-platform.gradle +++ b/hibernate-platform/hibernate-platform.gradle @@ -75,9 +75,3 @@ publishing { } } } - -project( ":release" ).getTasks().named( "publishReleaseArtifacts" ).configure { - dependsOn tasks.release -} - -tasks.release.dependsOn tasks.publishToSonatype diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/bytecode/enhancement/BytecodeEnhancerRunner.java b/hibernate-testing/src/main/java/org/hibernate/testing/bytecode/enhancement/BytecodeEnhancerRunner.java index dfdd5218d863..22e518345f87 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/bytecode/enhancement/BytecodeEnhancerRunner.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/bytecode/enhancement/BytecodeEnhancerRunner.java @@ -24,6 +24,7 @@ import org.hibernate.bytecode.enhance.spi.UnloadedField; import org.hibernate.cfg.Environment; +import org.hibernate.bytecode.enhance.spi.UnsupportedEnhancementStrategy; import org.hibernate.testing.junit4.CustomRunner; import org.junit.runner.Runner; import org.junit.runner.notification.RunNotifier; @@ -32,6 +33,7 @@ import org.junit.runners.model.InitializationError; import org.junit.runners.model.RunnerBuilder; +import static org.hibernate.bytecode.enhance.spi.UnsupportedEnhancementStrategy.SKIP; import static org.hibernate.bytecode.internal.BytecodeProviderInitiator.buildDefaultBytecodeProvider; /** @@ -116,6 +118,12 @@ public boolean hasLazyLoadableAttributes(UnloadedClass classDescriptor) { public boolean isLazyLoadable(UnloadedField field) { return options.lazyLoading() && super.isLazyLoadable( field ); } + + @Override + public UnsupportedEnhancementStrategy getUnsupportedEnhancementStrategy() { + final UnsupportedEnhancementStrategy strategy = options.unsupportedEnhancementStrategy(); + return strategy != SKIP ? strategy : super.getUnsupportedEnhancementStrategy(); + } }; } diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/bytecode/enhancement/EnhancementOptions.java b/hibernate-testing/src/main/java/org/hibernate/testing/bytecode/enhancement/EnhancementOptions.java index 7853126bec3b..043181934598 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/bytecode/enhancement/EnhancementOptions.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/bytecode/enhancement/EnhancementOptions.java @@ -6,6 +6,8 @@ */ package org.hibernate.testing.bytecode.enhancement; +import org.hibernate.bytecode.enhance.spi.UnsupportedEnhancementStrategy; + import java.lang.annotation.ElementType; import java.lang.annotation.Inherited; import java.lang.annotation.Retention; @@ -23,4 +25,5 @@ boolean inlineDirtyChecking() default false; boolean lazyLoading() default false; boolean extendedEnhancement() default false; + UnsupportedEnhancementStrategy unsupportedEnhancementStrategy() default UnsupportedEnhancementStrategy.SKIP; } diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/bytecode/enhancement/extension/engine/BytecodeEnhancedClassUtils.java b/hibernate-testing/src/main/java/org/hibernate/testing/bytecode/enhancement/extension/engine/BytecodeEnhancedClassUtils.java index 78cda58e4530..8e18e8457464 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/bytecode/enhancement/extension/engine/BytecodeEnhancedClassUtils.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/bytecode/enhancement/extension/engine/BytecodeEnhancedClassUtils.java @@ -6,6 +6,7 @@ */ package org.hibernate.testing.bytecode.enhancement.extension.engine; +import static org.hibernate.bytecode.enhance.spi.UnsupportedEnhancementStrategy.SKIP; import static org.hibernate.bytecode.internal.BytecodeProviderInitiator.buildDefaultBytecodeProvider; import java.io.BufferedInputStream; @@ -29,6 +30,7 @@ import org.hibernate.bytecode.enhance.spi.UnloadedClass; import org.hibernate.bytecode.enhance.spi.UnloadedField; +import org.hibernate.bytecode.enhance.spi.UnsupportedEnhancementStrategy; import org.hibernate.testing.bytecode.enhancement.ClassEnhancementSelector; import org.hibernate.testing.bytecode.enhancement.ClassEnhancementSelectors; import org.hibernate.testing.bytecode.enhancement.ClassSelector; @@ -114,6 +116,12 @@ public boolean hasLazyLoadableAttributes(UnloadedClass classDescriptor) { public boolean isLazyLoadable(UnloadedField field) { return options.lazyLoading() && super.isLazyLoadable( field ); } + + @Override + public UnsupportedEnhancementStrategy getUnsupportedEnhancementStrategy() { + final UnsupportedEnhancementStrategy strategy = options.unsupportedEnhancementStrategy(); + return strategy != SKIP ? strategy : super.getUnsupportedEnhancementStrategy(); + } }; } diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/bytecode/enhancement/extension/engine/BytecodeEnhancedEngineDescriptor.java b/hibernate-testing/src/main/java/org/hibernate/testing/bytecode/enhancement/extension/engine/BytecodeEnhancedEngineDescriptor.java index e8e279614cdf..dabdb9d7417f 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/bytecode/enhancement/extension/engine/BytecodeEnhancedEngineDescriptor.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/bytecode/enhancement/extension/engine/BytecodeEnhancedEngineDescriptor.java @@ -8,10 +8,18 @@ import org.junit.jupiter.engine.config.JupiterConfiguration; import org.junit.jupiter.engine.descriptor.JupiterEngineDescriptor; +import org.junit.platform.engine.TestDescriptor; import org.junit.platform.engine.UniqueId; public class BytecodeEnhancedEngineDescriptor extends JupiterEngineDescriptor { public BytecodeEnhancedEngineDescriptor(UniqueId uniqueId, JupiterConfiguration configuration) { super( uniqueId, configuration ); } + + public BytecodeEnhancedEngineDescriptor(JupiterEngineDescriptor engineDescriptor) { + super( engineDescriptor.getUniqueId(), engineDescriptor.getConfiguration() ); + for ( TestDescriptor child : engineDescriptor.getChildren() ) { + addChild( child ); + } + } } diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/bytecode/enhancement/extension/engine/BytecodeEnhancedTestEngine.java b/hibernate-testing/src/main/java/org/hibernate/testing/bytecode/enhancement/extension/engine/BytecodeEnhancedTestEngine.java index 0ec122aae6b2..bcc9d49f227f 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/bytecode/enhancement/extension/engine/BytecodeEnhancedTestEngine.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/bytecode/enhancement/extension/engine/BytecodeEnhancedTestEngine.java @@ -35,15 +35,13 @@ import org.junit.jupiter.api.io.CleanupMode; import org.junit.jupiter.api.io.TempDirFactory; import org.junit.jupiter.api.parallel.ExecutionMode; -import org.junit.jupiter.engine.config.CachingJupiterConfiguration; -import org.junit.jupiter.engine.config.DefaultJupiterConfiguration; +import org.junit.jupiter.engine.JupiterTestEngine; import org.junit.jupiter.engine.config.JupiterConfiguration; import org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor; import org.junit.jupiter.engine.descriptor.ClassTestDescriptor; import org.junit.jupiter.engine.descriptor.JupiterEngineDescriptor; import org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor; import org.junit.jupiter.engine.descriptor.TestTemplateTestDescriptor; -import org.junit.jupiter.engine.discovery.DiscoverySelectorResolver; import org.junit.jupiter.engine.execution.JupiterEngineExecutionContext; import org.junit.platform.engine.EngineDiscoveryRequest; import org.junit.platform.engine.ExecutionRequest; @@ -62,10 +60,9 @@ public String getId() { @Override public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId uniqueId) { - JupiterConfiguration configuration = new CachingJupiterConfiguration( - new DefaultJupiterConfiguration( discoveryRequest.getConfigurationParameters() ) ); - JupiterEngineDescriptor engineDescriptor = new BytecodeEnhancedEngineDescriptor( uniqueId, configuration ); - new DiscoverySelectorResolver().resolveSelectors( discoveryRequest, engineDescriptor ); + final BytecodeEnhancedEngineDescriptor engineDescriptor = new BytecodeEnhancedEngineDescriptor( + (JupiterEngineDescriptor) new JupiterTestEngine().discover( discoveryRequest, uniqueId ) + ); for ( TestDescriptor testDescriptor : new HashSet<>( engineDescriptor.getChildren() ) ) { if ( testDescriptor instanceof ClassBasedTestDescriptor ) { diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java index d775d6cbc7d4..dd8e2475d133 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java @@ -10,6 +10,7 @@ import org.hibernate.boot.model.TruthValue; import org.hibernate.community.dialect.FirebirdDialect; +import org.hibernate.community.dialect.InformixDialect; import org.hibernate.dialect.AbstractHANADialect; import org.hibernate.dialect.CockroachDialect; import org.hibernate.dialect.DB2Dialect; @@ -322,8 +323,9 @@ public boolean apply(Dialect dialect) { public static class SupportsRepeat implements DialectFeatureCheck { public boolean apply(Dialect dialect) { dialect = DialectDelegateWrapper.extractRealDialect( dialect ); - // Derby doesn't support the `REPLACE` function - return !( dialect instanceof DerbyDialect ); + // Derby doesn't support the `REPEAT` function + return !( dialect instanceof DerbyDialect + || dialect instanceof InformixDialect ); } } @@ -333,6 +335,12 @@ public boolean apply(Dialect dialect) { } } + public static class SupportsSubqueryInSelect implements DialectFeatureCheck { + public boolean apply(Dialect dialect) { + return dialect.supportsSubqueryInSelect(); + } + } + public static class SupportsValuesListForInsert implements DialectFeatureCheck { public boolean apply(Dialect dialect) { return dialect.supportsValuesListForInsert(); @@ -489,6 +497,7 @@ public boolean apply(Dialect dialect) { || dialect instanceof DerbyDialect || dialect instanceof FirebirdDialect || dialect instanceof DB2Dialect && ( (DB2Dialect) dialect ).getDB2Version().isBefore( 11 ) ) + || dialect instanceof InformixDialect || dialect instanceof MariaDBDialect; } } diff --git a/hibernate-vibur/src/main/java/org/hibernate/vibur/internal/ViburDBCPConnectionProvider.java b/hibernate-vibur/src/main/java/org/hibernate/vibur/internal/ViburDBCPConnectionProvider.java index e2aad4b54b42..52a81ec8fd08 100644 --- a/hibernate-vibur/src/main/java/org/hibernate/vibur/internal/ViburDBCPConnectionProvider.java +++ b/hibernate-vibur/src/main/java/org/hibernate/vibur/internal/ViburDBCPConnectionProvider.java @@ -8,6 +8,7 @@ package org.hibernate.vibur.internal; import java.sql.Connection; +import java.sql.DatabaseMetaData; import java.sql.SQLException; import java.util.Map; import java.util.Properties; @@ -17,6 +18,7 @@ import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider; import org.hibernate.engine.jdbc.connections.spi.DatabaseConnectionInfo; import org.hibernate.internal.log.ConnectionInfoLogger; +import org.hibernate.internal.util.StringHelper; import org.hibernate.service.UnknownUnwrapTypeException; import org.hibernate.service.spi.Configurable; import org.hibernate.service.spi.Stoppable; @@ -29,6 +31,7 @@ import static org.hibernate.cfg.AvailableSettings.PASS; import static org.hibernate.cfg.AvailableSettings.URL; import static org.hibernate.cfg.AvailableSettings.USER; +import static org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.allowJdbcMetadataAccess; /** *

    ViburDBCP connection provider for Hibernate integration. @@ -60,9 +63,12 @@ public class ViburDBCPConnectionProvider implements ConnectionProvider, Configur private static final String VIBUR_PREFIX = VIBUR_CONFIG_PREFIX + "."; private ViburDBCPDataSource dataSource = null; + private boolean isMetadataAccessAllowed = true; @Override public void configure(Map configurationValues) { + isMetadataAccessAllowed = allowJdbcMetadataAccess( configurationValues ); + ConnectionInfoLogger.INSTANCE.configureConnectionPool( "Vibur" ); dataSource = new ViburDBCPDataSource( transform( configurationValues ) ); @@ -97,7 +103,9 @@ public boolean supportsAggressiveRelease() { public DatabaseConnectionInfo getDatabaseConnectionInfo(Dialect dialect) { return new DatabaseConnectionInfoImpl( dataSource.getJdbcUrl(), - dataSource.getDriverClassName(), + // Attempt to resolve the driver name from the dialect, in case it wasn't explicitly set and access to + // the database metadata is allowed + !StringHelper.isBlank( dataSource.getDriverClassName() ) ? dataSource.getDriverClassName() : extractDriverNameFromMetadata(), dialect.getVersion(), String.valueOf( dataSource.getDefaultAutoCommit() ), dataSource.getDefaultTransactionIsolation(), @@ -106,6 +114,19 @@ public DatabaseConnectionInfo getDatabaseConnectionInfo(Dialect dialect) { ); } + private String extractDriverNameFromMetadata() { + if (isMetadataAccessAllowed) { + try ( Connection conn = getConnection() ) { + DatabaseMetaData dbmd = conn.getMetaData(); + return dbmd.getDriverName(); + } + catch (SQLException e) { + // Do nothing + } + } + return null; + } + @Override public boolean isUnwrappableAs(Class unwrapType) { return ConnectionProvider.class.equals( unwrapType ) diff --git a/local-build-plugins/src/main/java/org/hibernate/orm/properties/AsciiDocWriter.java b/local-build-plugins/src/main/java/org/hibernate/orm/properties/AsciiDocWriter.java index 3004ea90be7e..6f628226b407 100644 --- a/local-build-plugins/src/main/java/org/hibernate/orm/properties/AsciiDocWriter.java +++ b/local-build-plugins/src/main/java/org/hibernate/orm/properties/AsciiDocWriter.java @@ -68,9 +68,7 @@ private static void write( for ( SettingDescriptor settingDescriptor : sectionSettingDescriptors ) { // write an anchor in the form `[[{anchorNameBase}-{settingName}]]` tryToWriteLine( writer, "[[", anchorNameBase, "-", settingDescriptor.getName(), "]]" ); - - writeSettingName( settingDescriptor, writer ); - writer.write( "::\n" ); + tryToWriteLine( writer, "==== ", settingName( settingDescriptor ) ); writeMetadata( settingDescriptor, writer ); @@ -88,7 +86,6 @@ private static void writeMetadata(SettingDescriptor settingDescriptor, FileWrite return; } - writer.write( "+\n" ); writer.write( "****\n" ); writer.write( @@ -112,6 +109,12 @@ private static void writeMetadata(SettingDescriptor settingDescriptor, FileWrite if ( lifecycleDetails.isDeprecated() ) { writer.write( "WARNING: *_This setting is considered deprecated_*\n\n" ); } + if ( settingDescriptor.isUnsafe() ) { + writer.write( "WARNING: *_This setting is considered unsafe_*\n\n" ); + } + if ( settingDescriptor.isCompatibility() ) { + writer.write( "INFO: *_This setting manages a certain backwards compatibility_*\n\n" ); + } if ( lifecycleDetails.getSince() != null ) { writer.write( "*_Since:_* _" + lifecycleDetails.getSince() + "_\n\n" ); @@ -125,27 +128,24 @@ private static void writeMetadata(SettingDescriptor settingDescriptor, FileWrite writer.write( settingDescriptor.getApiNote() + "\n\n" ); } - writer.write( "****\n+\n" ); + writer.write( "****\n\n" ); } - private static void writeSettingName(SettingDescriptor settingDescriptor, FileWriter writer) throws IOException { - writer.write( "`" ); - if ( settingDescriptor.getLifecycleDetails().isDeprecated() ) { - writer.write( "[.line-through]#" ); - } - else { - writer.write( '*' ); - } - - writer.write( settingDescriptor.getName() ); - + private static String settingName(SettingDescriptor settingDescriptor) { if ( settingDescriptor.getLifecycleDetails().isDeprecated() ) { - writer.write( '#' ); + return String.format( + Locale.ROOT, + "`[.line-through]#%s#`", + settingDescriptor.getName() + ); } else { - writer.write( '*' ); + return String.format( + Locale.ROOT, + "`%s`", + settingDescriptor.getName() + ); } - writer.write( '`' ); } private static void tryToWriteLine(Writer writer, String prefix, String value, String... other) { diff --git a/local-build-plugins/src/main/java/org/hibernate/orm/properties/SettingDescriptor.java b/local-build-plugins/src/main/java/org/hibernate/orm/properties/SettingDescriptor.java index 754512bc28ce..03ed31308893 100644 --- a/local-build-plugins/src/main/java/org/hibernate/orm/properties/SettingDescriptor.java +++ b/local-build-plugins/src/main/java/org/hibernate/orm/properties/SettingDescriptor.java @@ -18,6 +18,8 @@ public class SettingDescriptor { private final String defaultValue; private final String apiNote; private final LifecycleDetails lifecycleDetails; + private final boolean unsafe; + private final boolean compatibility; public SettingDescriptor( String name, @@ -27,6 +29,8 @@ public SettingDescriptor( String comment, String defaultValue, String apiNote, + boolean unsafe, + boolean compatibility, LifecycleDetails lifecycleDetails) { this.name = name; this.settingsClassName = settingsClassName; @@ -35,6 +39,8 @@ public SettingDescriptor( this.publishedJavadocLink = publishedJavadocLink; this.defaultValue = defaultValue; this.apiNote = apiNote; + this.unsafe = unsafe; + this.compatibility = compatibility; this.lifecycleDetails = lifecycleDetails; } @@ -48,7 +54,9 @@ public SettingDescriptor( String apiNote, String since, boolean deprecated, - boolean incubating) { + boolean incubating, + boolean unsafe, + boolean compatibility) { this( name, settingsClassName, @@ -56,6 +64,8 @@ public SettingDescriptor( publishedJavadocLink, comment, defaultValue, apiNote, + unsafe, + compatibility, new LifecycleDetails( since, deprecated, incubating ) ); } @@ -100,6 +110,14 @@ public String getSettingFieldName() { return settingFieldName; } + public boolean isUnsafe() { + return unsafe; + } + + public boolean isCompatibility() { + return compatibility; + } + public LifecycleDetails getLifecycleDetails() { return lifecycleDetails; } diff --git a/local-build-plugins/src/main/java/org/hibernate/orm/properties/SettingWorkingDetails.java b/local-build-plugins/src/main/java/org/hibernate/orm/properties/SettingWorkingDetails.java index d6dbd0b277d2..6fad3071c648 100644 --- a/local-build-plugins/src/main/java/org/hibernate/orm/properties/SettingWorkingDetails.java +++ b/local-build-plugins/src/main/java/org/hibernate/orm/properties/SettingWorkingDetails.java @@ -23,6 +23,8 @@ public class SettingWorkingDetails { private String since; private boolean deprecated; private boolean incubating; + private boolean unsafe; + private boolean compatibility; private List relatedSettingNames; public SettingWorkingDetails( @@ -92,6 +94,22 @@ public void setIncubating(boolean incubating) { this.incubating = incubating; } + public boolean isUnsafe() { + return unsafe; + } + + public void setUnsafe(boolean unsafe) { + this.unsafe = unsafe; + } + + public boolean isCompatibility() { + return compatibility; + } + + public void setCompatibility(boolean compatibility) { + this.compatibility = compatibility; + } + public List getRelatedSettingNames() { return relatedSettingNames; } @@ -134,7 +152,9 @@ public SettingDescriptor buildDescriptor(String asciidoc) { apiNote, since, deprecated, - incubating + incubating, + unsafe, + compatibility ); } } diff --git a/local-build-plugins/src/main/java/org/hibernate/orm/properties/Utils.java b/local-build-plugins/src/main/java/org/hibernate/orm/properties/Utils.java index 61a7994b7c89..b6e4ce8fcd69 100644 --- a/local-build-plugins/src/main/java/org/hibernate/orm/properties/Utils.java +++ b/local-build-plugins/src/main/java/org/hibernate/orm/properties/Utils.java @@ -14,6 +14,8 @@ import java.util.TreeMap; import java.util.TreeSet; +import org.jsoup.nodes.Element; + /** * @author Steve Ebersole */ @@ -39,4 +41,39 @@ public static Map> createResult } ); return map; } + + public static boolean containsHref(Element fieldJavadocElement, String target) { + final String cssQuery = "a[href$=" + target + "]"; + final Element incubatingMarkerElement = fieldJavadocElement.selectFirst( cssQuery ); + return incubatingMarkerElement != null; + + } + + public static boolean interpretIncubation(Element fieldJavadocElement) { + return containsHref( fieldJavadocElement, "Incubating.html" ); + } + + public static boolean interpretUnsafe(Element fieldJavadocElement) { + return containsHref( fieldJavadocElement, "Unsafe.html" ); + } + + public static boolean interpretCompatibility(Element fieldJavadocElement) { + return containsHref( fieldJavadocElement, "Compatibility.html" ); + } + + public static boolean interpretDeprecation(Element fieldJavadocElement) { + // A setting is considered deprecated with either `@Deprecated` + final Element deprecationDiv = fieldJavadocElement.selectFirst( ".deprecationBlock" ); + // presence of this

    indicates the member is deprecated + if ( deprecationDiv != null ) { + return true; + } + + // or `@Remove` + if ( containsHref( fieldJavadocElement, "Remove.html" ) ) { + return true; + } + + return false; + } } diff --git a/local-build-plugins/src/main/java/org/hibernate/orm/properties/jdk11/DomToAsciidocConverter.java b/local-build-plugins/src/main/java/org/hibernate/orm/properties/jdk11/DomToAsciidocConverter.java index c76853692850..19a8e7b388d5 100644 --- a/local-build-plugins/src/main/java/org/hibernate/orm/properties/jdk11/DomToAsciidocConverter.java +++ b/local-build-plugins/src/main/java/org/hibernate/orm/properties/jdk11/DomToAsciidocConverter.java @@ -93,9 +93,6 @@ private void visitElement(Element element) { } private void visitDiv(Element div) { - if ( converted.length() != 0 ) { - converted.append( "\n+\n" ); - } for ( Node childNode : div.childNodes() ) { visitNode( childNode ); } @@ -129,7 +126,6 @@ private void visitUl(Element ul) { if ( converted.lastIndexOf( "\n" ) != converted.length() - 1 ) { converted.append( '\n' ); } - converted.append( "+\n" ); for ( Node childNode : ul.childNodes() ) { visitNode( childNode ); } diff --git a/local-build-plugins/src/main/java/org/hibernate/orm/properties/jdk11/SettingsCollector.java b/local-build-plugins/src/main/java/org/hibernate/orm/properties/jdk11/SettingsCollector.java index 0115a2c06a9b..9f1e5dc58a9c 100644 --- a/local-build-plugins/src/main/java/org/hibernate/orm/properties/jdk11/SettingsCollector.java +++ b/local-build-plugins/src/main/java/org/hibernate/orm/properties/jdk11/SettingsCollector.java @@ -23,6 +23,10 @@ import org.jsoup.nodes.Element; import org.jsoup.select.Elements; +import static org.hibernate.orm.properties.Utils.interpretCompatibility; +import static org.hibernate.orm.properties.Utils.interpretDeprecation; +import static org.hibernate.orm.properties.Utils.interpretIncubation; +import static org.hibernate.orm.properties.Utils.interpretUnsafe; import static org.hibernate.orm.properties.Utils.packagePrefix; import static org.hibernate.orm.properties.Utils.withoutPackagePrefix; @@ -195,6 +199,8 @@ private static void applyMetadata( settingDetails.setIncubating( interpretIncubation( fieldJavadocElement ) ); settingDetails.setDeprecated( interpretDeprecation( fieldJavadocElement ) ); + settingDetails.setUnsafe( interpretUnsafe( fieldJavadocElement ) ); + settingDetails.setCompatibility( interpretCompatibility( fieldJavadocElement ) ); } private static SettingsDocSection findMatchingDocSection( @@ -245,28 +251,6 @@ private static void processNotes( } } - private static boolean interpretIncubation(Element fieldJavadocElement) { - final Element incubatingMarkerElement = fieldJavadocElement.selectFirst( "[href*=.Incubating.html]" ); - return incubatingMarkerElement != null; - } - - private static boolean interpretDeprecation(Element fieldJavadocElement) { - // A setting is considered deprecated with either `@Deprecated` - final Element deprecationDiv = fieldJavadocElement.selectFirst( ".deprecationBlock" ); - // presence of this
    indicates the member is deprecated - if ( deprecationDiv != null ) { - return true; - } - - // or `@Remove` - final Element removeMarkerElement = fieldJavadocElement.selectFirst( "[href*=.Remove.html]" ); - if ( removeMarkerElement != null ) { - return true; - } - - return false; - } - private static Elements cleanupFieldJavadocElement( Element fieldJavadocElement, String className, diff --git a/local-build-plugins/src/main/java/org/hibernate/orm/properties/jdk17/JavadocToAsciidocConverter.java b/local-build-plugins/src/main/java/org/hibernate/orm/properties/jdk17/JavadocToAsciidocConverter.java index def21e395c91..456cacffbce4 100644 --- a/local-build-plugins/src/main/java/org/hibernate/orm/properties/jdk17/JavadocToAsciidocConverter.java +++ b/local-build-plugins/src/main/java/org/hibernate/orm/properties/jdk17/JavadocToAsciidocConverter.java @@ -195,12 +195,9 @@ private void visitElement(Element element) { } private void visitDiv(Element div) { - if ( converted.length() != 0 ) { - converted.append( "\n+\n" ); - } boolean deprecation = div.hasClass( "deprecationBlock" ); if ( deprecation ) { - converted.append( "+\n[WARNING]\n====\n" ); + converted.append( "[WARNING]\n====\n" ); } for ( Node childNode : div.childNodes() ) { visitNode( childNode ); @@ -238,7 +235,6 @@ private void visitUl(Element ul) { if ( converted.lastIndexOf( "\n" ) != converted.length() - 1 ) { converted.append( '\n' ); } - converted.append( "+\n" ); for ( Node childNode : ul.childNodes() ) { visitNode( childNode ); } diff --git a/local-build-plugins/src/main/java/org/hibernate/orm/properties/jdk17/SettingsCollector.java b/local-build-plugins/src/main/java/org/hibernate/orm/properties/jdk17/SettingsCollector.java index e5bacf1cd129..b9aea25d99db 100644 --- a/local-build-plugins/src/main/java/org/hibernate/orm/properties/jdk17/SettingsCollector.java +++ b/local-build-plugins/src/main/java/org/hibernate/orm/properties/jdk17/SettingsCollector.java @@ -24,6 +24,10 @@ import org.jsoup.nodes.Element; import org.jsoup.select.Elements; +import static org.hibernate.orm.properties.Utils.interpretCompatibility; +import static org.hibernate.orm.properties.Utils.interpretDeprecation; +import static org.hibernate.orm.properties.Utils.interpretIncubation; +import static org.hibernate.orm.properties.Utils.interpretUnsafe; import static org.hibernate.orm.properties.jdk17.JavadocToAsciidocConverter.convertFieldJavadocHtmlToAsciidoc; /** @@ -160,6 +164,8 @@ private static void applyMetadata(SettingWorkingDetails settingDetails, Element ); settingDetails.setIncubating( interpretIncubation( fieldJavadocElement ) ); settingDetails.setDeprecated( interpretDeprecation( fieldJavadocElement ) ); + settingDetails.setUnsafe( interpretUnsafe( fieldJavadocElement ) ); + settingDetails.setCompatibility( interpretCompatibility( fieldJavadocElement ) ); } public static Document loadConstants(File javadocDirectory) { @@ -244,27 +250,4 @@ private static void processNotes( noteConsumer.consumeNote( dtNode.text().trim(), ddNode.text().trim() ); } } - - private static boolean interpretIncubation(Element fieldJavadocElement) { - final Element incubatingMarkerElement = fieldJavadocElement.selectFirst( "[href*=.Incubating.html]" ); - return incubatingMarkerElement != null; - } - - private static boolean interpretDeprecation(Element fieldJavadocElement) { - // A setting is considered deprecated with either `@Deprecated` - final Element deprecationDiv = fieldJavadocElement.selectFirst( ".deprecation-block" ); - // presence of this
    indicates the member is deprecated - if ( deprecationDiv != null ) { - return true; - } - - // or `@Remove` - final Element removeMarkerElement = fieldJavadocElement.selectFirst( "[href*=.Remove.html]" ); - if ( removeMarkerElement != null ) { - return true; - } - - return false; - } - } diff --git a/migration-guide.adoc b/migration-guide.adoc index c2c366657569..04e931858dc0 100644 --- a/migration-guide.adoc +++ b/migration-guide.adoc @@ -105,6 +105,32 @@ The behaviour of `jakarta.persistence.criteria.Expression#as(Class)` has been ch In order to perform an actual typecast, `org.hibernate.query.criteria.JpaExpression#cast(Class)` can be used. E.g. -``` + +[source,java] +---- ( (JpaExpression) from.get( "theInt" ) ).cast( String.class ) -``` +---- + + +[[table-annotation-subclasses]] +== @Table and SINGLE_TABLE inheritance + +In previous versions, `@Table` on a subclass in a SINGLE_TABLE hierarchy was simply ignored. E.g. + +[source,java] +---- +@Entity +@Inherited +class RootClass { + // ... +} +@Entity +@Table(...) +class SubClass extends RootClass { + // ... +} +---- + +As part of a larger effort toward better annotation validation, this now results in a mapping exception. +All classes in the hierarchy are stored to a single table as defined by the root. `@Table` on the subclasses is, +at best, confusing. diff --git a/nightly.Jenkinsfile b/nightly.Jenkinsfile deleted file mode 100644 index 54fc0f6a48c5..000000000000 --- a/nightly.Jenkinsfile +++ /dev/null @@ -1,357 +0,0 @@ -/* - * Hibernate, Relational Persistence for Idiomatic Java - * - * License: GNU Lesser General Public License (LGPL), version 2.1 or later. - * See the lgpl.txt file in the root directory or . - */ - -import groovy.transform.Field -import io.jenkins.blueocean.rest.impl.pipeline.PipelineNodeGraphVisitor -import io.jenkins.blueocean.rest.impl.pipeline.FlowNodeWrapper -import org.jenkinsci.plugins.workflow.support.steps.build.RunWrapper - -/* - * See https://github.com/hibernate/hibernate-jenkins-pipeline-helpers - */ -@Library('hibernate-jenkins-pipeline-helpers@1.13') _ -import org.hibernate.jenkins.pipeline.helpers.job.JobHelper - -@Field final String DEFAULT_JDK_VERSION = '11' -@Field final String DEFAULT_JDK_TOOL = "OpenJDK ${DEFAULT_JDK_VERSION} Latest" -@Field final String NODE_PATTERN_BASE = 'Worker&&Containers' -@Field List environments - -this.helper = new JobHelper(this) - -helper.runWithNotification { -stage('Configure') { - this.environments = [ - // Minimum supported versions - new BuildEnvironment( dbName: 'hsqldb_2_6' ), - new BuildEnvironment( dbName: 'mysql_8_0' ), - new BuildEnvironment( dbName: 'mariadb_10_4' ), - new BuildEnvironment( dbName: 'postgresql_12' ), - new BuildEnvironment( dbName: 'edb_12' ), - new BuildEnvironment( dbName: 'db2_10_5', longRunning: true ), - new BuildEnvironment( dbName: 'mssql_2017' ), // Unfortunately there is no SQL Server 2008 image, so we have to test with 2017 -// new BuildEnvironment( dbName: 'sybase_16' ), // There only is a Sybase ASE 16 image, so no pint in testing that nightly - new BuildEnvironment( dbName: 'sybase_jconn' ), - // Long running databases - new BuildEnvironment( dbName: 'cockroachdb', node: 'cockroachdb', longRunning: true ), - new BuildEnvironment( dbName: 'hana_cloud', dbLockableResource: 'hana-cloud', dbLockResourceAsHost: true ) - ]; - - helper.configure { - file 'job-configuration.yaml' - // We don't require the following, but the build helper plugin apparently does - jdk { - defaultTool DEFAULT_JDK_TOOL - } - maven { - defaultTool 'Apache Maven 3.8' - } - } - properties([ - buildDiscarder( - logRotator(daysToKeepStr: '30', numToKeepStr: '10') - ), - rateLimitBuilds(throttle: [count: 1, durationName: 'day', userBoost: true]), - // If two builds are about the same branch or pull request, - // the older one will be aborted when the newer one starts. - disableConcurrentBuilds(abortPrevious: true), - helper.generateNotificationProperty() - ]) -} - -// Avoid running the pipeline on branch indexing -if (currentBuild.getBuildCauses().toString().contains('BranchIndexingCause')) { - print "INFO: Build skipped due to trigger being Branch Indexing" - currentBuild.result = 'NOT_BUILT' - return -} - -stage('Build') { - Map executions = [:] - Map> state = [:] - environments.each { BuildEnvironment buildEnv -> - // Don't build environments for newer JDKs when this is a PR - if ( helper.scmSource.pullRequest && buildEnv.testJdkVersion ) { - return - } - state[buildEnv.tag] = [:] - executions.put(buildEnv.tag, { - runBuildOnNode(buildEnv.node ?: NODE_PATTERN_BASE) { - def testJavaHome - if ( buildEnv.testJdkVersion ) { - testJavaHome = tool(name: "OpenJDK ${buildEnv.testJdkVersion} Latest", type: 'jdk') - } - def javaHome = tool(name: DEFAULT_JDK_TOOL, type: 'jdk') - // Use withEnv instead of setting env directly, as that is global! - // See https://github.com/jenkinsci/pipeline-plugin/blob/master/TUTORIAL.md - withEnv(["JAVA_HOME=${javaHome}", "PATH+JAVA=${javaHome}/bin"]) { - state[buildEnv.tag]['additionalOptions'] = '' - if ( testJavaHome ) { - state[buildEnv.tag]['additionalOptions'] = state[buildEnv.tag]['additionalOptions'] + - " -Ptest.jdk.version=${buildEnv.testJdkVersion} -Porg.gradle.java.installations.paths=${javaHome},${testJavaHome}" - } - if ( buildEnv.testJdkLauncherArgs ) { - state[buildEnv.tag]['additionalOptions'] = state[buildEnv.tag]['additionalOptions'] + - " -Ptest.jdk.launcher.args=${buildEnv.testJdkLauncherArgs}" - } - state[buildEnv.tag]['containerName'] = null; - stage('Checkout') { - checkout scm - } - tryFinally({ - stage('Start database') { - switch (buildEnv.dbName) { - case "hsqldb_2_6": - state[buildEnv.tag]['additionalOptions'] = state[buildEnv.tag]['additionalOptions'] + - " -Pgradle.libs.versions.hsqldb=2.6.1" - break; - case "mysql_8_0": - docker.withRegistry('https://index.docker.io/v1/', 'hibernateci.hub.docker.com') { - docker.image('mysql:8.0.31').pull() - } - sh "./docker_db.sh mysql_8_0" - state[buildEnv.tag]['containerName'] = "mysql" - break; - case "mariadb_10_4": - docker.withRegistry('https://index.docker.io/v1/', 'hibernateci.hub.docker.com') { - docker.image('mariadb:10.4.31').pull() - } - sh "./docker_db.sh mariadb_10_4" - state[buildEnv.tag]['containerName'] = "mariadb" - break; - case "postgresql_12": - // use the postgis image to enable the PGSQL GIS (spatial) extension - docker.withRegistry('https://index.docker.io/v1/', 'hibernateci.hub.docker.com') { - docker.image('postgis/postgis:12-3.4').pull() - } - sh "./docker_db.sh postgresql_12" - state[buildEnv.tag]['containerName'] = "postgres" - break; - case "edb_12": - docker.image('quay.io/enterprisedb/edb-postgres-advanced:12.16-3.3-postgis').pull() - sh "./docker_db.sh edb_12" - state[buildEnv.tag]['containerName'] = "edb" - break; - case "db2_10_5": - docker.withRegistry('https://index.docker.io/v1/', 'hibernateci.hub.docker.com') { - docker.image('ibmoms/db2express-c@sha256:a499afd9709a1f69fb41703e88def9869955234c3525547e2efc3418d1f4ca2b').pull() - } - sh "./docker_db.sh db2_10_5" - state[buildEnv.tag]['containerName'] = "db2" - break; - case "mssql_2017": - docker.image('mcr.microsoft.com/mssql/server@sha256:7d194c54e34cb63bca083542369485c8f4141596805611e84d8c8bab2339eede').pull() - sh "./docker_db.sh mssql_2017" - state[buildEnv.tag]['containerName'] = "mssql" - break; - case "sybase_jconn": - docker.withRegistry('https://index.docker.io/v1/', 'hibernateci.hub.docker.com') { - docker.image('nguoianphu/docker-sybase').pull() - } - sh "./docker_db.sh sybase" - state[buildEnv.tag]['containerName'] = "sybase" - break; - case "cockroachdb": - docker.withRegistry('https://index.docker.io/v1/', 'hibernateci.hub.docker.com') { - docker.image('cockroachdb/cockroach:v23.1.12').pull() - } - sh "./docker_db.sh cockroachdb" - state[buildEnv.tag]['containerName'] = "cockroach" - break; - } - } - stage('Test') { - String args = "${buildEnv.additionalOptions ?: ''} ${state[buildEnv.tag]['additionalOptions'] ?: ''}" - withEnv(["RDBMS=${buildEnv.dbName}"]) { - tryFinally({ - if (buildEnv.dbLockableResource == null) { - withCredentials([file(credentialsId: 'sybase-jconnect-driver', variable: 'jconnect_driver')]) { - sh 'cp -f $jconnect_driver ./drivers/jconn4.jar' - timeout( [time: buildEnv.longRunning ? 480 : 120, unit: 'MINUTES'] ) { - ciBuild buildEnv, args - } - } - } - else { - lock(label: buildEnv.dbLockableResource, quantity: 1, variable: 'LOCKED_RESOURCE') { - if ( buildEnv.dbLockResourceAsHost ) { - args += " -DdbHost=${LOCKED_RESOURCE}" - } - timeout( [time: buildEnv.longRunning ? 480 : 120, unit: 'MINUTES'] ) { - ciBuild buildEnv, args - } - } - } - }, { // Finally - junit '**/target/test-results/test/*.xml,**/target/test-results/testKitTest/*.xml' - }) - } - } - }, { // Finally - if ( state[buildEnv.tag]['containerName'] != null ) { - sh "docker rm -f ${state[buildEnv.tag]['containerName']}" - } - // Skip this for PRs - if ( !env.CHANGE_ID && buildEnv.notificationRecipients != null ) { - handleNotifications(currentBuild, buildEnv) - } - }) - } - } - }) - } - parallel(executions) -} - -} // End of helper.runWithNotification - -// Job-specific helpers - -class BuildEnvironment { - String testJdkVersion - String testJdkLauncherArgs - String dbName = 'h2' - String node - String dbLockableResource - boolean dbLockResourceAsHost - String additionalOptions - String notificationRecipients - boolean longRunning - - String toString() { getTag() } - String getTag() { "${node ? node + "_" : ''}${testJdkVersion ? 'jdk_' + testJdkVersion + '_' : '' }${dbName}" } -} - -void runBuildOnNode(String label, Closure body) { - node( label ) { - pruneDockerContainers() - tryFinally(body, { - // If this is a PR, we clean the workspace at the end - if ( env.CHANGE_BRANCH != null ) { - cleanWs() - } - pruneDockerContainers() - }) - } -} - -void ciBuild(buildEnv, String args) { - // On untrusted nodes, we use the same access key as for PRs: - // it has limited access, essentially it can only push build scans. - def develocityCredentialsId = buildEnv.node ? 'ge.hibernate.org-access-key-pr' : 'ge.hibernate.org-access-key' - - withCredentials([string(credentialsId: develocityCredentialsId, - variable: 'DEVELOCITY_ACCESS_KEY')]) { - withGradle { // withDevelocity, actually: https://plugins.jenkins.io/gradle/#plugin-content-capturing-build-scans-from-jenkins-pipeline - sh "./ci/build.sh $args" - } - } -} - -void pruneDockerContainers() { - if ( !sh( script: 'command -v docker || true', returnStdout: true ).trim().isEmpty() ) { - sh 'docker container prune -f || true' - sh 'docker image prune -f || true' - sh 'docker network prune -f || true' - sh 'docker volume prune -f || true' - } -} - -void handleNotifications(currentBuild, buildEnv) { - def currentResult = getParallelResult(currentBuild, buildEnv.tag) - boolean success = currentResult == 'SUCCESS' || currentResult == 'UNKNOWN' - def previousResult = currentBuild.previousBuild == null ? null : getParallelResult(currentBuild.previousBuild, buildEnv.tag) - - // Ignore success after success - if ( !( success && previousResult == 'SUCCESS' ) ) { - def subject - def body - if ( success ) { - if ( previousResult != 'SUCCESS' && previousResult != null ) { - subject = "${env.JOB_NAME} - Build ${env.BUILD_NUMBER} - Fixed" - body = """

    ${env.JOB_NAME} - Build ${env.BUILD_NUMBER} - Fixed:

    -

    Check console output at ${env.BUILD_URL} to view the results.

    """ - } - else { - subject = "${env.JOB_NAME} - Build ${env.BUILD_NUMBER} - Success" - body = """

    ${env.JOB_NAME} - Build ${env.BUILD_NUMBER} - Success:

    -

    Check console output at ${env.BUILD_URL} to view the results.

    """ - } - } - else if (currentBuild.rawBuild.getActions(jenkins.model.InterruptedBuildAction.class).isEmpty()) { - // If there are interrupted build actions, this means the build was cancelled, probably superseded - // Thanks to https://issues.jenkins.io/browse/JENKINS-43339 for the "hack" to determine this - if ( currentResult == 'FAILURE' ) { - if ( previousResult != null && previousResult == "FAILURE" ) { - subject = "${env.JOB_NAME} - Build ${env.BUILD_NUMBER} - Still failing" - body = """

    ${env.JOB_NAME} - Build ${env.BUILD_NUMBER} - Still failing:

    -

    Check console output at ${env.BUILD_URL} to view the results.

    """ - } - else { - subject = "${env.JOB_NAME} - Build ${env.BUILD_NUMBER} - Failure" - body = """

    ${env.JOB_NAME} - Build ${env.BUILD_NUMBER} - Failure:

    -

    Check console output at ${env.BUILD_URL} to view the results.

    """ - } - } - else { - subject = "${env.JOB_NAME} - Build ${env.BUILD_NUMBER} - ${currentResult}" - body = """

    ${env.JOB_NAME} - Build ${env.BUILD_NUMBER} - ${currentResult}:

    -

    Check console output at ${env.BUILD_URL} to view the results.

    """ - } - } - - emailext( - subject: subject, - body: body, - to: buildEnv.notificationRecipients - ) - } -} - -@NonCPS -String getParallelResult( RunWrapper build, String parallelBranchName ) { - def visitor = new PipelineNodeGraphVisitor( build.rawBuild ) - def branch = visitor.pipelineNodes.find{ it.type == FlowNodeWrapper.NodeType.PARALLEL && parallelBranchName == it.displayName } - if ( branch == null ) { - echo "Couldn't find parallel branch name '$parallelBranchName'. Available parallel branch names:" - visitor.pipelineNodes.findAll{ it.type == FlowNodeWrapper.NodeType.PARALLEL }.each{ - echo " - ${it.displayName}" - } - return null; - } - return branch.status.result -} - -// try-finally construct that properly suppresses exceptions thrown in the finally block. -def tryFinally(Closure main, Closure ... finallies) { - def mainFailure = null - try { - main() - } - catch (Throwable t) { - mainFailure = t - throw t - } - finally { - finallies.each {it -> - try { - it() - } - catch (Throwable t) { - if ( mainFailure ) { - mainFailure.addSuppressed( t ) - } - else { - mainFailure = t - } - } - } - } - if ( mainFailure ) { // We may reach here if only the "finally" failed - throw mainFailure - } -} diff --git a/release/release.gradle b/release/release.gradle index e5b07b329998..ceca2cab6638 100644 --- a/release/release.gradle +++ b/release/release.gradle @@ -481,14 +481,14 @@ class ChangeLogFile { } private static getVersionId(jsonReleaseNotes, String restReleaseVersion) { - def fixVersions = jsonReleaseNotes.issues.get( 0 ).fields.fixVersions - - for ( def fixVersion : fixVersions ) { - if ( fixVersion.name.equals( restReleaseVersion ) ) { - return fixVersion.id + for ( def issue : jsonReleaseNotes.issues ) { + for ( def fixVersion : issue.fields.fixVersions ) { + if ( fixVersion.name == restReleaseVersion ) { + return fixVersion.id + } } } - throw new GradleException( "Unable to determine the version id of the current release." ) + throw new GradleException("No issues found for current release version (" + restReleaseVersion + "), aborting.") } } diff --git a/settings.gradle b/settings.gradle index 6f3436e1b724..a6a5c3c4e9af 100644 --- a/settings.gradle +++ b/settings.gradle @@ -68,10 +68,12 @@ dependencyResolutionManagement { } libs { def antlrVersion = version "antlr", "4.13.0" - def byteBuddyVersion = version "byteBuddy", "1.14.18" + // WARNING: When upgrading to a version of bytebuddy that supports a new bytecode version, + // make sure to remove the now unnecessary net.bytebuddy.experimental=true in relevant CI jobs (Jenkinsfile). + def byteBuddyVersion = version "byteBuddy", "1.15.11" def classmateVersion = version "classmate", "1.5.1" def geolatteVersion = version "geolatte", "1.9.1" - def hcannVersion = version "hcann", "7.0.1.Final" + def hcannVersion = version "hcann", "7.0.3.Final" def jandexVersion = version "jandex", "3.2.0" def jacksonVersion = version "jackson", "2.17.0" def jbossLoggingVersion = version "jbossLogging", "3.5.0.Final" @@ -300,7 +302,7 @@ buildCache { enabled = !settings.ext.isCiEnvironment } remote(develocity.buildCache) { - enabled = true + enabled = settings.ext.useRemoteCache // Check access key presence to avoid build cache errors on PR builds when access key is not present def accessKey = System.getenv("DEVELOCITY_ACCESS_KEY") push = settings.ext.populateRemoteBuildCache && accessKey diff --git a/tooling/hibernate-gradle-plugin/hibernate-gradle-plugin.gradle b/tooling/hibernate-gradle-plugin/hibernate-gradle-plugin.gradle index 551820769781..a6b783bee5eb 100644 --- a/tooling/hibernate-gradle-plugin/hibernate-gradle-plugin.gradle +++ b/tooling/hibernate-gradle-plugin/hibernate-gradle-plugin.gradle @@ -23,10 +23,7 @@ apply from: rootProject.file( 'gradle/releasable.gradle' ) description = "Gradle plugin for integrating Hibernate aspects into your build" -ext { - pluginId = 'org.hibernate.orm' - pluginVersion = project.version -} +def pluginId = 'org.hibernate.orm' dependencies { implementation project(':hibernate-core') @@ -50,7 +47,7 @@ dependencies { gradlePlugin { plugins { ormPlugin { - id = project.pluginId + id = pluginId implementationClass = 'org.hibernate.orm.tooling.gradle.HibernateOrmPlugin' } } @@ -70,7 +67,7 @@ pluginBundle { plugins { ormPlugin { - id = project.pluginId + id = pluginId displayName = 'Gradle plugin for Hibernate ORM' description = 'Applies Hibernate aspects into the build' } @@ -100,21 +97,6 @@ publishing { } } -// local publishing (SNAPSHOT testing), cont. -// - https://github.com/gradle-nexus/publish-plugin/issues/143 -// - https://github.com/gradle-nexus/publish-plugin/pull/144 -gradle.taskGraph.whenReady { - tasks.withType(PublishToMavenRepository) { PublishToMavenRepository t -> - if ( t.repository == null ) { - logger.info( "Task `{}` had null repository", t.path ) - } - else if ( t.repository.name == "sonatype" ) { - logger.debug( "Disabling task `{}` because it publishes to Sonatype", t.path ) - t.enabled = false - } - } -} - processResources { inputs.property( "orm-version", getVersion() ) description = description + " (injected with Hibernate version)" @@ -153,20 +135,38 @@ tasks.publish.enabled !project.ormVersion.isSnapshot tasks.publishPlugins.enabled !project.ormVersion.isSnapshot gradle.taskGraph.whenReady { tg -> - if ( tg.hasTask( project.tasks.publishPlugins ) && project.tasks.publishPlugins.enabled ) { - // look for sys-prop or env-var overrides of the tokens used for publishing - if ( project.properties.containsKey( 'gradle.publish.key' ) - || project.properties.containsKey( 'gradle.publish.secret' ) ) { - // nothing to do - already explicitly set - } - else { - // use the values from the credentials provider, if any - if ( project.property( 'gradle.publish.key' ) == null ) { - throw new RuntimeException( "`-Pgradle.publish.key=...` not found" ) + // verify credentials for publishing the plugin up front to avoid any work (only if we are publishing) + if ( tg.hasTask( ":publishPlugins" ) && project.tasks.publishPlugins.enabled ) { + // we are publishing the plugin - make sure there is a credentials pair + // + // first, check the `GRADLE_PUBLISH_KEY` / `GRADLE_PUBLISH_SECRET` combo (env vars) + // and then the `gradle.publish.key` / `gradle.publish.secret` combo (project prop) + // - see https://docs.gradle.org/current/userguide/publishing_gradle_plugins.html#account_setup + if ( System.getenv().get("GRADLE_PUBLISH_KEY") != null ) { + if ( System.getenv().get("GRADLE_PUBLISH_SECRET") != null ) { + throw new RuntimeException( "`GRADLE_PUBLISH_KEY` specified, but not `GRADLE_PUBLISH_SECRET` for publishing Gradle plugin" ) } - if ( project.property( 'gradle.publish.secret' ) == null ) { - throw new RuntimeException( "`-Pgradle.publish.secret=...` not found" ) + } + else if ( project.findProperty( 'gradle.publish.key' ) != null ) { + if ( project.findProperty( 'gradle.publish.secret' ) != null ) { + throw new RuntimeException( "`gradle.publish.key` specified, but not `gradle.publish.secret` for publishing Gradle plugin" ) } } + else { + throw new RuntimeException( "No credentials specified for publishing Gradle plugin" ) + } + } + + // local publishing (SNAPSHOT testing), cont. + // - https://github.com/gradle-nexus/publish-plugin/issues/143 + // - https://github.com/gradle-nexus/publish-plugin/pull/144 + tasks.withType(PublishToMavenRepository) { PublishToMavenRepository t -> + if ( t.repository == null ) { + logger.info( "Task `{}` had null repository", t.path ) + } + else if ( t.repository.name == "staging" || t.repository.name == "snapshots" ) { + logger.debug( "Disabling task `{}` because it publishes to remote Maven repository", t.path ) + t.enabled = false + } } -} \ No newline at end of file +} diff --git a/tooling/hibernate-gradle-plugin/src/test/java/org/hibernate/orm/tooling/gradle/ModuleInfoProjectTests.java b/tooling/hibernate-gradle-plugin/src/test/java/org/hibernate/orm/tooling/gradle/ModuleInfoProjectTests.java new file mode 100644 index 000000000000..a926488862f1 --- /dev/null +++ b/tooling/hibernate-gradle-plugin/src/test/java/org/hibernate/orm/tooling/gradle/ModuleInfoProjectTests.java @@ -0,0 +1,53 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.tooling.gradle; + +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + + +/** + * Basic functional tests + * + * @author Steve Ebersole + */ +class ModuleInfoProjectTests extends TestsBase { + + @Override + protected String getProjectName() { + return "simple-moduleinfo"; + } + + @Override + protected String getSourceSetName() { + return "main"; + } + + @Override + protected String getLanguageName() { + return "java"; + } + + @Override + protected String getCompileTaskName() { + return "compileJava"; + } + + @Test + @Override + public void testEnhancement(@TempDir Path projectDir) throws Exception { + super.testEnhancement( projectDir ); + } + + @Test + @Override + public void testEnhancementUpToDate(@TempDir Path projectDir) throws Exception { + super.testEnhancementUpToDate( projectDir ); + } +} diff --git a/tooling/hibernate-gradle-plugin/src/test/resources/projects/simple-moduleinfo/build.gradle b/tooling/hibernate-gradle-plugin/src/test/resources/projects/simple-moduleinfo/build.gradle new file mode 100644 index 000000000000..33a514518a87 --- /dev/null +++ b/tooling/hibernate-gradle-plugin/src/test/resources/projects/simple-moduleinfo/build.gradle @@ -0,0 +1,40 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ + +plugins { + id 'java' + id 'org.hibernate.orm' +} + +repositories { + mavenCentral() + + maven { + name 'jboss-snapshots-repository' + url 'https://repository.jboss.org/nexus/content/repositories/snapshots' + } +} + +dependencies { + // NOTE : The version used here is irrelevant in terms of testing the plugin. + // We just need a resolvable version + implementation 'org.hibernate.orm:hibernate-core:6.1.0.Final' + implementation 'jakarta.annotation:jakarta.annotation-api:2.1.1' +} + +hibernate { + useSameVersion = false + enhancement { + enableLazyInitialization.set(true) + lazyInitialization = true + + enableDirtyTracking.set(true) + dirtyTracking = true + + enableExtendedEnhancement.set(true) + } +} diff --git a/tooling/hibernate-gradle-plugin/src/test/resources/projects/simple-moduleinfo/settings.gradle b/tooling/hibernate-gradle-plugin/src/test/resources/projects/simple-moduleinfo/settings.gradle new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tooling/hibernate-gradle-plugin/src/test/resources/projects/simple-moduleinfo/src/main/java/TheEmbeddable.java b/tooling/hibernate-gradle-plugin/src/test/resources/projects/simple-moduleinfo/src/main/java/TheEmbeddable.java new file mode 100644 index 000000000000..25b8849aac2c --- /dev/null +++ b/tooling/hibernate-gradle-plugin/src/test/resources/projects/simple-moduleinfo/src/main/java/TheEmbeddable.java @@ -0,0 +1,29 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +import jakarta.persistence.Embeddable; + +@Embeddable +public class TheEmbeddable { + private String valueOne; + private String valueTwo; + + public String getValueOne() { + return valueOne; + } + + public void setValueOne(String valueOne) { + this.valueOne = valueOne; + } + + public String getValueTwo() { + return valueTwo; + } + + public void setValueTwo(String valueTwo) { + this.valueTwo = valueTwo; + } +} diff --git a/tooling/hibernate-gradle-plugin/src/test/resources/projects/simple-moduleinfo/src/main/java/TheEntity.java b/tooling/hibernate-gradle-plugin/src/test/resources/projects/simple-moduleinfo/src/main/java/TheEntity.java new file mode 100644 index 000000000000..b5d9eb5eead4 --- /dev/null +++ b/tooling/hibernate-gradle-plugin/src/test/resources/projects/simple-moduleinfo/src/main/java/TheEntity.java @@ -0,0 +1,88 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; + +import org.hibernate.annotations.BatchSize; + +import java.util.Set; + +@Entity +@BatchSize( size = 20 ) +public class TheEntity { + @Id + private Integer id; + private String name; + + @Embedded + private TheEmbeddable theEmbeddable; + + @ManyToOne + @JoinColumn + private TheEntity theManyToOne; + + @OneToMany( mappedBy = "theManyToOne" ) + private Set theOneToMany; + + @ElementCollection + @JoinColumn( name = "owner_id" ) + private Set theEmbeddableCollection; + + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public TheEmbeddable getTheEmbeddable() { + return theEmbeddable; + } + + public void setTheEmbeddable(TheEmbeddable theEmbeddable) { + this.theEmbeddable = theEmbeddable; + } + + public TheEntity getTheManyToOne() { + return theManyToOne; + } + + public void setTheManyToOne(TheEntity theManyToOne) { + this.theManyToOne = theManyToOne; + } + + public Set getTheOneToMany() { + return theOneToMany; + } + + public void setTheOneToMany(Set theOneToMany) { + this.theOneToMany = theOneToMany; + } + + public Set getTheEmbeddableCollection() { + return theEmbeddableCollection; + } + + public void setTheEmbeddableCollection(Set theEmbeddableCollection) { + this.theEmbeddableCollection = theEmbeddableCollection; + } +} diff --git a/tooling/hibernate-gradle-plugin/src/test/resources/projects/simple-moduleinfo/src/main/java/module-info.java b/tooling/hibernate-gradle-plugin/src/test/resources/projects/simple-moduleinfo/src/main/java/module-info.java new file mode 100644 index 000000000000..ea488cd33f8f --- /dev/null +++ b/tooling/hibernate-gradle-plugin/src/test/resources/projects/simple-moduleinfo/src/main/java/module-info.java @@ -0,0 +1,5 @@ +module test { + requires org.hibernate.orm.core; + requires jakarta.persistence; + requires jakarta.annotation; +} diff --git a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/basic/Concrete.java b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/basic/Concrete.java index 049a4c5f92f9..af416bc08fb0 100644 --- a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/basic/Concrete.java +++ b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/basic/Concrete.java @@ -3,5 +3,5 @@ import jakarta.data.repository.Repository; @Repository -public interface Concrete extends IdOperations { +public interface Concrete extends IdOperations { } diff --git a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/basic/DataTest.java b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/basic/DataTest.java index 7fcf07a4c89a..5637f5977e5e 100644 --- a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/basic/DataTest.java +++ b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/basic/DataTest.java @@ -19,7 +19,7 @@ */ public class DataTest extends CompilationTest { @Test - @WithClasses({ Author.class, Book.class, BookAuthorRepository.class, IdOperations.class, Concrete.class }) + @WithClasses({ Author.class, Book.class, BookAuthorRepository.class, IdOperations.class, Concrete.class, Thing.class }) public void test() { System.out.println( getMetaModelSourceAsString( Author.class ) ); System.out.println( getMetaModelSourceAsString( Book.class ) ); diff --git a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/basic/IdOperations.java b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/basic/IdOperations.java index 0f45d49beb38..0525ad8632a7 100644 --- a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/basic/IdOperations.java +++ b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/basic/IdOperations.java @@ -23,11 +23,6 @@ import jakarta.data.Sort; import jakarta.data.repository.Query; -/** - * This interface contains common operations for the NaturalNumbers and AsciiCharacters repositories. - * - * @param type of entity. - */ public interface IdOperations { @Query("where id(this) between ?1 and ?2") Stream findByIdBetween(long minimum, long maximum, Sort sort); diff --git a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/basic/Thing.java b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/basic/Thing.java new file mode 100644 index 000000000000..5181e9babd35 --- /dev/null +++ b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/basic/Thing.java @@ -0,0 +1,9 @@ +package org.hibernate.processor.test.data.basic; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + +@Entity +public class Thing { + @Id long id; +} diff --git a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/eg/Bookshop.java b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/eg/Bookshop.java index e57f661dd380..583f29f0281f 100644 --- a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/eg/Bookshop.java +++ b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/eg/Bookshop.java @@ -1,5 +1,6 @@ package org.hibernate.processor.test.data.eg; +import jakarta.annotation.Nonnull; import jakarta.data.repository.CrudRepository; import jakarta.data.repository.Find; import jakarta.data.repository.Query; @@ -10,12 +11,17 @@ import java.util.List; +import static jakarta.transaction.Transactional.TxType.REQUIRES_NEW; + @Repository public interface Bookshop extends CrudRepository { @Find - @Transactional + @Transactional(REQUIRES_NEW) List byPublisher(String publisher_name); + @Find + List byTitle(@Nonnull String title); + @Query("select isbn where title like ?1 order by isbn") String[] ssns(@NotBlank String title); @@ -25,6 +31,9 @@ public interface Bookshop extends CrudRepository { @Query("select count(this) where this.title like ?1 order by this.isbn") long count2(String title); + @Query("select length(text) where title = ?1") + int length(@Nonnull String title); + @Query("select count(this)") long countAll(); diff --git a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/embeddedid/EmbeddedIdTest.java b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/embeddedid/EmbeddedIdTest.java new file mode 100644 index 000000000000..fd5d54cd7603 --- /dev/null +++ b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/embeddedid/EmbeddedIdTest.java @@ -0,0 +1,26 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.processor.test.data.embeddedid; + +import org.hibernate.processor.test.util.CompilationTest; +import org.hibernate.processor.test.util.WithClasses; +import org.junit.Test; + +import static org.hibernate.processor.test.util.TestUtil.assertMetamodelClassGeneratedFor; +import static org.hibernate.processor.test.util.TestUtil.getMetaModelSourceAsString; + +/** + * @author Gavin King + */ +public class EmbeddedIdTest extends CompilationTest { + @Test + @WithClasses({ Thing.class, ThingRepo.class }) + public void test() { + System.out.println( getMetaModelSourceAsString( ThingRepo.class ) ); + assertMetamodelClassGeneratedFor( Thing.class, true ); + assertMetamodelClassGeneratedFor( Thing.class ); + assertMetamodelClassGeneratedFor( ThingRepo.class ); + } +} diff --git a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/embeddedid/Thing.java b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/embeddedid/Thing.java new file mode 100644 index 000000000000..c145b90cf0c4 --- /dev/null +++ b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/embeddedid/Thing.java @@ -0,0 +1,20 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.processor.test.data.embeddedid; + +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; + +@Entity +public class Thing { + @Embeddable + public static class Id { + long id; + String key; + } + + @EmbeddedId Id id; +} diff --git a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/embeddedid/ThingRepo.java b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/embeddedid/ThingRepo.java new file mode 100644 index 000000000000..563a97e5a940 --- /dev/null +++ b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/embeddedid/ThingRepo.java @@ -0,0 +1,15 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.processor.test.data.embeddedid; + +import jakarta.data.repository.CrudRepository; +import jakarta.data.repository.Find; +import jakarta.data.repository.Repository; + +@Repository +public interface ThingRepo extends CrudRepository { + @Find + Thing thing(Thing.Id id); +} diff --git a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/selectenumproperty/Topic.java b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/selectenumproperty/Topic.java new file mode 100644 index 000000000000..7d86f0c0f4e9 --- /dev/null +++ b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/selectenumproperty/Topic.java @@ -0,0 +1,21 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.processor.test.data.selectenumproperty; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; + +@Entity +public class Topic { + + @Id + @GeneratedValue + private Integer topicId; + + private TopicStatus topicStatus; +} diff --git a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/selectenumproperty/TopicData.java b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/selectenumproperty/TopicData.java new file mode 100644 index 000000000000..1d20af8b056c --- /dev/null +++ b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/selectenumproperty/TopicData.java @@ -0,0 +1,12 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.processor.test.data.selectenumproperty; + + +record TopicData(Integer topicId, TopicStatus topicStatus) { + +} diff --git a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/selectenumproperty/TopicRepository.java b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/selectenumproperty/TopicRepository.java new file mode 100644 index 000000000000..143a12943569 --- /dev/null +++ b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/selectenumproperty/TopicRepository.java @@ -0,0 +1,20 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.processor.test.data.selectenumproperty; + +import jakarta.data.repository.DataRepository; +import jakarta.data.repository.Query; +import jakarta.data.repository.Repository; + +import java.util.List; + +@Repository +public interface TopicRepository extends DataRepository { + + @Query("select topicId, topicStatus from Topic") + List getTopicData(); +} diff --git a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/selectenumproperty/TopicStatus.java b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/selectenumproperty/TopicStatus.java new file mode 100644 index 000000000000..8361b88967df --- /dev/null +++ b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/selectenumproperty/TopicStatus.java @@ -0,0 +1,22 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.processor.test.data.selectenumproperty; + +public enum TopicStatus { + TOPIC_UNLOCKED( 0 ), + TOPIC_LOCKED( 1 ); + + private final int topicStatus; + + TopicStatus(final int topicStatus) { + this.topicStatus = topicStatus; + } + + public int topicStatus() { + return topicStatus; + } +} diff --git a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/selectenumproperty/TopicTypeEnumTest.java b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/selectenumproperty/TopicTypeEnumTest.java new file mode 100644 index 000000000000..bac292994c97 --- /dev/null +++ b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/selectenumproperty/TopicTypeEnumTest.java @@ -0,0 +1,27 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.processor.test.data.selectenumproperty; + +import org.hibernate.processor.test.util.CompilationTest; +import org.hibernate.processor.test.util.WithClasses; +import org.junit.Test; + +import static org.hibernate.processor.test.util.TestUtil.assertMetamodelClassGeneratedFor; +import static org.hibernate.processor.test.util.TestUtil.getMetaModelSourceAsString; + +public class TopicTypeEnumTest extends CompilationTest { + @Test + @WithClasses({Topic.class, TopicRepository.class}) + public void test() { + System.out.println( getMetaModelSourceAsString( Topic.class ) ); + System.out.println( getMetaModelSourceAsString( Topic.class, true ) ); + System.out.println( getMetaModelSourceAsString( TopicRepository.class ) ); + assertMetamodelClassGeneratedFor( Topic.class, true ); + assertMetamodelClassGeneratedFor( Topic.class ); + assertMetamodelClassGeneratedFor( TopicRepository.class ); + } +} diff --git a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/versioned/SpecialVersioned.java b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/versioned/SpecialVersioned.java new file mode 100644 index 000000000000..610c7efb6d6d --- /dev/null +++ b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/versioned/SpecialVersioned.java @@ -0,0 +1,7 @@ +package org.hibernate.processor.test.data.versioned; + +import jakarta.persistence.Entity; + +@Entity +public class SpecialVersioned extends Versioned { +} diff --git a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/versioned/SpecialVersionedRepo.java b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/versioned/SpecialVersionedRepo.java new file mode 100644 index 000000000000..11fb3fdddb68 --- /dev/null +++ b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/versioned/SpecialVersionedRepo.java @@ -0,0 +1,17 @@ +package org.hibernate.processor.test.data.versioned; + +import jakarta.data.repository.Query; +import jakarta.data.repository.Repository; + +@Repository +public interface SpecialVersionedRepo { + @Query("where id(this) = ?1") + SpecialVersioned forId(long id); + + @Query("where id(this) = ?1 and version(this) = ?2") + SpecialVersioned forIdAndVersion(long id, int version); + + @Query("select count(this) from SpecialVersioned") + long count(); + +} diff --git a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/versioned/Versioned.java b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/versioned/Versioned.java new file mode 100644 index 000000000000..61405f61edab --- /dev/null +++ b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/versioned/Versioned.java @@ -0,0 +1,11 @@ +package org.hibernate.processor.test.data.versioned; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Version; + +@Entity +public class Versioned { + @Id long id; + @Version int version; +} diff --git a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/versioned/VersionedRepo.java b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/versioned/VersionedRepo.java new file mode 100644 index 000000000000..df312b541573 --- /dev/null +++ b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/versioned/VersionedRepo.java @@ -0,0 +1,17 @@ +package org.hibernate.processor.test.data.versioned; + +import jakarta.data.repository.Query; +import jakarta.data.repository.Repository; + +@Repository +public interface VersionedRepo { + @Query("where id(this) = ?1") + Versioned forId(long id); + + @Query("where id(this) = ?1 and version(this) = ?2") + Versioned forIdAndVersion(long id, int version); + + @Query("select count(this) from Versioned") + long count(); + +} diff --git a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/versioned/VersionedTest.java b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/versioned/VersionedTest.java new file mode 100644 index 000000000000..fbfc622cc3ee --- /dev/null +++ b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/versioned/VersionedTest.java @@ -0,0 +1,31 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.processor.test.data.versioned; + +import org.hibernate.processor.test.util.CompilationTest; +import org.hibernate.processor.test.util.WithClasses; +import org.junit.Test; + +import static org.hibernate.processor.test.util.TestUtil.assertMetamodelClassGeneratedFor; +import static org.hibernate.processor.test.util.TestUtil.getMetaModelSourceAsString; + +/** + * @author Gavin King + */ +public class VersionedTest extends CompilationTest { + @Test + @WithClasses({ Versioned.class, VersionedRepo.class, SpecialVersioned.class, SpecialVersionedRepo.class }) + public void test() { + System.out.println( getMetaModelSourceAsString( VersionedRepo.class ) ); + assertMetamodelClassGeneratedFor( Versioned.class, true ); + assertMetamodelClassGeneratedFor( Versioned.class ); + assertMetamodelClassGeneratedFor( SpecialVersioned.class, true ); + assertMetamodelClassGeneratedFor( SpecialVersioned.class ); + assertMetamodelClassGeneratedFor( VersionedRepo.class ); + assertMetamodelClassGeneratedFor( SpecialVersionedRepo.class ); + } +} diff --git a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/namedquery/Thing.java b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/namedquery/Thing.java new file mode 100644 index 000000000000..29abb055972d --- /dev/null +++ b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/namedquery/Thing.java @@ -0,0 +1,18 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.processor.test.namedquery; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.NamedQuery; + +@NamedQuery(name = "#things", + query = "from Thing where name like :name") +@Entity +public class Thing { + @Id @GeneratedValue long id; + String name; +} diff --git a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/namedquery/ThingTest.java b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/namedquery/ThingTest.java new file mode 100644 index 000000000000..a10682cf3356 --- /dev/null +++ b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/namedquery/ThingTest.java @@ -0,0 +1,26 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.processor.test.namedquery; + +import jakarta.persistence.EntityManager; +import org.hibernate.processor.test.util.CompilationTest; +import org.hibernate.processor.test.util.WithClasses; +import org.junit.Test; + +import static org.hibernate.processor.test.util.TestUtil.assertMetamodelClassGeneratedFor; +import static org.hibernate.processor.test.util.TestUtil.assertPresenceOfFieldInMetamodelFor; +import static org.hibernate.processor.test.util.TestUtil.assertPresenceOfMethodInMetamodelFor; +import static org.hibernate.processor.test.util.TestUtil.getMetaModelSourceAsString; + +public class ThingTest extends CompilationTest { + @Test @WithClasses( Thing.class ) + public void test() { + System.out.println( getMetaModelSourceAsString( Thing.class) ); + System.out.println( getMetaModelSourceAsString( Thing.class, true) ); + assertMetamodelClassGeneratedFor(Thing.class); + assertMetamodelClassGeneratedFor(Thing.class, true); + assertPresenceOfMethodInMetamodelFor( Thing.class, "things", EntityManager.class, String.class ); + } +} diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/ClassWriter.java b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/ClassWriter.java index fc24d625f027..f7a6479b5574 100644 --- a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/ClassWriter.java +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/ClassWriter.java @@ -10,8 +10,12 @@ import org.hibernate.processor.model.Metamodel; import javax.annotation.processing.FilerException; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; import javax.lang.model.element.ElementKind; import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.TypeMirror; import javax.tools.Diagnostic; import javax.tools.FileObject; import java.io.IOException; @@ -94,7 +98,11 @@ private static StringBuffer generateBody(Metamodel entity, Context context) { if ( context.addSuppressWarningsAnnotation() ) { pw.println( writeSuppressWarnings(context) ); } - entity.inheritedAnnotations().forEach(pw::println); + entity.inheritedAnnotations() + .forEach( annotation -> { + printAnnotation( annotation, pw ); + pw.print('\n'); + } ); printClassDeclaration( entity, pw ); @@ -116,9 +124,10 @@ private static StringBuffer generateBody(Metamodel entity, Context context) { pw.println('\t' + line); if ( line.trim().startsWith("@Override") ) { metaMember.inheritedAnnotations() - .forEach(x -> { + .forEach(annotation -> { pw.print('\t'); - pw.println(x); + printAnnotation( annotation, pw ); + pw.print('\n'); }); } }); @@ -131,6 +140,66 @@ private static StringBuffer generateBody(Metamodel entity, Context context) { } } + private static void printAnnotation(AnnotationMirror annotation, PrintWriter pw) { + pw.print('@'); + final TypeElement type = (TypeElement) annotation.getAnnotationType().asElement(); + pw.print( type.getQualifiedName().toString() ); + var elementValues = annotation.getElementValues(); + if (!elementValues.isEmpty()) { + pw.print('('); + boolean first = true; + for (var entry : elementValues.entrySet()) { + if (first) { + first = false; + } + else { + pw.print(','); + } + pw.print( entry.getKey().getSimpleName() ); + pw.print( '=' ); + printAnnotationValue( pw, entry.getValue() ); + } + pw.print(')'); + } + } + + private static void printAnnotationValue(PrintWriter pw, AnnotationValue value) { + final Object argument = value.getValue(); + if (argument instanceof VariableElement) { + VariableElement variable = (VariableElement) argument; + pw.print( variable.getEnclosingElement() ); + pw.print('.'); + pw.print( variable.getSimpleName().toString() ); + } + else if (argument instanceof AnnotationMirror) { + AnnotationMirror childAnnotation = (AnnotationMirror) argument; + printAnnotation( childAnnotation, pw ); + } + else if (argument instanceof TypeMirror) { + pw.print(argument); + pw.print(".class"); + } + else if (argument instanceof List) { + final List list = + (List) argument; + pw.print('{'); + boolean first = true; + for (AnnotationValue listedValue : list) { + if (first) { + first = false; + } + else { + pw.print(','); + } + printAnnotationValue( pw, listedValue ); + } + pw.print('}'); + } + else { + pw.print( argument ); + } + } + private static void printClassDeclaration(Metamodel entity, PrintWriter pw) { pw.print( "public " ); if ( !entity.isImplementation() && !entity.isJakartaDataStyle() ) { diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/Context.java b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/Context.java index 95a65d14cb0b..fce3c353c38b 100644 --- a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/Context.java +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/Context.java @@ -412,7 +412,7 @@ public boolean checkNamedQuery(String name) { public void setUsesQuarkusOrm(boolean b) { usesQuarkusOrm = b; } - + public boolean usesQuarkusOrm() { return usesQuarkusOrm; } @@ -420,7 +420,7 @@ public boolean usesQuarkusOrm() { public void setUsesQuarkusReactive(boolean b) { usesQuarkusReactive = b; } - + public boolean usesQuarkusReactive() { return usesQuarkusReactive; } @@ -504,7 +504,21 @@ public Map> getEnumTypesByValue() { return enumTypesByValue; } - public void addEnumValue(String type, String value) { - enumTypesByValue.computeIfAbsent( value, s -> new TreeSet<>() ).add( type ); + public void addEnumValue( + String qualifiedTypeName, String shortTypeName, + @Nullable String outerTypeQualifiedName, @Nullable String outerShortTypeName, + String value) { + addEnumValue( qualifiedTypeName, value ); + addEnumValue( qualifiedTypeName, qualifiedTypeName + '.' + value ); + addEnumValue( qualifiedTypeName, shortTypeName + '.' + value ); + if ( outerShortTypeName != null ) { + addEnumValue( qualifiedTypeName, outerShortTypeName + '.' + shortTypeName + '.' + value ); + addEnumValue( qualifiedTypeName, outerShortTypeName + '$' + shortTypeName + '.' + value ); + addEnumValue( qualifiedTypeName, outerTypeQualifiedName + '$' + shortTypeName + '.' + value ); + } + } + + private void addEnumValue(String qualifiedTypeName, String value) { + enumTypesByValue.computeIfAbsent( value, s -> new TreeSet<>() ).add( qualifiedTypeName ); } } diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/HibernateProcessor.java b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/HibernateProcessor.java index 65d65b4d2e76..6ab92c1c15b9 100644 --- a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/HibernateProcessor.java +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/HibernateProcessor.java @@ -221,15 +221,18 @@ private boolean handleSettings(ProcessingEnvironment environment) { final PackageElement jakartaContextPackage = context.getProcessingEnvironment().getElementUtils() .getPackageElement( "jakarta.enterprise.context" ); - final PackageElement jakartaTransactionsPackage = + final PackageElement jakartaTransactionPackage = context.getProcessingEnvironment().getElementUtils() - .getPackageElement( "jakarta.transactions" ); + .getPackageElement( "jakarta.transaction" ); final PackageElement jakartaDataPackage = context.getProcessingEnvironment().getElementUtils() .getPackageElement( "jakarta.data" ); final PackageElement quarkusOrmPackage = context.getProcessingEnvironment().getElementUtils() .getPackageElement( "io.quarkus.hibernate.orm" ); + final PackageElement quarkusReactivePackage = + context.getProcessingEnvironment().getElementUtils() + .getPackageElement( "io.quarkus.hibernate.reactive.runtime" ); PackageElement quarkusOrmPanachePackage = context.getProcessingEnvironment().getElementUtils() @@ -251,8 +254,8 @@ && packagePresent(quarkusOrmPanachePackage) ) { context.setAddNonnullAnnotation( packagePresent(jakartaAnnotationPackage) ); context.setAddGeneratedAnnotation( packagePresent(jakartaAnnotationPackage) ); context.setAddDependentAnnotation( packagePresent(jakartaContextPackage) ); - context.setAddTransactionScopedAnnotation( packagePresent(jakartaTransactionsPackage) ); - context.setQuarkusInjection( packagePresent(quarkusOrmPackage) ); + context.setAddTransactionScopedAnnotation( packagePresent(jakartaTransactionPackage) ); + context.setQuarkusInjection( packagePresent(quarkusOrmPackage) || packagePresent(quarkusReactivePackage) ); context.setUsesQuarkusOrm( packagePresent(quarkusOrmPanachePackage) ); context.setUsesQuarkusReactive( packagePresent(quarkusReactivePanachePackage) ); @@ -367,7 +370,7 @@ private void processClasses(RoundEnvironment roundEnvironment) { try { if ( !included( element ) || hasAnnotation( element, Constants.EXCLUDE ) - || hasAnnotation( context.getElementUtils().getPackageOf(element), Constants.EXCLUDE ) ) { + || hasPackageAnnotation( element, Constants.EXCLUDE ) ) { // skip it completely } else if ( isEntityOrEmbeddable( element ) ) { @@ -420,6 +423,11 @@ else if ( element instanceof TypeElement ) { } } + private boolean hasPackageAnnotation(Element element, String annotation) { + final PackageElement pack = context.getElementUtils().getPackageOf( element ); // null for module descriptor + return pack != null && hasAnnotation( pack, annotation ); + } + private void createMetaModelClasses() { for ( Metamodel aux : context.getMetaAuxiliaries() ) { @@ -641,9 +649,15 @@ private void indexEnumValues(TypeMirror type) { final DeclaredType declaredType = (DeclaredType) type; final TypeElement fieldType = (TypeElement) declaredType.asElement(); if ( fieldType.getKind() == ElementKind.ENUM ) { - for (Element enumMember : fieldType.getEnclosedElements() ) { + for ( Element enumMember : fieldType.getEnclosedElements() ) { if ( enumMember.getKind() == ElementKind.ENUM_CONSTANT ) { + final Element enclosingElement = fieldType.getEnclosingElement(); + final boolean hasOuterType = + enclosingElement.getKind().isClass() || enclosingElement.getKind().isInterface(); context.addEnumValue( fieldType.getQualifiedName().toString(), + fieldType.getSimpleName().toString(), + hasOuterType ? ((TypeElement) enclosingElement).getQualifiedName().toString() : null, + hasOuterType ? enclosingElement.getSimpleName().toString() : null, enumMember.getSimpleName().toString() ); } } diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/AnnotationMeta.java b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/AnnotationMeta.java index 276af407c7d8..4d28e6e20385 100644 --- a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/AnnotationMeta.java +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/AnnotationMeta.java @@ -105,8 +105,10 @@ private void handleNamedQuery(AnnotationMirror mirror, boolean checkHql) { context.getEntityNameMappings(), context.getEnumTypesByValue() ) ); if ( statement instanceof SqmSelectStatement - && isQueryMethodName( name ) ) { + && isQueryMethodName( name ) + && !isJakartaDataStyle() ) { putMember( name, + // TODO: respect @NamedQuery(resultClass) new NamedQueryMethod( this, (SqmSelectStatement) statement, @@ -153,15 +155,17 @@ private void addAuxiliaryMembersForAnnotation(String annotationName, String pref } private void addAuxiliaryMembersForMirror(AnnotationMirror mirror, String prefix) { - mirror.getElementValues().forEach((key, value) -> { - if ( key.getSimpleName().contentEquals("name") ) { - final String name = value.getValue().toString(); - if ( !name.isEmpty() ) { - putMember( prefix + name, - new NameMetaAttribute( this, name, prefix ) ); + if ( !isJakartaDataStyle() ) { + mirror.getElementValues().forEach((key, value) -> { + if ( key.getSimpleName().contentEquals( "name" ) ) { + final String name = value.getValue().toString(); + if ( !name.isEmpty() ) { + putMember( prefix + name, + new NameMetaAttribute( this, name, prefix ) ); + } } - } - }); + }); + } } protected String getSessionVariableName() { diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/AnnotationMetaEntity.java b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/AnnotationMetaEntity.java index 1083592a0167..2429c83de1bb 100644 --- a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/AnnotationMetaEntity.java +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/AnnotationMetaEntity.java @@ -33,7 +33,6 @@ import org.hibernate.query.sqm.tree.expression.SqmParameter; import org.hibernate.query.sqm.tree.select.SqmSelectStatement; -import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.AnnotationValue; import javax.lang.model.element.Element; @@ -51,7 +50,6 @@ import javax.lang.model.type.TypeMirror; import javax.lang.model.type.TypeVariable; import javax.lang.model.type.WildcardType; -import javax.lang.model.util.Elements; import javax.lang.model.util.Types; import javax.tools.Diagnostic; import java.util.ArrayList; @@ -86,10 +84,12 @@ import static org.hibernate.processor.util.TypeUtils.containsAnnotation; import static org.hibernate.processor.util.TypeUtils.determineAccessTypeForHierarchy; import static org.hibernate.processor.util.TypeUtils.determineAnnotationSpecifiedAccessType; +import static org.hibernate.processor.util.TypeUtils.extendsClass; import static org.hibernate.processor.util.TypeUtils.findMappedSuperClass; import static org.hibernate.processor.util.TypeUtils.getAnnotationMirror; import static org.hibernate.processor.util.TypeUtils.getAnnotationValue; import static org.hibernate.processor.util.TypeUtils.hasAnnotation; +import static org.hibernate.processor.util.TypeUtils.implementsInterface; import static org.hibernate.processor.util.TypeUtils.primitiveClassMatchesKind; import static org.hibernate.processor.util.TypeUtils.propertyName; @@ -349,14 +349,15 @@ public boolean isInjectable() { @Override public String scope() { - if (jakartaDataRepository) { - return context.addTransactionScopedAnnotation() - ? "jakarta.transaction.TransactionScoped" - : "jakarta.enterprise.context.RequestScoped"; - } - else { - return "jakarta.enterprise.context.Dependent"; - } + // @TransactionScoped doesn't work here because repositories + // are supposed to be able to demarcate transactions, which + // means they should be injectable when there is no active tx + // @RequestScoped doesn't work because Arc folks think this + // scope should only be active during a HTTP request, which + // is simply wrong according to me, but whatever + // @ApplicationScoped could work in principle, but buys us + // nothing additional, since repositories are stateless + return "jakarta.enterprise.context.Dependent"; } @Override @@ -660,41 +661,18 @@ boolean needsDefaultConstructor() { } private boolean isPanacheType(TypeElement type) { - return isOrmPanacheType( type ) - || isReactivePanacheType( type ); + return context.usesQuarkusOrm() && isOrmPanacheType( type ) + || context.usesQuarkusReactive() && isReactivePanacheType( type ); } private boolean isOrmPanacheType(TypeElement type) { - final ProcessingEnvironment processingEnvironment = context.getProcessingEnvironment(); - final Elements elements = processingEnvironment.getElementUtils(); - final TypeElement panacheRepositorySuperType = elements.getTypeElement( PANACHE_ORM_REPOSITORY_BASE ); - final TypeElement panacheEntitySuperType = elements.getTypeElement( PANACHE_ORM_ENTITY_BASE ); - if ( panacheRepositorySuperType == null || panacheEntitySuperType == null ) { - return false; - } - else { - final Types types = processingEnvironment.getTypeUtils(); - // check against a raw supertype of PanacheRepositoryBase, which .asType() is not - return types.isSubtype( type.asType(), types.getDeclaredType( panacheRepositorySuperType ) ) - || types.isSubtype( type.asType(), panacheEntitySuperType.asType() ); - } + return implementsInterface( type, PANACHE_ORM_REPOSITORY_BASE ) + || extendsClass( type, PANACHE_ORM_ENTITY_BASE ); } private boolean isReactivePanacheType(TypeElement type) { - final ProcessingEnvironment processingEnvironment = context.getProcessingEnvironment(); - final Elements elements = processingEnvironment.getElementUtils(); - final TypeElement panacheRepositorySuperType = elements.getTypeElement( PANACHE_REACTIVE_REPOSITORY_BASE ); - final TypeElement panacheEntitySuperType = elements.getTypeElement( PANACHE_REACTIVE_ENTITY_BASE ); - - if ( panacheRepositorySuperType == null || panacheEntitySuperType == null ) { - return false; - } - else { - final Types types = processingEnvironment.getTypeUtils(); - // check against a raw supertype of PanacheRepositoryBase, which .asType() is not - return types.isSubtype( type.asType(), types.getDeclaredType( panacheRepositorySuperType ) ) - || types.isSubtype( type.asType(), panacheEntitySuperType.asType() ); - } + return implementsInterface( type, PANACHE_REACTIVE_REPOSITORY_BASE ) + || extendsClass( type, PANACHE_REACTIVE_ENTITY_BASE ); } /** @@ -1921,25 +1899,28 @@ private void createSingleParameterFinder( ); break; case NATURAL_ID: - putMember( methodKey, - new NaturalIdFinderMethod( - this, method, - methodName, - returnType.toString(), - containerType, - paramNames, - paramTypes, - parameterNullability(method, entity), - repository, - sessionType[0], - sessionType[1], - profiles, - context.addNonnullAnnotation(), - jakartaDataRepository, - fullReturnType(method) - ) - ); - break; + if ( !usingStatelessSession( sessionType[0] ) ) {// no byNaturalId() lookup API for SS + putMember( methodKey, + new NaturalIdFinderMethod( + this, method, + methodName, + returnType.toString(), + containerType, + paramNames, + paramTypes, + parameterNullability(method, entity), + repository, + sessionType[0], + sessionType[1], + profiles, + context.addNonnullAnnotation(), + jakartaDataRepository, + fullReturnType(method) + ) + ); + break; + } + // else intentionally fall through case BASIC: case MULTIVALUED: final List paramPatterns = parameterPatterns( method ); @@ -2128,7 +2109,8 @@ private void parameterTypeError(TypeElement entityType, VariableElement param, T private boolean finderParameterNullable(TypeElement entity, VariableElement param) { final Element member = memberMatchingPath( entity, parameterName( param ) ); - return member == null || isNullable(member); + return isNullable( param ) + && ( member == null || isNullable( member ) ); } private AccessType getAccessType(TypeElement entity) { @@ -2155,7 +2137,7 @@ private static TypeMirror memberType(Element member) { final AccessType accessType = getAccessType(entityType); final String nextToken = tokens.nextToken(); for ( Element member : context.getAllMembers(entityType) ) { - if ( isIdRef(nextToken) && hasAnnotation( member, ID) ) { + if ( isIdRef(nextToken) && hasAnnotation(member, ID, EMBEDDED_ID) ) { return member; } final Element match = @@ -2705,12 +2687,11 @@ private static boolean constructorMatches( private static boolean parameterMatches(VariableElement parameter, JpaSelection item) { final Class javaType = item.getJavaType(); - return javaType != null && parameterMatches( parameter.asType(), javaType ); + return javaType != null && parameterMatches( parameter.asType(), javaType, item.getJavaTypeName() ); } - private static boolean parameterMatches(TypeMirror parameterType, Class itemType) { + private static boolean parameterMatches(TypeMirror parameterType, Class itemType, String itemTypeName) { final TypeKind kind = parameterType.getKind(); - final String itemTypeName = itemType.getName(); if ( kind == TypeKind.DECLARED ) { final DeclaredType declaredType = (DeclaredType) parameterType; final TypeElement paramTypeElement = (TypeElement) declaredType.asElement(); @@ -2722,7 +2703,7 @@ else if ( kind.isPrimitive() ) { else if ( kind == TypeKind.ARRAY ) { final ArrayType arrayType = (ArrayType) parameterType; return itemType.isArray() - && parameterMatches( arrayType.getComponentType(), itemType.getComponentType() ); + && parameterMatches( arrayType.getComponentType(), itemType.getComponentType(), itemType.getComponentType().getTypeName() ); } else { return false; @@ -2981,30 +2962,29 @@ private static boolean isNullable(Element member) { return false; } case FIELD: + case PARAMETER: if ( member.asType().getKind().isPrimitive() ) { return false; } } - boolean nullable = true; for ( AnnotationMirror mirror : member.getAnnotationMirrors() ) { final TypeElement annotationType = (TypeElement) mirror.getAnnotationType().asElement(); final Name name = annotationType.getQualifiedName(); - if ( name.contentEquals(Constants.ID) ) { - nullable = false; - } - if ( name.contentEquals("jakarta.validation.constraints.NotNull")) { - nullable = false; + if ( name.contentEquals(Constants.ID) + || name.contentEquals(Constants.NOT_NULL) + || name.contentEquals(Constants.NONNULL) ) { + return false; } - if ( name.contentEquals(Constants.BASIC) + else if ( name.contentEquals(Constants.BASIC) || name.contentEquals(Constants.MANY_TO_ONE) || name.contentEquals(Constants.ONE_TO_ONE)) { - AnnotationValue optional = getAnnotationValue(mirror, "optional"); + final AnnotationValue optional = getAnnotationValue(mirror, "optional"); if ( optional != null && optional.getValue().equals(FALSE) ) { - nullable = false; + return false; } } } - return nullable; + return true; } private void checkParameters( diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/MetaAttributeGenerationVisitor.java b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/MetaAttributeGenerationVisitor.java index abe3c356733d..7b28f26f135e 100644 --- a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/MetaAttributeGenerationVisitor.java +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/MetaAttributeGenerationVisitor.java @@ -29,6 +29,7 @@ import java.util.List; import static org.hibernate.processor.util.Constants.ELEMENT_COLLECTION; +import static org.hibernate.processor.util.Constants.LIST_ATTRIBUTE; import static org.hibernate.processor.util.Constants.MANY_TO_ANY; import static org.hibernate.processor.util.Constants.MANY_TO_MANY; import static org.hibernate.processor.util.Constants.MAP_KEY_CLASS; @@ -72,7 +73,13 @@ private Types typeUtils() { @Override public @Nullable AnnotationMetaAttribute visitArray(ArrayType arrayType, Element element) { - return new AnnotationMetaSingleAttribute( entity, element, toArrayTypeString( arrayType, context ) ); + if ( hasAnnotation( element, MANY_TO_MANY, ONE_TO_MANY, ELEMENT_COLLECTION ) ) { + return new AnnotationMetaCollection( entity, element, LIST_ATTRIBUTE, + toTypeString(arrayType.getComponentType()) ); + } + else { + return new AnnotationMetaSingleAttribute( entity, element, toArrayTypeString( arrayType, context ) ); + } } @Override diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/NameMetaAttribute.java b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/NameMetaAttribute.java index b2aa1872f46e..3a086348a400 100644 --- a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/NameMetaAttribute.java +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/NameMetaAttribute.java @@ -42,8 +42,11 @@ public String getAttributeDeclarationString() { @Override public String getAttributeNameDeclarationString() { - return new StringBuilder() - .append("public static final ") + final StringBuilder declaration = new StringBuilder(); + if ( !annotationMetaEntity.isJakartaDataStyle() ) { + declaration.append( "public static final " ); + } + return declaration .append(annotationMetaEntity.importType(String.class.getName())) .append(" ") .append(prefix) diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/util/Constants.java b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/util/Constants.java index 1fa6632df54c..1ceceb79cfb6 100644 --- a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/util/Constants.java +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/util/Constants.java @@ -131,6 +131,8 @@ public final class Constants { public static final String STREAM = "java.util.stream.Stream"; public static final String NULLABLE = "jakarta.annotation.Nullable"; + public static final String NONNULL = "jakarta.annotation.Nonnull"; + public static final String NOT_NULL = "jakarta.validation.constraints.NotNull"; public static final String PANACHE_ORM_REPOSITORY_BASE = "io.quarkus.hibernate.orm.panache.PanacheRepositoryBase"; public static final String PANACHE_ORM_ENTITY_BASE = "io.quarkus.hibernate.orm.panache.PanacheEntityBase"; diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/util/StringUtil.java b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/util/StringUtil.java index 1d6160635bbe..994857eec775 100644 --- a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/util/StringUtil.java +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/util/StringUtil.java @@ -8,6 +8,10 @@ import java.util.Locale; +import static java.lang.Character.charCount; +import static java.lang.Character.isUpperCase; +import static java.lang.Character.toUpperCase; + /** * @author Hardy Ferentschik */ @@ -102,8 +106,24 @@ public static String nameToMethodName(String name) { return name.replaceAll("[\\s.\\-!@#%=+/*^&|(){}\\[\\],]", "_"); } - public static String getUpperUnderscoreCaseFromLowerCamelCase(String lowerCamelCaseString){ - return lowerCamelCaseString.replaceAll("(.)(\\p{Upper})", "$1_$2").toUpperCase(Locale.ROOT); + public static String getUpperUnderscoreCaseFromLowerCamelCase(String lowerCamelCaseString) { + final StringBuilder result = new StringBuilder(); + int position = 0; + boolean wasLowerCase = false; + while ( position < lowerCamelCaseString.length() ) { + final int codePoint = lowerCamelCaseString.codePointAt( position ); + final boolean isUpperCase = isUpperCase( codePoint ); + if ( wasLowerCase && isUpperCase ) { + result.append('_'); + } + result.appendCodePoint( toUpperCase( codePoint ) ); + position += charCount( codePoint ); + wasLowerCase = !isUpperCase; + } + if ( result.toString().equals( lowerCamelCaseString ) ) { + result.insert(0, '_'); + } + return result.toString(); } private static boolean startsWithSeveralUpperCaseLetters(String string) { diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/util/TypeUtils.java b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/util/TypeUtils.java index 724dd3c8668f..567cc886ed3b 100644 --- a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/util/TypeUtils.java +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/util/TypeUtils.java @@ -129,12 +129,11 @@ protected TypeMirror defaultAction(TypeMirror e, Void aVoid) { } public static @Nullable TypeElement getSuperclassTypeElement(TypeElement element) { - final TypeMirror superClass = element.getSuperclass(); + final TypeMirror superclass = element.getSuperclass(); //superclass of Object is of NoType which returns some other kind - if ( superClass.getKind() == TypeKind.DECLARED ) { - //F..king Ch...t Have those people used their horrible APIs even once? - final Element superClassElement = ( (DeclaredType) superClass ).asElement(); - return (TypeElement) superClassElement; + if ( superclass.getKind() == TypeKind.DECLARED ) { + final DeclaredType declaredType = (DeclaredType) superclass; + return (TypeElement) declaredType.asElement(); } else { return null; @@ -602,7 +601,7 @@ else if ( element.getKind() == ElementKind.METHOD ) { return elementsUtil.getName(decapitalize(name.substring(3))).toString(); } else if ( name.startsWith( "is" ) ) { - return (elementsUtil.getName(decapitalize(name.substring(2)))).toString(); + return elementsUtil.getName(decapitalize(name.substring(2))).toString(); } return elementsUtil.getName(decapitalize(name)).toString(); } @@ -654,6 +653,33 @@ private static boolean extendsSuperMetaModel(Element superClassElement, boolean || !entityMetaComplete && containsAnnotation( superClassElement, ENTITY, MAPPED_SUPERCLASS ); } + public static boolean implementsInterface(TypeElement type, String interfaceName) { + for ( TypeMirror iface : type.getInterfaces() ) { + if ( iface.getKind() == TypeKind.DECLARED ) { + final DeclaredType declaredType = (DeclaredType) iface; + final TypeElement typeElement = (TypeElement) declaredType.asElement(); + if ( typeElement.getQualifiedName().contentEquals( interfaceName ) + || implementsInterface( typeElement, interfaceName ) ) { + return true; + } + } + } + return false; + } + + public static boolean extendsClass(TypeElement type, String className) { + TypeMirror superclass = type.getSuperclass(); + while ( superclass != null && superclass.getKind() == TypeKind.DECLARED ) { + final DeclaredType declaredType = (DeclaredType) superclass; + final TypeElement typeElement = (TypeElement) declaredType.asElement(); + if ( typeElement.getQualifiedName().contentEquals( className ) ) { + return true; + } + superclass = typeElement.getSuperclass(); + } + return false; + } + static class EmbeddedAttributeVisitor extends SimpleTypeVisitor8<@Nullable TypeElement, Element> { private final Context context; @@ -665,7 +691,7 @@ static class EmbeddedAttributeVisitor extends SimpleTypeVisitor8<@Nullable TypeE public @Nullable TypeElement visitDeclared(DeclaredType declaredType, Element element) { final TypeElement returnedElement = (TypeElement) context.getTypeUtils().asElement( declaredType ); - return containsAnnotation( NullnessUtil.castNonNull( returnedElement ), EMBEDDABLE ) ? returnedElement : null; + return containsAnnotation( castNonNull( returnedElement ), EMBEDDABLE ) ? returnedElement : null; } @Override diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/validation/MockCollectionPersister.java b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/validation/MockCollectionPersister.java index 7ac32c7a549e..28ef59d27b8d 100644 --- a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/validation/MockCollectionPersister.java +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/validation/MockCollectionPersister.java @@ -12,12 +12,12 @@ import org.hibernate.persister.collection.QueryableCollection; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.type.CollectionType; +import org.hibernate.type.EntityType; import org.hibernate.type.ListType; import org.hibernate.type.MapType; import org.hibernate.type.Type; import static org.hibernate.internal.util.StringHelper.root; -import static org.hibernate.processor.validation.MockSessionFactory.typeConfiguration; /** * @author Gavin King @@ -95,11 +95,11 @@ public Type getKeyType() { @Override public Type getIndexType() { if (collectionType instanceof ListType) { - return typeConfiguration.getBasicTypeForJavaType(Integer.class); + return factory.getTypeConfiguration().getBasicTypeForJavaType(Integer.class); } else if (collectionType instanceof MapType) { //TODO!!! this is incorrect, return the correct key type - return typeConfiguration.getBasicTypeForJavaType(String.class); + return factory.getTypeConfiguration().getBasicTypeForJavaType(String.class); } else { return null; @@ -113,7 +113,7 @@ public Type getElementType() { @Override public Type getIdentifierType() { - return typeConfiguration.getBasicTypeForJavaType(Long.class); + return factory.getTypeConfiguration().getBasicTypeForJavaType(Long.class); } @Override @@ -124,7 +124,7 @@ public boolean hasIndex() { @Override public EntityPersister getElementPersister() { - if (elementType.isEntityType()) { + if (elementType instanceof EntityType ) { return factory.getMetamodel() .entityPersister(elementType.getName()); } @@ -140,7 +140,7 @@ public SessionFactoryImplementor getFactory() { @Override public boolean isOneToMany() { - return elementType.isEntityType(); + return elementType instanceof EntityType; } @Override diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/validation/MockEntityPersister.java b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/validation/MockEntityPersister.java index db099edb186f..010c206d7a6e 100644 --- a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/validation/MockEntityPersister.java +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/validation/MockEntityPersister.java @@ -13,6 +13,7 @@ import org.hibernate.persister.entity.EntityPersister; import org.hibernate.persister.entity.Queryable; import org.hibernate.tuple.entity.EntityMetamodel; +import org.hibernate.type.BasicType; import org.hibernate.type.Type; import java.io.Serializable; @@ -24,8 +25,6 @@ import java.util.Objects; import java.util.Set; -import static org.hibernate.processor.validation.MockSessionFactory.typeConfiguration; - /** * @author Gavin King */ @@ -117,18 +116,36 @@ public final Type getPropertyType(String propertyPath) { abstract Type createPropertyType(String propertyPath); + /** + * Override on subclasses! + */ + @Override + public String getIdentifierPropertyName() { + return getRootEntityPersister().identifierPropertyName(); + } + + protected abstract String identifierPropertyName(); + + /** + * Override on subclasses! + */ @Override public Type getIdentifierType() { - //TODO: propertyType(getIdentifierPropertyName()) - return typeConfiguration.getBasicTypeForJavaType(Long.class); + return getRootEntityPersister().identifierType(); } + protected abstract Type identifierType(); + + /** + * Override on subclasses! + */ @Override - public String getIdentifierPropertyName() { - //TODO: return the correct @Id property name - return "id"; + public BasicType getVersionType() { + return getRootEntityPersister().versionType(); } + protected abstract BasicType versionType(); + @Override public Type toType(String propertyName) throws QueryException { Type type = getPropertyType(propertyName); @@ -141,13 +158,10 @@ public Type toType(String propertyName) throws QueryException { } @Override - public String getRootEntityName() { - for (MockEntityPersister persister : factory.getMockEntityPersisters()) { - if (this != persister && persister.isSubclassPersister(this)) { - return persister.getRootEntityName(); - } - } - return entityName; + public abstract String getRootEntityName(); + + public MockEntityPersister getRootEntityPersister() { + return factory.createMockEntityPersister(getRootEntityName()); } @Override @@ -204,7 +218,7 @@ public DiscriminatorMetadata getTypeDiscriminatorMetadata() { @Override public Type getResolutionType() { - return typeConfiguration.getBasicTypeForJavaType(Class.class); + return factory.getTypeConfiguration().getBasicTypeForJavaType(Class.class); } @Override @@ -219,7 +233,12 @@ public String toString() { @Override public int getVersionProperty() { - return -66; + return 0; + } + + @Override + public boolean isVersioned() { + return true; } @Override @@ -234,6 +253,11 @@ public boolean consumesEntityAlias() { @Override public Type getDiscriminatorType() { - return typeConfiguration.getBasicTypeForJavaType(String.class); + return factory.getTypeConfiguration().getBasicTypeForJavaType(String.class); + } + + @Override + public boolean isMutable() { + return true; } } diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/validation/MockSessionFactory.java b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/validation/MockSessionFactory.java index 103cd5c579e3..707e7e584b69 100644 --- a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/validation/MockSessionFactory.java +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/validation/MockSessionFactory.java @@ -6,6 +6,7 @@ */ package org.hibernate.processor.validation; +import jakarta.persistence.metamodel.Bindable; import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.CustomEntityDirtinessStrategy; import org.hibernate.EntityNameResolver; @@ -22,10 +23,12 @@ import org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl; import org.hibernate.boot.model.relational.Database; import org.hibernate.boot.model.relational.SqlStringGenerationContext; -import org.hibernate.boot.registry.BootstrapServiceRegistryBuilder; import org.hibernate.boot.registry.classloading.internal.ClassLoaderServiceImpl; +import org.hibernate.boot.registry.classloading.spi.ClassLoaderService; import org.hibernate.boot.registry.classloading.spi.ClassLoadingException; +import org.hibernate.boot.registry.internal.BootstrapServiceRegistryImpl; import org.hibernate.boot.registry.internal.StandardServiceRegistryImpl; +import org.hibernate.boot.registry.selector.internal.StrategySelectorImpl; import org.hibernate.boot.spi.BootstrapContext; import org.hibernate.boot.spi.MappingDefaults; import org.hibernate.boot.spi.MetadataBuildingContext; @@ -57,16 +60,22 @@ import org.hibernate.metamodel.internal.JpaStaticMetaModelPopulationSetting; import org.hibernate.metamodel.internal.MetadataContext; import org.hibernate.metamodel.internal.RuntimeMetamodelsImpl; +import org.hibernate.metamodel.mapping.EntityIdentifierMapping; import org.hibernate.metamodel.mapping.JdbcMapping; +import org.hibernate.metamodel.model.domain.BasicDomainType; import org.hibernate.metamodel.model.domain.DomainType; +import org.hibernate.metamodel.model.domain.EmbeddableDomainType; import org.hibernate.metamodel.model.domain.EntityDomainType; import org.hibernate.metamodel.model.domain.ManagedDomainType; import org.hibernate.metamodel.model.domain.PersistentAttribute; +import org.hibernate.metamodel.model.domain.SingularPersistentAttribute; import org.hibernate.metamodel.model.domain.internal.AbstractAttribute; import org.hibernate.metamodel.model.domain.internal.AbstractPluralAttribute; import org.hibernate.metamodel.model.domain.internal.BagAttributeImpl; +import org.hibernate.metamodel.model.domain.internal.BasicSqmPathSource; import org.hibernate.metamodel.model.domain.internal.BasicTypeImpl; import org.hibernate.metamodel.model.domain.internal.EmbeddableTypeImpl; +import org.hibernate.metamodel.model.domain.internal.EmbeddedSqmPathSource; import org.hibernate.metamodel.model.domain.internal.EntityTypeImpl; import org.hibernate.metamodel.model.domain.internal.JpaMetamodelImpl; import org.hibernate.metamodel.model.domain.internal.ListAttributeImpl; @@ -102,6 +111,7 @@ import org.hibernate.stat.internal.StatisticsImpl; import org.hibernate.stat.spi.StatisticsImplementor; import org.hibernate.type.BagType; +import org.hibernate.type.BasicType; import org.hibernate.type.CollectionType; import org.hibernate.type.CompositeType; import org.hibernate.type.ListType; @@ -140,9 +150,7 @@ public abstract class MockSessionFactory private static final BasicTypeImpl OBJECT_BASIC_TYPE = new BasicTypeImpl<>(new UnknownBasicJavaType<>(Object.class), ObjectJdbcType.INSTANCE); - // static so other things can get at it - // TODO: make a static instance of this whole object instead! - static TypeConfiguration typeConfiguration; + private final TypeConfiguration typeConfiguration; private final Map entityPersistersByName = new HashMap<>(); private final Map collectionPersistersByName = new HashMap<>(); @@ -155,25 +163,31 @@ public abstract class MockSessionFactory private final MetadataContext metadataContext; public MockSessionFactory() { - - serviceRegistry = StandardServiceRegistryImpl.create( - new BootstrapServiceRegistryBuilder().applyClassLoaderService(new ClassLoaderServiceImpl() { - @Override - @SuppressWarnings("unchecked") - public Class classForName(String className) { - try { - return super.classForName(className); - } - catch (ClassLoadingException e) { - if (isClassDefined(className)) { - return Object[].class; - } - else { - throw e; - } - } + final ClassLoaderService classLoaderService = new ClassLoaderServiceImpl() { + @Override + @SuppressWarnings("unchecked") + public Class classForName(String className) { + try { + return super.classForName( className ); + } + catch (ClassLoadingException e) { + if ( isClassDefined( className ) ) { + return Object[].class; } - }).build(), + else { + throw e; + } + } + } + }; + serviceRegistry = StandardServiceRegistryImpl.create( + new BootstrapServiceRegistryImpl( + true, + classLoaderService, + new StrategySelectorImpl( classLoaderService ), + () -> emptyList() + ), +// new BootstrapServiceRegistryBuilder().applyClassLoaderService( classLoaderService ).build(), singletonList(MockJdbcServicesInitiator.INSTANCE), emptyList(), emptyMap() @@ -338,6 +352,12 @@ public Type getIdentifierType(String className) .getIdentifierType(); } + public BasicType getVersionType(String className) + throws MappingException { + return createEntityPersister(className) + .getVersionType(); + } + @Override public String getIdentifierPropertyName(String className) throws MappingException { @@ -917,9 +937,73 @@ public MockEntityDomainType(String entityName) { metamodel.getJpaMetamodel()); } + @Override + public SingularPersistentAttribute findVersionAttribute() { + final BasicType type = getVersionType(getHibernateEntityName()); + if (type == null) { + return null; + } + else { + return new SingularAttributeImpl<>( + MockEntityDomainType.this, + "{version}", + AttributeClassification.BASIC, + type, + type.getRelationalJavaType(), + null, + false, + true, + false, + false, + metadataContext + ); + } + } + + @Override + public boolean hasVersionAttribute() { + return getVersionType(getHibernateEntityName()) != null; + } + + @Override + public SqmPathSource getIdentifierDescriptor() { + final Type type = getIdentifierType(getHibernateEntityName()); + if (type == null) { + return null; + } + else if (type instanceof BasicDomainType) { + return new BasicSqmPathSource<>( + EntityIdentifierMapping.ID_ROLE_NAME, + null, + (BasicDomainType) type, + MockEntityDomainType.this.getExpressibleJavaType(), + Bindable.BindableType.SINGULAR_ATTRIBUTE, + false + ); + } + else if (type instanceof EmbeddableDomainType) { + return new EmbeddedSqmPathSource<>( + EntityIdentifierMapping.ID_ROLE_NAME, + null, + (EmbeddableDomainType) type, + Bindable.BindableType.SINGULAR_ATTRIBUTE, + false + ); + } + else { + return null; + } + } + @Override public SqmPathSource findSubPathSource(String name, JpaMetamodelImplementor metamodel) { - SqmPathSource source = super.findSubPathSource(name, metamodel); + switch (name) { + case EntityIdentifierMapping.ID_ROLE_NAME: + return getIdentifierDescriptor(); + case "{version}": + return findVersionAttribute(); + } + final SqmPathSource source = super.findSubPathSource(name, metamodel); if ( source != null ) { return source; } diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/validation/ProcessorSessionFactory.java b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/validation/ProcessorSessionFactory.java index abbbc9967a29..1925e82af4c5 100644 --- a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/validation/ProcessorSessionFactory.java +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/validation/ProcessorSessionFactory.java @@ -10,6 +10,7 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.PropertyNotFoundException; import org.hibernate.engine.spi.Mapping; +import org.hibernate.metamodel.mapping.EntityIdentifierMapping; import org.hibernate.type.BasicType; import org.hibernate.type.CollectionType; import org.hibernate.type.CompositeType; @@ -58,7 +59,6 @@ import static org.hibernate.internal.util.StringHelper.root; import static org.hibernate.internal.util.StringHelper.split; import static org.hibernate.internal.util.StringHelper.unroot; -import static org.hibernate.metamodel.model.domain.internal.JpaMetamodelImpl.addAllowedEnumLiteralsToEnumTypesMap; import static org.hibernate.processor.util.Constants.JAVA_OBJECT; /** @@ -91,7 +91,7 @@ public static MockSessionFactory create( private final Types typeUtil; private final Filer filer; private final Map entityNameMappings; - private final Map> allowedEnumLiteralsToEnumTypeNames; + private final Map> enumTypesByValue; public ProcessorSessionFactory( ProcessingEnvironment processingEnvironment, @@ -101,23 +101,7 @@ public ProcessorSessionFactory( typeUtil = processingEnvironment.getTypeUtils(); filer = processingEnvironment.getFiler(); this.entityNameMappings = entityNameMappings; - final Map> allowedEnumLiteralsToEnumTypeNames = new HashMap<>( enumTypesByValue.size() << 2 ); - for ( Map.Entry> entry : enumTypesByValue.entrySet() ) { - final String enumConstantName = entry.getKey(); - for ( String enumClassName : entry.getValue() ) { - final TypeElement enumTypeElement = elementUtil.getTypeElement( enumClassName ); - if ( enumTypeElement != null ) { - addAllowedEnumLiteralsToEnumTypesMap( - allowedEnumLiteralsToEnumTypeNames, - enumConstantName, - enumTypeElement.getSimpleName().toString(), - elementUtil.getBinaryName( enumTypeElement ).toString(), - enumClassName - ); - } - } - } - this.allowedEnumLiteralsToEnumTypeNames = allowedEnumLiteralsToEnumTypeNames; + this.enumTypesByValue = enumTypesByValue; } @Override @@ -148,6 +132,7 @@ else if (isElementCollectionProperty(property)) { } } + @Override Type propertyType(String typeName, String propertyPath) { TypeElement type = findClassByQualifiedName(typeName); @@ -178,13 +163,13 @@ private static Element dereference(AccessType defaultAccessType, Element symbol, } } - static Type propertyType(Element member, String entityName, String path, AccessType defaultAccessType) { + private Type propertyType(Element member, String entityName, String path, AccessType defaultAccessType) { final TypeMirror memberType = memberType(member); if (isEmbeddedProperty(member)) { - return component.make(asElement(memberType), entityName, path, defaultAccessType); + return component.make(asElement(memberType), entityName, path, defaultAccessType, this); } else if (isToOneAssociation(member)) { - return new ManyToOneType(typeConfiguration, getToOneTargetEntity(member)); + return new ManyToOneType(getTypeConfiguration(), getToOneTargetEntity(member)); } else if (isToManyAssociation(member)) { return collectionType(memberType, qualify(entityName, path)); @@ -193,10 +178,10 @@ else if (isElementCollectionProperty(member)) { return collectionType(memberType, qualify(entityName, path)); } else if (isEnumProperty(member)) { - return enumType( member, memberType ); + return enumType(member, memberType); } else { - return typeConfiguration.getBasicTypeRegistry() + return getTypeConfiguration().getBasicTypeRegistry() .getRegisteredType(qualifiedName(memberType)); } } @@ -234,7 +219,7 @@ private static JdbcType enumJdbcType(Element member) { @Override @Nullable Set getEnumTypesForValue(String value) { - Set result = allowedEnumLiteralsToEnumTypeNames.get( value); + Set result = enumTypesByValue.get(value); if ( result != null ) { return result; } @@ -255,12 +240,13 @@ Set getEnumTypesForValue(String value) { private static Type elementCollectionElementType(TypeElement elementType, String role, String path, - AccessType defaultAccessType) { + AccessType defaultAccessType, + MockSessionFactory factory) { if (isEmbeddableType(elementType)) { - return component.make(elementType, role, path, defaultAccessType); + return component.make(elementType, role, path, defaultAccessType, factory); } else { - return typeConfiguration.getBasicTypeRegistry() + return factory.getTypeConfiguration().getBasicTypeRegistry() .getRegisteredType(qualifiedName(elementType.asType())); } } @@ -277,7 +263,8 @@ public static abstract class Component implements CompositeType { public Component(TypeElement type, String entityName, String path, - AccessType defaultAccessType) { + AccessType defaultAccessType, + ProcessorSessionFactory factory) { this.type = type; List names = new ArrayList<>(); @@ -290,7 +277,7 @@ public Component(TypeElement type, if (isPersistable(member, accessType)) { String name = propertyName(member); Type propertyType = - propertyType(member, entityName, + factory.propertyType(member, entityName, qualify(path, name), defaultAccessType); if (propertyType != null) { names.add(name); @@ -358,14 +345,31 @@ public int getColumnSpan(Mapping mapping) { public static abstract class EntityPersister extends MockEntityPersister { private final TypeElement type; private final Types typeUtil; + private final ProcessorSessionFactory factory; - public EntityPersister(String entityName, TypeElement type, ProcessorSessionFactory that) { - super(entityName, getDefaultAccessType(type), that); + public EntityPersister(String entityName, TypeElement type, ProcessorSessionFactory factory) { + super(entityName, getDefaultAccessType(type), factory); this.type = type; - this.typeUtil = that.typeUtil; + this.typeUtil = factory.typeUtil; + this.factory = factory; initSubclassPersisters(); } + @Override + public String getRootEntityName() { + TypeElement result = type; + TypeMirror superclass = type.getSuperclass(); + while ( superclass!=null && superclass.getKind() == TypeKind.DECLARED ) { + final DeclaredType declaredType = (DeclaredType) superclass; + final TypeElement typeElement = (TypeElement) declaredType.asElement(); + if ( hasAnnotation(typeElement, "Entity") ) { + result = typeElement; + } + superclass = typeElement.getSuperclass(); + } + return ProcessorSessionFactory.getEntityName(result); + } + @Override boolean isSubclassPersister(MockEntityPersister entityPersister) { EntityPersister persister = (EntityPersister) entityPersister; @@ -376,15 +380,44 @@ boolean isSubclassPersister(MockEntityPersister entityPersister) { Type createPropertyType(String propertyPath) { Element symbol = findPropertyByPath(type, propertyPath, defaultAccessType); return symbol == null ? null : - propertyType(symbol, getEntityName(), propertyPath, defaultAccessType); + factory.propertyType(symbol, getEntityName(), propertyPath, defaultAccessType); } + @Override + public String identifierPropertyName() { + for (Element element : type.getEnclosedElements()) { + if ( hasAnnotation(element, "Id") || hasAnnotation(element, "EmbeddedId") ) { + return element.getSimpleName().toString(); + } + } + return "id"; + } + + @Override + public Type identifierType() { + for (Element element : type.getEnclosedElements()) { + if ( hasAnnotation(element, "Id")|| hasAnnotation(element, "EmbeddedId") ) { + return factory.propertyType(element, getEntityName(), EntityIdentifierMapping.ID_ROLE_NAME, defaultAccessType); + } + } + return null; + } + + @Override + public BasicType versionType() { + for (Element element : type.getEnclosedElements()) { + if ( hasAnnotation(element, "Version") ) { + return (BasicType) factory.propertyType(element, getEntityName(), "{version}", defaultAccessType); + } + } + return null; + } } public abstract static class ToManyAssociationPersister extends MockCollectionPersister { public ToManyAssociationPersister(String role, CollectionType collectionType, String targetEntityName, ProcessorSessionFactory that) { super(role, collectionType, - new ManyToOneType(typeConfiguration, targetEntityName), + new ManyToOneType(that.getTypeConfiguration(), targetEntityName), that); } @@ -397,26 +430,29 @@ Type getElementPropertyType(String propertyPath) { public abstract static class ElementCollectionPersister extends MockCollectionPersister { private final TypeElement elementType; private final AccessType defaultAccessType; + private final ProcessorSessionFactory factory; public ElementCollectionPersister(String role, CollectionType collectionType, TypeElement elementType, String propertyPath, AccessType defaultAccessType, - ProcessorSessionFactory that) { + ProcessorSessionFactory factory) { super(role, collectionType, elementCollectionElementType(elementType, role, - propertyPath, defaultAccessType), - that); + propertyPath, defaultAccessType, + factory), + factory); this.elementType = elementType; this.defaultAccessType = defaultAccessType; + this.factory = factory; } @Override Type getElementPropertyType(String propertyPath) { Element symbol = findPropertyByPath(elementType, propertyPath, defaultAccessType); return symbol == null ? null : - propertyType(symbol, getOwnerEntityName(), propertyPath, defaultAccessType); + factory.propertyType(symbol, getOwnerEntityName(), propertyPath, defaultAccessType); } } diff --git a/tooling/metamodel-generator/src/test/java/org/hibernate/processor/test/hhh18858/ArrayTest.java b/tooling/metamodel-generator/src/test/java/org/hibernate/processor/test/hhh18858/ArrayTest.java new file mode 100644 index 000000000000..3c63aa131f6b --- /dev/null +++ b/tooling/metamodel-generator/src/test/java/org/hibernate/processor/test/hhh18858/ArrayTest.java @@ -0,0 +1,91 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.processor.test.hhh18858; + + +import jakarta.persistence.metamodel.ListAttribute; +import jakarta.persistence.metamodel.SingularAttribute; +import org.hibernate.processor.test.util.CompilationTest; +import org.hibernate.processor.test.util.WithClasses; +import org.junit.Test; +import org.junit.jupiter.api.Assertions; + +import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Collection; + +import static org.hibernate.processor.test.util.TestUtil.getFieldFromMetamodelFor; +import static org.hibernate.processor.test.util.TestUtil.getMetaModelSourceAsString; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * @author Emmanuel Bernard + */ +public class ArrayTest extends CompilationTest { + + @Test + @WithClasses({Competitor.class, Contest.class}) + public void testOneToMany() throws NoSuchFieldException, IllegalAccessException { + System.out.println( getMetaModelSourceAsString( Competitor.class ) ); + assertValidMetamodelField( Competitor.class, "id" ); + assertValidMetamodelField( Competitor.class, "name" ); + + System.out.println( getMetaModelSourceAsString( Contest.class ) ); + assertValidMetamodelField( Contest.class, "id" ); + assertValidMetamodelField( Contest.class, "results" ); + assertValidMetamodelField( Contest.class, "heldIn" ); + } + + private void assertValidMetamodelField(Class entityClass, String fieldName) + throws NoSuchFieldException, IllegalAccessException { + final Field entityField = entityClass.getDeclaredField( fieldName ); + final Class entityFieldType = entityField.getType(); + + final Field modelField = getFieldFromMetamodelFor( entityClass, fieldName ); + final Type modelFieldGenericType = modelField.getGenericType(); + if (modelFieldGenericType instanceof ParameterizedType) { + ParameterizedType parametrized = (ParameterizedType) modelFieldGenericType; + final Type[] typeArguments = parametrized.getActualTypeArguments(); + assertEquals( 2, typeArguments.length ); + assertEquals( entityClass, typeArguments[0] ); + if ( Collection.class.isAssignableFrom( entityFieldType ) || entityFieldType.isArray() ) { + assertEquals( ListAttribute.class, parametrized.getRawType() ); + if ( Collection.class.isAssignableFrom( entityFieldType ) ) { + final ParameterizedType entityFieldGenericType = (ParameterizedType) entityField.getGenericType(); + assertEquals( entityFieldGenericType.getActualTypeArguments()[0], typeArguments[1] ); + } + else if ( entityFieldType.getComponentType().isPrimitive() ) { + assertEquals( + entityFieldType.getComponentType(), + ((Class) typeArguments[1]).getDeclaredField( "TYPE" ).get( null ) + ); + } + else { + assertEquals( entityFieldType.getComponentType(), typeArguments[1] ); + } + } + else { + assertEquals( SingularAttribute.class, parametrized.getRawType() ); + if ( entityFieldType.isPrimitive() ) { + assertEquals( + entityFieldType, + ((Class) typeArguments[1]).getDeclaredField( "TYPE" ).get( null ) + ); + } + else { + assertEquals( entityFieldType, typeArguments[1] ); + } + } + } + else { + Assertions.fail(); + } + + } + +} diff --git a/tooling/metamodel-generator/src/test/java/org/hibernate/processor/test/hhh18858/Competitor.java b/tooling/metamodel-generator/src/test/java/org/hibernate/processor/test/hhh18858/Competitor.java new file mode 100644 index 000000000000..a8b0677cf0cf --- /dev/null +++ b/tooling/metamodel-generator/src/test/java/org/hibernate/processor/test/hhh18858/Competitor.java @@ -0,0 +1,37 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.processor.test.hhh18858; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; + +/** + * @author Emmanuel Bernard + */ +@Entity +public class Competitor { + private int id; + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Id + @GeneratedValue + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } +} diff --git a/tooling/metamodel-generator/src/test/java/org/hibernate/processor/test/hhh18858/Contest.java b/tooling/metamodel-generator/src/test/java/org/hibernate/processor/test/hhh18858/Contest.java new file mode 100644 index 000000000000..761ad7b287b2 --- /dev/null +++ b/tooling/metamodel-generator/src/test/java/org/hibernate/processor/test/hhh18858/Contest.java @@ -0,0 +1,62 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.processor.test.hhh18858; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OrderColumn; + +import org.hibernate.annotations.ListIndexBase; + +/** + * @author Emmanuel Bernard + */ +@Entity +public class Contest { + private int id; + private Competitor[] results; + private int[] heldIn; + + @Id + @GeneratedValue + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + @OneToMany(cascade = CascadeType.ALL) + @OrderColumn(name = "pos") + public Competitor[] getResults() { + return results; + } + + public void setResults(Competitor[] results) { + this.results = results; + } + + @ElementCollection + @OrderColumn + @ListIndexBase( 1 ) + public int[] getHeldIn() { + return heldIn; + } + + public void setHeldIn(int[] heldIn) { + this.heldIn = heldIn; + } + + /*public enum Month { + January, February, March, April, May, June, July, August, September, October, November, December; + };*/ +} diff --git a/tooling/metamodel-generator/src/test/java/org/hibernate/processor/test/uppercase/Person.java b/tooling/metamodel-generator/src/test/java/org/hibernate/processor/test/uppercase/Person.java new file mode 100644 index 000000000000..078e9e4b1b77 --- /dev/null +++ b/tooling/metamodel-generator/src/test/java/org/hibernate/processor/test/uppercase/Person.java @@ -0,0 +1,14 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.processor.test.uppercase; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + +@Entity +public class Person { + @Id String SSN; + String UserID; +} diff --git a/tooling/metamodel-generator/src/test/java/org/hibernate/processor/test/uppercase/UppercaseTest.java b/tooling/metamodel-generator/src/test/java/org/hibernate/processor/test/uppercase/UppercaseTest.java new file mode 100644 index 000000000000..d765288a8246 --- /dev/null +++ b/tooling/metamodel-generator/src/test/java/org/hibernate/processor/test/uppercase/UppercaseTest.java @@ -0,0 +1,29 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.processor.test.uppercase; + +import org.hibernate.processor.test.util.CompilationTest; +import org.hibernate.processor.test.util.WithClasses; +import org.junit.Test; + +import static org.hibernate.processor.test.util.TestUtil.assertMetamodelClassGeneratedFor; +import static org.hibernate.processor.test.util.TestUtil.assertPresenceOfFieldInMetamodelFor; +import static org.hibernate.processor.test.util.TestUtil.getMetaModelSourceAsString; + +public class UppercaseTest extends CompilationTest { + + @Test + @WithClasses(value = Person.class) + public void test() { + System.out.println( getMetaModelSourceAsString( Person.class ) ); + + assertMetamodelClassGeneratedFor( Person.class ); + + assertPresenceOfFieldInMetamodelFor( Person.class, "SSN" ); + assertPresenceOfFieldInMetamodelFor( Person.class, "_SSN" ); + assertPresenceOfFieldInMetamodelFor( Person.class, "UserID" ); + assertPresenceOfFieldInMetamodelFor( Person.class, "USER_ID" ); + } +}