diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 2402d43b9f8d..7bd550051dd4 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -5,3 +5,14 @@ 9c7dd3bf34dfbb2f4fa9eb79a0294c2cc759c836 b2a528f2945cee26667b22b867ccef143b7c430c 4273f1cde06f3622f50c2846078398c1977a49c4 +# Hibernate Tools merging +05a9a8742e1684419ca4bcabcca060dc31048053 +3710fc0e8332992d7b87f66839dc489c4f46a8c8 +09f4af10843fc1d1e1dcd01d1f4856991fa8989d +c7cc700acfe372cf6e67d7e60b021a8bf08360c6 +6acfbba01701d767065ed87207269d96adbdc52f +bede74dc6f816065c78e31cd62ef16981dd90497 +75f4ce5d156e7987579eb32834667975d2d81ad4 +c448b6ba40c93c648b8d645d59ccf4bba55df567 +c57ae64b723fc6c7cb23a06a6b871869dca6ff3f +9580fd1af50a637431a7a51242cdcb35aa3c0fe8 diff --git a/.github/ci-prerequisites.sh b/.github/ci-prerequisites.sh index 4902f78ef2bc..23ed879416de 100755 --- a/.github/ci-prerequisites.sh +++ b/.github/ci-prerequisites.sh @@ -1,8 +1,14 @@ # Reclaim disk space, otherwise we only have 13 GB free at the start of a job +echo 'Before the cleanup:' +df -h # Remove the container images for node: -docker rmi node:10 node:12 mcr.microsoft.com/azure-pipelines/node8-typescript:latest +echo 'Docker images (available for possible removal):' +docker images # That is 18 GB sudo rm -rf /usr/share/dotnet # That is 1.2 GB -sudo rm -rf /usr/share/swift \ No newline at end of file +sudo rm -rf /usr/share/swift + +echo 'After the cleanup:' +df -h diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 16bf812c5d25..1e8c53ec7ac9 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -17,6 +17,8 @@ updates: allow: - dependency-name: "actions/*" - dependency-name: "redhat-actions/*" + - dependency-name: "graalvm/setup-graalvm" + - dependency-name: "oracle-actions/setup-testpilot" - package-ecosystem: "gradle" directory: "/" registries: diff --git a/.github/hibernate-github-bot.yml b/.github/hibernate-github-bot.yml index 26a48092d408..a0a954c885a0 100644 --- a/.github/hibernate-github-bot.yml +++ b/.github/hibernate-github-bot.yml @@ -81,3 +81,27 @@ branches: ignore: - user: dependabot[bot] titlePattern: ".*" +pullRequestTasks: + # Make the bot add list of tasks to the pull requests and enable the check that makes sure all tasks are completed: + enabled: true + tasks: + # List of tasks for commits without a Jira ID + # or for those with Jira ID but that don't have a specific configuration for a corresponding issue type: + default: + - Add test **OR** check there is no need for a test + - "Update documentation as relevant: javadoc for changed API, `documentation/src/main/asciidoc/userguide` for all features, `documentation/src/main/asciidoc/introduction` for main features, links from existing documentation" + - "Add entries as relevant to `migration-guide.adoc` (breaking changes) and `whats-new.adoc` (new features/improvements)" + # Tasks specific to the bug issue type: + bug: + - Add test reproducing the bug + - "Add entries as relevant to `migration-guide.adoc` **OR** check there are no breaking changes" + # Tasks specific to the improvement issue type: + improvement: + - Add tests for feature/improvement + - "Update documentation as relevant: javadoc for changed API, `documentation/src/main/asciidoc/userguide` for all features, `documentation/src/main/asciidoc/introduction` for main features, links from existing documentation" + - "Add entries as relevant to `migration-guide.adoc` (breaking changes) and `whats-new.adoc` (new features/improvements)" + # Tasks specific to the "new feature" issue type (same as improvement): + new feature: + - Add tests for feature/improvement + - "Update documentation as relevant: javadoc for changed API, `documentation/src/main/asciidoc/userguide` for all features, `documentation/src/main/asciidoc/introduction` for main features, links from existing documentation" + - "Add entries as relevant to `migration-guide.adoc` (breaking changes) and `whats-new.adoc` (new features/improvements)" \ No newline at end of file diff --git a/.github/workflows/ci-report.yml b/.github/workflows/ci-report.yml index 5a56b1c3d9e7..81f538cc1369 100644 --- a/.github/workflows/ci-report.yml +++ b/.github/workflows/ci-report.yml @@ -17,12 +17,12 @@ jobs: steps: # Checkout target branch which has trusted code - name: Check out target branch - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false ref: ${{ github.ref }} - name: Set up JDK - uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 with: distribution: 'temurin' java-version: '25' @@ -38,7 +38,7 @@ jobs: 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@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: path: | ~/.m2/repository/ @@ -52,7 +52,7 @@ jobs: - name: Download GitHub Actions artifacts for the Develocity build scans id: downloadBuildScan - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 with: pattern: build-scan-data-* github-token: ${{ github.token }} @@ -76,3 +76,116 @@ jobs: exit $status env: DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY_PR }} + + publish-sonar-scans: + name: Publish Sonar scan + if: github.repository == 'hibernate/hibernate-orm' && github.event.workflow_run.conclusion != 'cancelled' + runs-on: ubuntu-latest + steps: + - name: Determine the Branch Reference for which the original action was triggered + id: determine_branch_ref + env: + GH_TOKEN: ${{ github.token }} + run: | + if [ "${{ github.event.workflow_run.event }}" == "pull_request" ]; then + echo "::notice::Triggering workflow was executed for a pull request" + + FORK_OWNER="${{ github.event.workflow_run.head_repository.owner.login }}" + BRANCH_NAME="${{ github.event.workflow_run.head_branch }}" + if [ "${{ github.event.workflow_run.head_repository.owner.login }}" != "${{ github.event.workflow_run.repository.owner.login }}" ]; then + BRANCH_NAME="$FORK_OWNER:$BRANCH_NAME" + fi + GH_RESPONSE=$(gh pr view "$BRANCH_NAME" --repo ${{ github.event.workflow_run.repository.full_name }} --json number,baseRefName) + TARGET_BRANCH=$(echo $GH_RESPONSE | jq -r '.baseRefName') + PR_ID=$(echo $GH_RESPONSE | jq -r '.number') + + echo "::notice::PR found. Target branch is: $TARGET_BRANCH" + echo "::notice:: Pull Request number is: $PR_ID" + echo "::notice:: Branch to merge is: $BRANCH_NAME" + echo "original_branch_ref=$TARGET_BRANCH" >> "$GITHUB_OUTPUT" + echo "pr_id=$PR_ID" >> "$GITHUB_OUTPUT" + echo "branch_to_merge=$BRANCH_NAME" >> "$GITHUB_OUTPUT" + else + echo "::notice::Triggering workflow was executed for a push event? Using the head_branch value." + echo "original_branch_ref=${{ github.event.workflow_run.head_branch }}" >> "$GITHUB_OUTPUT" + fi + # Checkout target branch (from the main repository) + - name: Check out target branch + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + # By default, a workflow that is triggered with on workflow_run would run on the main (default) branch. + # Different branches might have different versions of Develocity, and we want to make sure + # that we publish with the one that we built the scan with in the first place. + ref: ${{ steps.determine_branch_ref.outputs.original_branch_ref }} + fetch-depth: 0 + + # Note: we need to check out the code with all the changes so that we have the sources, + # matching our compiled classes we'll pull from the build artifacts. + # We won't be running any builds from the checked out code, + # but we'll use the code to run the sonar scanner tool. + # + # Only needed if we are analysing the PR, + # as otherwise the previous checkout already did the work. + - name: Check out merged code (if PR) + env: + GH_TOKEN: ${{ github.token }} + run: | + if [ "${{ github.event.workflow_run.event }}" == "pull_request" ]; then + gh pr checkout ${{steps.determine_branch_ref.outputs.pr_id}} + fi + + # so we aren't tempted to run a Gradle command! + rm -rf gradlew* + + - name: Set up Java 25 + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + with: + java-version: 25 + distribution: temurin + + - name: Download coverage reports + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # 7.0.0 + with: + pattern: build-results-data + github-token: ${{ github.token }} + repository: ${{ github.repository }} + run-id: ${{ github.event.workflow_run.id }} + path: . + merge-multiple: 'true' + # Don't fail the build if there are no matching artifacts + continue-on-error: true + + - name: Install Sonar CLI + run: | + SONAR_HASH=8fbfb1eb546b734a60fc3e537108f06e389a8ca124fbab3a16236a8a51edcc15 + SONAR_SCANNER_VERSION=8.0.1.6346 + export SONAR_SCANNER_HOME=$HOME/.sonar/sonar-scanner-$SONAR_SCANNER_VERSION + curl --create-dirs -sSLo $HOME/.sonar/sonar-scanner.zip https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-$SONAR_SCANNER_VERSION.zip + DOWNLOADED_HASH=$(sha256sum $HOME/.sonar/sonar-scanner.zip | awk '{print $1}') + if [ "$DOWNLOADED_HASH" == "$SONAR_HASH" ]; then + echo "Successfully verified the file checksum" + else + echo "Error: Failed the file checksum verification. Expected: $SONAR_HASH but got $DOWNLOADED_HASH instead" + exit 1 + fi + unzip -o $HOME/.sonar/sonar-scanner.zip -d $HOME/.sonar/ + mv "$HOME/.sonar/sonar-scanner-$SONAR_SCANNER_VERSION"/* "$HOME/.sonar/" + + - name: Sonar Analysis + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: | + find . -name "*.exec" -type f + EXTRA_ARGS="" + if [ "${{ github.event.workflow_run.event }}" == "pull_request" ]; then + echo "::notice::Triggering workflow was executed for a pull request" + + EXTRA_ARGS="-Dsonar.pullrequest.branch=${{steps.determine_branch_ref.outputs.branch_to_merge}} -Dsonar.pullrequest.key=${{steps.determine_branch_ref.outputs.pr_id}} -Dsonar.pullrequest.base=${{steps.determine_branch_ref.outputs.original_branch_ref}} -Dsonar.pullrequest.provider=GitHub -Dsonar.pullrequest.github.repository=hibernate/hibernate-orm" + else + EXTRA_ARGS="-Dsonar.branch.name=${{github.event.workflow_run.head_branch}}" + fi + + $HOME/.sonar/bin/sonar-scanner $EXTRA_ARGS \ + -Dsonar.java.libraries="$(pwd)/target/sonar-dependencies/*.jar" \ + -Dsonar.coverage.jacoco.xmlReportPaths="$(pwd)/reporting/target/reports/jacoco/mergeCodeCoverageReport/mergeCodeCoverageReport.xml" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cfa18d3f560f..bda4e0797569 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,7 +54,7 @@ jobs: # 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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Reclaim Disk Space @@ -64,7 +64,7 @@ jobs: RDBMS: ${{ matrix.rdbms }} run: ci/database-start.sh - name: Set up Java 25 - uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 with: distribution: 'temurin' java-version: '25' @@ -81,16 +81,17 @@ jobs: 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@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 # 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/caches/ + !~/.gradle/caches/build-cache-* ~/.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. + # 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 }} @@ -98,14 +99,15 @@ jobs: ${{ 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@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 - # if it a pull request, we restore the cache but we don't save it + uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 + # if it is 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/caches/ + !~/.gradle/caches/build-cache-* ~/.gradle/wrapper/ key: ${{ steps.cache-key.outputs.buildtool-cache-key }} restore-keys: | @@ -126,14 +128,31 @@ jobs: # 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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 if: "${{ github.event_name == 'pull_request' && !cancelled() }}" with: name: build-scan-data-${{ matrix.rdbms }} path: ~/.gradle/build-scan-data - + - name: Store coverage report + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: build-coverage-data-${{ matrix.rdbms }} + retention-days: 1 + path: | + ./**/target/jacoco/*.exec + - name: Store build results + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + if: "${{ matrix.rdbms == 'h2' }}" + with: + name: build-compilation-data + retention-days: 1 + path: | + ./**/target/resources/ + ./**/target/classes/ + ./**/target/generated/ + .gradle/caches/build-cache-* - name: Upload test reports (if Gradle failed) - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 if: failure() with: name: test-reports-java11-${{ matrix.rdbms }} @@ -149,20 +168,22 @@ jobs: contents: read name: GraalVM 25 - ${{matrix.rdbms}} runs-on: [ self-hosted, Linux, X64, OracleTestPilot ] + if: github.repository == 'hibernate/hibernate-orm' strategy: fail-fast: false matrix: include: - #- rdbms: autonomous-transaction-processing-serverless + #- rdbms: autonomous-transaction-processing-serverless-26ai + #- rdbms: autonomous-transaction-processing-serverless-19c - rdbms: base-database-service-19c - rdbms: base-database-service-21c - - rdbms: base-database-service-23ai + - rdbms: base-database-service-26ai steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Set up Java 25 - uses: graalvm/setup-graalvm@aafbedb8d382ed0ca6167d3a051415f20c859274 # v1.2.8 + uses: graalvm/setup-graalvm@f744c72a42b1995d7b0cbc314bde4bace7ac1fe1 # v1.5.0 with: distribution: 'graalvm' java-version: '25' @@ -172,22 +193,23 @@ jobs: 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" + ROOT_CACHE_KEY="buildtool-cache-oracle-test-pilot" 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@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 # 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/caches/ + !~/.gradle/caches/build-cache-* ~/.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. + # 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 }} @@ -195,14 +217,15 @@ jobs: ${{ 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@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 - # if it is a pull request, we restore the cache but we don't save it + uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 + # if it is 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/caches/ + !~/.gradle/caches/build-cache-* ~/.gradle/wrapper/ key: ${{ steps.cache-key.outputs.buildtool-cache-key }} restore-keys: | @@ -210,11 +233,11 @@ jobs: ${{ steps.cache-key.outputs.buildtool-monthly-cache-key }}- - id: create_database - uses: loiclefevre/test@ce2f5049188a384c17ffcfcb8c8d04cf118e2cd7 # v1.0.20 + uses: oracle-actions/setup-testpilot@f620f11f9f26dacfe80ba1823342e3e92604c55f # v1.0.23 with: oci-service: ${{ matrix.rdbms }} action: create - user: hibernate_orm_test_1,hibernate_orm_test_2,hibernate_orm_test_3,hibernate_orm_test_4 + user: hibernate_orm_test_1,hibernate_orm_test_2,hibernate_orm_test_3,hibernate_orm_test_4,hibernate_orm_test_5,hibernate_orm_test_6,hibernate_orm_test_7,hibernate_orm_test_8 - name: Run build script env: @@ -228,28 +251,44 @@ jobs: # Needed for TFO (TCP fast open) LD_PRELOAD: /home/ubuntu/libtfojdbc1.so LD_LIBRARY_PATH: /home/ubuntu + # maximum number of worker (upper limit to maxParallelForks property, otherwise number of available CPUs) + GRADLE_OPTS: "-Dorg.gradle.workers.max=8" + # Better control of RAM for Gradle, must be aligned with gradle.properties + # Daemon: + ORG_GRADLE_JVMARGS: "-Dlog4j2.disableJmx -Xmx4g -XX:MaxMetaspaceSize=256m -XX:+HeapDumpOnOutOfMemoryError -Duser.language=en -Duser.country=US -Duser.timezone=UTC -Dfile.encoding=UTF-8" + # Java compiler: + ORG_GRADLE_PROJECT_toolchain.compiler.jvmargs: "-Dlog4j2.disableJmx=true -Xmx4g -XX:MaxMetaspaceSize=256m -XX:+HeapDumpOnOutOfMemoryError -Duser.language=en -Duser.country=US -Duser.timezone=UTC -Dfile.encoding=UTF-8" + # Gradle workers (Tests...) + ORG_GRADLE_PROJECT_toolchain.launcher.jvmargs: "-Dlog4j2.disableJmx=true -Xmx3g -XX:MaxMetaspaceSize=448m -XX:+HeapDumpOnOutOfMemoryError -Duser.language=en -Duser.country=US -Duser.timezone=UTC -Dfile.encoding=UTF-8" run: ./ci/build-github.sh shell: bash - - uses: loiclefevre/test@ce2f5049188a384c17ffcfcb8c8d04cf118e2cd7 # v1.0.20 + - uses: oracle-actions/setup-testpilot@f620f11f9f26dacfe80ba1823342e3e92604c55f # v1.0.23 if: always() with: oci-service: ${{ matrix.rdbms }} action: delete - user: hibernate_orm_test_1,hibernate_orm_test_2,hibernate_orm_test_3,hibernate_orm_test_4 + user: hibernate_orm_test_1,hibernate_orm_test_2,hibernate_orm_test_3,hibernate_orm_test_4,hibernate_orm_test_5,hibernate_orm_test_6,hibernate_orm_test_7,hibernate_orm_test_8 # 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. + # That's even on push, because we do not trust Oracle Test Pilot runners to hold secrets: they are shared infrastructure. - name: Upload GitHub Actions artifact for the Develocity build scan - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 if: "${{ !cancelled() }}" with: name: build-scan-data-${{ matrix.rdbms }} path: ~/.gradle/build-scan-data + - name: Store coverage report + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: build-coverage-data-${{ matrix.rdbms }} + retention-days: 1 + path: | + ./**/target/jacoco/*.exec - name: Upload test reports (if Gradle failed) - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 if: failure() with: name: test-reports-java11-${{ matrix.rdbms }} @@ -265,13 +304,13 @@ jobs: name: Static code analysis runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Reclaim disk space and sanitize user home run: .github/ci-prerequisites-atlas.sh - name: Set up Java 25 - uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 with: distribution: 'temurin' java-version: '25' @@ -282,22 +321,23 @@ jobs: 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" + 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@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 # 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/caches/ + !~/.gradle/caches/build-cache-* ~/.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. + # 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 }} @@ -305,14 +345,15 @@ jobs: ${{ 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@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 - # if it a pull request, we restore the cache but we don't save it + uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 + # if it is 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/caches/ + !~/.gradle/caches/build-cache-* ~/.gradle/wrapper/ key: ${{ steps.cache-key.outputs.buildtool-cache-key }} restore-keys: | @@ -331,14 +372,14 @@ jobs: # 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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 if: "${{ github.event_name == 'pull_request' && !cancelled() }}" with: name: build-scan-data-sca path: ~/.gradle/build-scan-data - name: Upload test reports (if Gradle failed) - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 if: failure() with: name: test-reports-java11-sca @@ -346,3 +387,80 @@ jobs: ./**/target/reports/tests/ - name: Omit produced artifacts from build cache run: ./ci/before-cache.sh + + prepare-sonar-bundle: + name: Prepare build bundle for Sonar scanner + needs: + - build + - otp + if: | + always() && !cancelled() + && needs.build.result != 'cancelled' && needs.otp.result != 'cancelled' + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up JDK 25 + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + with: + java-version: '25' + distribution: 'temurin' + + - 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@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 + with: + path: | + ~/.m2/repository/ + ~/.m2/wrapper/ + ~/.gradle/caches/ + !~/.gradle/caches/build-cache-* + ~/.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 compilation results + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # 7.0.0 + with: + name: build-compilation-data + path: . + # Don't fail the build if there are no matching artifacts (the build will re-compile things then) + continue-on-error: true + + - name: Download coverage reports + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # 7.0.0 + with: + pattern: build-coverage-data* + path: . + merge-multiple: 'true' + # Don't fail the build if there are no matching artifacts + continue-on-error: true + + - name: Merge Jacoco Reports + run: ./gradlew mergeCodeCoverageReport copyDependenciesSonar --no-parallel + + - name: Store build info for Sonar scanning + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: build-results-data + retention-days: 1 + path: | + ./**/target/jacoco/*.exec + ./**/target/classes/ + ./**/target/generated/ + ./**/target/resources/ + ./**/target/reports/ + ./**/target/sonar-dependencies/ diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index ee4e0e81c9f0..000000000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,79 +0,0 @@ -name: "CodeQL" - -on: - push: - branches: [ 'main' ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ 'main' ] - schedule: - - cron: '34 11 * * 4' - -# 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' || github.repository != 'hibernate/hibernate-orm' }} - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ 'java' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] - # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support - - steps: - - - name: Set up JDK - uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 - with: - distribution: 'temurin' - java-version: '25' - - - name: Checkout repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@7e3036b9cd87fc26dd06747b7aa4b96c27aaef3a # v3.28.4 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - - # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - queries: +security-and-quality - - - # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@7e3036b9cd87fc26dd06747b7aa4b96c27aaef3a # v3.28.4 - - # â„šī¸ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - - # If the Autobuild fails above, remove it and uncomment the following three lines. - # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. - - # - run: | - # echo "Run, Build Application using script" - # ./location_of_script_within_repo/buildscript.sh - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@7e3036b9cd87fc26dd06747b7aa4b96c27aaef3a # v3.28.4 - with: - category: "/language:${{matrix.language}}" diff --git a/.gitignore b/.gitignore index 38f13ef5aa12..8fa78041fb50 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,6 @@ ObjectStore # SDKman, used by some module maintainers .sdkmanrc + +# Sonar CLI local scan files: +.scannerwork diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9c7dbfd0c264..4baddffc1c65 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -36,7 +36,10 @@ While we try to keep requirements for contributing to a minimum, there are a few we ask that you mind. For code contributions, these guidelines include: -* Respect the project code style - find templates for [IntelliJ IDEA](https://hibernate.org/community/contribute/intellij-idea/) or [Eclipse](https://hibernate.org/community/contribute/eclipse-ide/) +* Respect the project code style: make sure to run spotless and checkstyle checks before commiting your changes, e.g. `./gradlew formatChecks`. +Project contains the basic set of formatting styles for IntelliJ IDEA in the [.idea](.idea) directory. +You can also refer to more generic [IntelliJ IDEA](https://hibernate.org/community/contribute/intellij-idea/) or [Eclipse](https://hibernate.org/community/contribute/eclipse-ide/) +guides for additional details. * Have a corresponding JIRA [issue](https://hibernate.atlassian.net/browse/HHH) and be sure to include the key for this JIRA issue in your commit messages. * Have a set of appropriate tests. For your convenience, a [set of test templates](https://github.com/hibernate/hibernate-test-case-templates/tree/main/orm) have been made available. diff --git a/Jenkinsfile b/Jenkinsfile index a09447ed07b6..ed042d2694f0 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -48,12 +48,13 @@ stage('Configure') { // 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: '25', testJdkLauncherArgs: '--enable-preview', additionalOptions: '-PskipJacoco=true' ), + new BuildEnvironment( testJdkVersion: '25', testJdkLauncherArgs: '--enable-preview' ), + new BuildEnvironment( testJdkVersion: '26', 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: '26', testJdkLauncherArgs: '--enable-preview -Dnet.bytebuddy.experimental=true', additionalOptions: '-PskipJacoco=true' ) + new BuildEnvironment( testJdkVersion: '27', testJdkLauncherArgs: '--enable-preview -Dnet.bytebuddy.experimental=true', additionalOptions: '-PskipJacoco=true' ) ]; if ( env.CHANGE_ID ) { @@ -69,6 +70,9 @@ stage('Configure') { if ( pullRequest.labels.contains( 'tidb' ) ) { this.environments.add( new BuildEnvironment( dbName: 'tidb', node: 'tidb', notificationRecipients: 'tidb_hibernate@pingcap.com' ) ) } + if ( pullRequest.labels.contains( 'informix' ) ) { + this.environments.add( new BuildEnvironment( dbName: 'informix' ) ) + } } helper.configure { @@ -167,6 +171,13 @@ stage('Build') { sh "./docker_db.sh cockroachdb" state[buildEnv.tag]['containerName'] = "cockroach" break; + case "informix": + sh "./docker_db.sh informix" + state[buildEnv.tag]['containerName'] = "informix" + // Disable parallel testing + state[buildEnv.tag]['additionalOptions'] = state[buildEnv.tag]['additionalOptions'] + + " -Ptest.threads=1" + break; } } stage('Test') { diff --git a/MAINTAINERS.md b/MAINTAINERS.md index 8a20f527688e..1f0ecd81860c 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -84,7 +84,7 @@ In any case, before the release: * Check there are no resolved/closed issues in the corresponding "work-in-progress version" (e.g. `6.6`, `6.6-next`, ... naming convention may vary); if there are, you might want to assign them to your release. -* Pull all upstream changes and perform `./gradlew preVerifyRelease` locally. +* Pull all upstream changes and perform `./gradlew releasePrepare` locally. **If it's the first `Alpha`/`Beta` of a new major or minor release**, before the release: @@ -163,3 +163,29 @@ In any case: * Reset the migration guide on the `main` branch if you forgot about it when preparing the release. * Create a maintenance branch for the previous series, if necessary; see [branching](branching.adoc). + + +### Setting up the maintenance branch + +Once the release series (e.g. 7.3) is branched out and goes into maintenance mode make sure to: +* Enable automated releases (for that branch) + - Update [Jenkinsfile](ci/release/Jenkinsfile) and switch `RELEASE_ON_SCHEDULE` to `true` +* Remove the nightly Jenkins job from that branch ([nightly.Jenkinsfile](nightly.Jenkinsfile)) +* Update GitHub workflows: + - For [ci.yml](.github/workflows/ci.yml) + + remove the branch push triggers (`on.pushbranches`) + + update branch in the pull request triggers +* Enable Quarkus testing job (if the update to this version was already merged(test against main Quarkus branch)/released(test the corresponding version branch)) + - In [quarkus.Jenkinsfile](ci/quarkus.Jenkinsfile) switch `ENABLE_QUARKUS_BUILDS` to true and update `QUARKUS_BRANCH_TO_TEST` as necessary. +* Enable Hibernate Search dependency update job + - In [Jenkinsfile](Jenkinsfile) add an execution just before the `parallel(executions)`: + ```groovy + executions.put('Hibernate Search Update Dependency', { + build job: '/hibernate-search-dependency-update/{HIBERNATE_SEARCH_VERSION}', propagate: true, parameters: [string(name: 'UPDATE_JOB', value: 'orm{HIBERNATE_ORM_VERSION}'), string(name: 'ORM_REPOSITORY', value: helper.scmSource.remoteUrl), string(name: 'ORM_PULL_REQUEST_ID', value: helper.scmSource.pullRequest.id)] + }) + ``` + - In this example above, replace `{HIBERNATE_SEARCH_VERSION}` and `{HIBERNATE_ORM_VERSION}` with actual release series, e.g. `8.3`/`7.3` +* Update main build [Jenkinsfile](Jenkinsfile) (for the branch) + - Enable JDK testing in the build by removing the conditions under `Don't build environments for newer JDKs` + - Stop running this build for pushes to the branch +* Update TCK job [jpa-3.2-tck.Jenkinsfile](ci/jpa-3.2-tck.Jenkinsfile) to always run for PRs and not for the pushes to the branch diff --git a/README.adoc b/README.adoc index cc38a7d3069a..442112065ee1 100644 --- a/README.adoc +++ b/README.adoc @@ -18,7 +18,7 @@ See link:MAINTAINERS.md#ci[MAINTAINERS.md] for information about CI. == Building from sources -The build requires at least JDK 21, and produces Java 17 bytecode. +The build requires at least JDK 25, and produces Java 17 bytecode. Hibernate uses https://gradle.org[Gradle] as its build tool. See the _Gradle Primer_ section below if you are new to Gradle. @@ -87,40 +87,19 @@ a JVM system prop (`-D`) or as a Gradle project property (`-P`). Examples below project property approach. ---- -gradle clean build -Pdb=pgsql +gradle clean build -Pdb=pgsql_ci ---- To run a test from your IDE, you need to ensure the property expansions happen. Use the following command: ---- -gradle clean compile -Pdb=pgsql +gradle clean compile -Pdb=pgsql_ci ---- __NOTE: If you are running tests against a JDBC driver that is not available via Maven central be sure to add these drivers to your local Maven repo cache (~/.m2/repository) or (better) add it to a personal Maven repo server__ -=== Running database-specific tests from the IDE using "profiles" - -You can run any test on any particular database that is configured in a `databases.gradle` profile. - -All you have to do is run the following command: - ----- -./gradlew setDataBase -Pdb=pgsql ----- - -or you can use the shortcut version: - ----- -./gradlew sDB -Pdb=pgsql ----- - -You can do this from the module which you are interested in testing or from the `hibernate-orm` root folder. - -Afterward, just pick any test from the IDE and run it as usual. Hibernate will pick the database configuration from the `hibernate.properties` -file that was set up by the `setDataBase` Gradle task. - === Starting test databases locally as docker containers You don't have to install all databases locally to be able to test against them in case you have docker available. diff --git a/build.gradle b/build.gradle index 9f5600731a06..b6422ba614fa 100644 --- a/build.gradle +++ b/build.gradle @@ -19,18 +19,18 @@ buildscript { plugins { id "local.module" - id "org.hibernate.build.version-injection" version "2.2.0" apply false + id "org.hibernate.build.version-injection" version "2.3.0" apply false id 'org.hibernate.orm.database-service' apply false - id 'biz.aQute.bnd' version '7.1.0' apply false + id 'biz.aQute.bnd' version '7.2.1' apply false - id "com.diffplug.spotless" version "7.0.4" - id 'org.checkerframework' version '0.6.61' + id "com.diffplug.spotless" version "8.3.0" + id 'org.checkerframework' version '1.0.2' id 'org.hibernate.orm.build.jdks' id 'io.github.gradle-nexus.publish-plugin' version '2.0.0' id 'idea' - id 'org.jetbrains.gradle.plugin.idea-ext' version '1.1.10' + id 'org.jetbrains.gradle.plugin.idea-ext' version '1.4.1' id 'eclipse' id "com.dorongold.task-tree" version "4.0.1" } @@ -40,8 +40,8 @@ plugins { // Releasing tasks.register( 'releasePrepare' ) { - group "release-prepare" - description "Scripted release 'Release Prepare' stage. " + + group = "release-prepare" + description = "Scripted release 'Release Prepare' stage. " + "Includes various checks as to the publish-ability of the project: testing, generation, etc. " + "Sub-projects register their own `releasePrepare` to hook into this stage." // See `:release:releasePrepare` which does a lot of heavy lifting here @@ -77,3 +77,21 @@ idea { name = "hibernate-orm" } } + +tasks.register('copyDependenciesSonar', Copy) { + description = "Aggregates all runtime dependencies for Sonar CLI analysis." + + def targetProjects = subprojects.findAll { it.name != 'reporting' } + + targetProjects.each { sub -> + evaluationDependsOn(sub.path) + + if (sub.plugins.hasPlugin('java')) { + from(sub.configurations.runtimeClasspath) + } + } + + into(layout.buildDirectory.dir("sonar-dependencies")) + + duplicatesStrategy = DuplicatesStrategy.EXCLUDE +} diff --git a/changelog.txt b/changelog.txt index 5335460b4196..9fb6cfb476cb 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,6 +1,153 @@ Hibernate 7 Changelog ======================= +Changes in 7.3.0.CR2 (February 03, 2026) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/37404 + + +** Bug + * HHH-20121 NPE when logging loaded values in follow-on locking post action + * HHH-20118 Vector operator SQL templates miss parenthesis around + * HHH-20107 SparseFloatVector and SparseDoubleVector accept invalid size <= 0 + * HHH-20103 @ElementCollection mapping should respect @JoinColumn(foreignKey) + * HHH-20102 spec says LockTimeoutException + QueryTimeoutException should not be thrown on PostgreSQL + * HHH-20101 Error persisting child entity of abstract generic entity + * HHH-19715 read-only mode and collections + +** Improvement + * HHH-20127 Avoid initializing the BigDecimal class when not strictly necessary + * HHH-19902 PostgreSQL failed statements mark tx for rollback + * HHH-19610 Make GraphParser support treatedSubGraph from root + +** New Feature + * HHH-19045 Bytecode enhancer should add a default constructor if it's missing + +** Sub-task + * HHH-20112 Skip concurrent modification related tests + * HHH-20111 Avoid harcoded column definition in tests + * HHH-20110 Handle TIMESTAMP and NUMERIC column types in Spanner PG + * HHH-20109 Handle Column types for Spanner PG + * HHH-20108 Fix connection URL in Spanner PG + * HHH-20100 Fix Foreign key issue + * HHH-20099 Handle ON CONFLICT clause for Spanner + * HHH-20098 Support FORWARD_ONLY scrollable resultset + +Changes in 7.3.0.CR1 (January 23, 2026) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/35980 + + +** Bug + * HHH-20095 New SchemaValidator nullability check should only consider explicitly declared nullability + * HHH-20088 HbmXmlTransform does not consider the , , , and outer-join value to determine the fetch mode. + * HHH-20087 NPE with StatelessSession + Bean Validation + * HHH-20079 HbmXmlTransform does not create for ManyToMany attributes + * HHH-20072 Javadoc cannot be generated without Jackson 3 dependencies + * HHH-20069 `DB2iDialect.rowId` causes an error in merge queries + * HHH-20055 is ignored in orm.xml + * HHH-20041 DB2 for z IN tuple list predicate performs badly + * HHH-20037 MappedSuperClasses can be enhanced more than once resulting in Duplicate annotation interface org...EnhancementInfo Exception + * HHH-20032 SubSequence.subSequence violates CharSequence contract for start == end == length() + * HHH-20027 Fix failing parsing of PostgreSQL canonical lock_timeout formats (0, ms, s, min, h) + * HHH-20021 Binding GregorianCalendar parameter fails with "Type registration was corrupted" + * HHH-20015 Hibernate Maven Plugin 7.x does not include maven project dependencies in the enhancement classpath + * HHH-20006 Outcome of getSingleResult changed since 6.0 + * HHH-20002 Ensure physical JDBC connection is released when closing LogicalConnectionManagedImpl + * HHH-19999 Caching APIs use Comparator for version comparison in Hibernate ORM 7.2.0.Final + * HHH-19932 NullPointerException in SqmInterpretationsKey::toString + * HHH-19929 DB2iDialect problem with supportsRowValueConstructorSyntaxInInSubQuery + * HHH-19861 EntityNotFoundException when using @Audited(targetAuditMode = RelationTargetAuditMode.NOT_AUDITED) on FK fields + * HHH-19390 obsolete documentation for bytecode enhancer + * HHH-19192 Bulk delete of owner with soft-delete element-collection physically deletes the collection rows + * HHH-18835 AssertionError when executing insert-select + * HHH-18040 MySQL update/delete statement issue with subqueries + * HHH-12678 PersistentClass.checkColumnDuplication fails for columns with same name from distinct tables + * HHH-12100 Misleading exception message in JTASessionContext + * HHH-9035 Globally quoted identifiers and id class not working properly with quoted join columns error Unable to find logical column name from physical name JobPositon+id in table JOB+Position + * HHH-7287 Problem in caching proper natural-id-values when obtaining result by naturalIdQuery + +** Deprecation + * HHH-20073 deprecate ByteArrayJavaType and CharacterArrayJavaType + +** Improvement + * HHH-20076 @Version columns should be NOT NULL + * HHH-20059 TiDB: don't propagate readonly to server + * HHH-20058 TiDB: Enable shared lock promotion + * HHH-20054 Optimize BasicTypeImpl allocation + * HHH-20030 FilterImpl: cache validate() results and harden deserialization restore + * HHH-20026 Stop hibernate-maven-plugin execution on errors + * HHH-20025 Align Generator with UserType + * HHH-20017 Refactor AnnotationBasedGenerator to align with AnnotationBasedUserType + * HHH-20013 remove dependency to Classmate + * HHH-20012 Update the TiDBDialect + * HHH-20011 Upgrade minmum version supported for cockroach and upgrade db testing to 25 + * HHH-20009 docker_db.sh fails to start PostgreSQL 17 and 18 on Apple Silicon (ARM64) + * HHH-19997 Update TiDB testing environment + * HHH-19992 auto-generate check constraint on @OrderColumn + * HHH-19950 Alternative to pre-set parameters offered for deprecated DynamicParameterizedType + * HHH-19919 Indexed collection initializers should resolveInstance instead of resolveKey + * HHH-19897 Hibernate Envers audited associations mapped-by non-audited ones + * HHH-19890 Support a FormatMapper for Jackson3 + * HHH-19867 Add support for Oracle 26ai in OTP + * HHH-19804 Documentation should recommend `annotationProcessorPathsUseDepMgmt` in maven-compiler-plugin when using hibernate-processor + * HHH-18984 Remove deprecated Gradle API usage in Hibernate Gradle Plugin + * HHH-18461 Improve dialects to generate sql contains offset clause if first result of native query is explicit 0 + * HHH-14584 Allow PhysicalNamingStrategy implementations to detect when a name is implicit or explicit + * HHH-7202 Early detection of bad targetEntity + * HHH-6882 Expose CollectionPersister from AbstractCollectionEvent + * HHH-6598 Immutable entities should not have up-to-date checks performed on a flush + +** New Feature + * HHH-20060 TenantCredentialsMapper + * HHH-20029 hibernate.connection.login_timeout + * HHH-19993 Introduce UserTypeCreationContext and AnnotationBasedUserType + * HHH-19989 RemovalsMode.EXCLUDE + * HHH-19978 Support type variable members also in abstract entities + * HHH-19880 Move Hibernate Tools' `hibernate-assistant` module to Hibernate ORM + * HHH-19826 Add array_reverse and array_sort functions + * HHH-19541 "exists" queries + * HHH-18998 Supply UserType and UserCollectionType with type-safe config via annotation + * HHH-17657 Support named enum types on h2 + * HHH-16383 NaturalIdClass + +** Sub-task + * HHH-20091 Fix CTE Rendering Syntax for Spanner + * HHH-20086 Handle no escape character in Spanner PG dialect + * HHH-20066 Support only JSON Aggregation for Spanner PG Dialect + * HHH-20050 Create Spanner PostgreSQL dialect + * HHH-20045 Add support to create unique index when unique column is not supported + * HHH-20038 Fixes for SpannerDialect-3 + * HHH-20034 Fixes for SpannerDialect-2 + * HHH-20033 Introduce Dialect#getSetOperatorSqlString to support Spanner's explicit DISTINCT requirement + * HHH-20003 Spanner temporary table exporter + * HHH-19991 Fixes for SpannerDialect + * HHH-19983 Config for running tests on Spanner emualtor + +Changes in 7.2.0.CR4 (December 10, 2025) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/36476 + + +** Bug + * HHH-19980 In JTA after-completion callbacks may get ignored + * HHH-19979 processor should handle @NamedEntityGraph with defaulted name + * HHH-19975 Calling entityManager.find(clazz, id) with null id throws NullPointerException + * HHH-19972 OptionalTableUpdateOperation can fail on PostgreSQL < 15 and CockroachDB + * HHH-19963 Wrong references in entity fields with circular associations + * HHH-19958 `` tag in orm.xml is not implemented + * HHH-19955 Thread-safety issue in EntityEntryContext resulting in NullPointerException for Session.contains() calls. + * HHH-19843 Bean Validation may fail on operations with stateless session + * HHH-18871 Nested NativeQuery mappings causing 'Could not locate TableGroup' exception after migration + * HHH-18217 StatelessSession.upsert() for entity with all-null non-id fields, or no non-id field + +** Improvement + * HHH-19943 Comparison of generic nested EmbeddedId's fails for JPQL and Criteria API + * HHH-19215 Extends Dialect#addQueryHints to support straight_join syntax + Changes in 7.2.0.CR3 (November 25, 2025) ------------------------------------------------------------------------------------------------------------------------ diff --git a/ci/build.sh b/ci/build.sh index ef52ed93dd94..d4da98efa8f8 100755 --- a/ci/build.sh +++ b/ci/build.sh @@ -3,28 +3,40 @@ goal= if [ "$RDBMS" == "h2" ] || [ "$RDBMS" == "" ]; then # This is the default. - # - special check for Jenkins CI jobs where we don't want to run preVerifyRelease - if [[ -n "$CI_SYSTEM" && "$CI_SYSTEM" != "jenkins" ]]; then - goal="preVerifyRelease" - # Settings needed for `preVerifyRelease` execution - for asciidoctor doc rendering - export 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' - fi -elif [ "$RDBMS" == "hsqldb" ] || [ "$RDBMS" == "hsqldb_2_6" ]; then + # - special check for Jenkins CI jobs where we don't want to run releasePrepare + if [[ "$CI_SYSTEM" != "jenkins" ]]; then + goal="releasePrepare" + # Settings needed for `releasePrepare` execution - for asciidoctor doc rendering + export 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' + fi +elif [ "$RDBMS" == "hsqldb" ]; then goal="-Pdb=hsqldb" -elif [ "$RDBMS" == "mysql" ] || [ "$RDBMS" == "mysql_8_0" ]; then +elif [ "$RDBMS" == "hsqldb_2_6" ]; then + goal="-Pdb=hsqldb -PdbVersion=2.6" +elif [ "$RDBMS" == "mysql" ]; then goal="-Pdb=mysql_ci" -elif [ "$RDBMS" == "mariadb" ] || [ "$RDBMS" == "mariadb_10_6" ]; then +elif [ "$RDBMS" == "mysql_8_0" ]; then + goal="-Pdb=mysql_ci -PdbVersion=8.0" +elif [ "$RDBMS" == "mariadb" ]; then goal="-Pdb=mariadb_ci" -elif [ "$RDBMS" == "postgresql" ] || [ "$RDBMS" == "postgresql_13" ]; then +elif [ "$RDBMS" == "mariadb_10_6" ]; then + goal="-Pdb=mariadb_ci -PdbVersion=10.6" +elif [ "$RDBMS" == "postgresql" ]; then goal="-Pdb=pgsql_ci" +elif [ "$RDBMS" == "postgresql_14" ]; then + goal="-Pdb=pgsql_ci -PdbVersion=14" elif [ "$RDBMS" == "gaussdb" ]; then goal="-Pdb=gaussdb -DdbHost=localhost:8000" -elif [ "$RDBMS" == "edb" ] || [ "$RDBMS" == "edb_13" ]; then +elif [ "$RDBMS" == "edb" ]; then goal="-Pdb=edb_ci -DdbHost=localhost:5444" +elif [ "$RDBMS" == "edb_14" ]; then + goal="-Pdb=edb_ci -DdbHost=localhost:5444 -PdbVersion=14" elif [ "$RDBMS" == "oracle" ]; then goal="-Pdb=oracle_ci" -elif [ "$RDBMS" == "oracle_xe" ] || [ "$RDBMS" == "oracle_21" ]; then - goal="-Pdb=oracle_xe_ci" +elif [ "$RDBMS" == "oracle_xe" ]; then + goal="-Pdb=oracle_xe_ci -PdbVersion=18" +elif [ "$RDBMS" == "oracle_21" ]; then + goal="-Pdb=oracle_xe_ci -PdbVersion=21" elif [ "$RDBMS" == "oracle_atps_tls" ]; then echo "Managing Oracle Autonomous Database..." export INFO=$(curl -s -k -L -X GET "https://api.atlas-controller.oraclecloud.com/ords/atlas/admin/database?type=autonomous&hostname=`hostname`" -H 'accept: application/json') @@ -56,15 +68,32 @@ elif [ "$RDBMS" == "oracle_db23c" ]; then export SERVICE=$(echo $INFO | jq -r '.database' | jq -r '.service') goal="-Pdb=oracle_cloud_db23c -DrunID=$RUNID -DdbHost=$HOST -DdbService=$SERVICE" # OTP -elif [ "$RDBMS" == "autonomous-transaction-processing-serverless" ] || [ "$RDBMS" == "base-database-service-19c" ] || [ "$RDBMS" == "base-database-service-21c" ] || [ "$RDBMS" == "base-database-service-23ai" ]; then +elif [ "$RDBMS" == "autonomous-transaction-processing-serverless-19c" ]; then echo "Managing OTP Database..." - goal="-Pdb=oracle_test_pilot_database -DrunID=$RUNID -DdbPassword=$TESTPILOT_PASSWORD -DdbConnectionStringSuffix=$TESTPILOT_CONNECTION_STRING_SUFFIX" + goal="-Pdb=oracle_test_pilot_database -PdbVersion=atps-19 -DrunID=$RUNID -DdbPassword=$TESTPILOT_PASSWORD -DdbConnectionStringSuffix=$TESTPILOT_CONNECTION_STRING_SUFFIX" +elif [ "$RDBMS" == "autonomous-transaction-processing-serverless-26ai" ]; then + echo "Managing OTP Database..." + goal="-Pdb=oracle_test_pilot_database -PdbVersion=atps-26 -DrunID=$RUNID -DdbPassword=$TESTPILOT_PASSWORD -DdbConnectionStringSuffix=$TESTPILOT_CONNECTION_STRING_SUFFIX" +elif [ "$RDBMS" == "autonomous-transaction-processing-serverless" ]; then + echo "Managing OTP Database..." + goal="-Pdb=oracle_test_pilot_database -PdbVersion=atps -DrunID=$RUNID -DdbPassword=$TESTPILOT_PASSWORD -DdbConnectionStringSuffix=$TESTPILOT_CONNECTION_STRING_SUFFIX" +elif [ "$RDBMS" == "base-database-service-19c" ]; then + echo "Managing OTP Database..." + goal="-Pdb=oracle_test_pilot_database -PdbVersion=19 -DrunID=$RUNID -DdbPassword=$TESTPILOT_PASSWORD -DdbConnectionStringSuffix=$TESTPILOT_CONNECTION_STRING_SUFFIX" +elif [ "$RDBMS" == "base-database-service-21c" ]; then + echo "Managing OTP Database..." + goal="-Pdb=oracle_test_pilot_database -PdbVersion=21 -DrunID=$RUNID -DdbPassword=$TESTPILOT_PASSWORD -DdbConnectionStringSuffix=$TESTPILOT_CONNECTION_STRING_SUFFIX" +elif [ "$RDBMS" == "base-database-service-26ai" ]; then + echo "Managing OTP Database..." + goal="-Pdb=oracle_test_pilot_database -PdbVersion=26 -DrunID=$RUNID -DdbPassword=$TESTPILOT_PASSWORD -DdbConnectionStringSuffix=$TESTPILOT_CONNECTION_STRING_SUFFIX" elif [ "$RDBMS" == "db2" ]; then goal="-Pdb=db2_ci" elif [ "$RDBMS" == "db2_11_5" ]; then goal="-Pdb=db2_old_ci" -elif [ "$RDBMS" == "mssql" ] || [ "$RDBMS" == "mssql_2017" ]; then +elif [ "$RDBMS" == "mssql" ]; then goal="-Pdb=mssql_ci" +elif [ "$RDBMS" == "mssql_2017" ]; then + goal="-Pdb=mssql_ci -PdbVersion=2017" # Exclude some Sybase tests on CI because they use `xmltable` function which has a memory leak on the DB version in CI elif [ "$RDBMS" == "sybase" ]; then goal="-Pdb=sybase_ci -PexcludeTests=**.GenerateSeriesTest*" diff --git a/ci/jpa-3.2-tck.Jenkinsfile b/ci/jpa-3.2-tck.Jenkinsfile index c617e8337618..4f7cf120c68a 100644 --- a/ci/jpa-3.2-tck.Jenkinsfile +++ b/ci/jpa-3.2-tck.Jenkinsfile @@ -31,9 +31,9 @@ pipeline { } parameters { choice(name: 'IMAGE_JDK', choices: ['jdk17', 'jdk25'], description: 'The JDK base image version to use for the TCK image.') - string(name: 'TCK_VERSION', defaultValue: '3.2.0', description: 'The version of the Jakarta JPA TCK i.e. `2.2.0` or `3.0.1`') - string(name: 'TCK_SHA', defaultValue: '', 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: 'https://www.eclipse.org/downloads/download.php?file=/ee4j/jakartaee-tck/jakartaee11/staged/eftl/jakarta-persistence-tck-3.2.0.zip&mirror_id=1', 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.') + string(name: 'TCK_VERSION', defaultValue: '3.2.1', description: 'The version of the Jakarta JPA TCK i.e. `2.2.0` or `3.0.1`') + string(name: 'TCK_SHA', defaultValue: '1d282675f43fa13cf8ab2537d6dbfb1e1c95f7b838ab7cdd053e185c363a6519', 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: 'https://download.eclipse.org/jakartaee/persistence/3.2/jakarta-persistence-tck-3.2.1.zip', 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.') choice(name: 'RDBMS', choices: ['postgresql','mysql','mssql','oracle','db2','sybase'], description: 'The JDK base image version to use for the TCK image.') } stages { diff --git a/ci/quarkus.Jenkinsfile b/ci/quarkus.Jenkinsfile new file mode 100644 index 000000000000..ebb51d32fb49 --- /dev/null +++ b/ci/quarkus.Jenkinsfile @@ -0,0 +1,207 @@ +import groovy.transform.Field + +@Library('hibernate-jenkins-pipeline-helpers') _ + +@Field final String ORM_JDK_VERSION = '25' +@Field final String QUARKUS_JDK_VERSION = '17' +@Field final String ORM_JDK_TOOL = "OpenJDK ${ORM_JDK_VERSION} Latest" +@Field final String QUARKUS_JDK_TOOL = "OpenJDK ${QUARKUS_JDK_VERSION} Latest" +@Field final String QUARKUS_BRANCH_TO_TEST = 'main' + +// When enabling Quarkus builds make sure to check the version/branch to test. See `QUARKUS_BRANCH_TO_TEST`. +def ENABLE_QUARKUS_BUILDS = false + +// 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 +} +// 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 +} + +def manuallyTriggered = currentBuild.getBuildCauses().toString().contains( 'UserIdCause' ) + +// allow to bypass the check if the build is triggered manually: +if (!manuallyTriggered && !ENABLE_QUARKUS_BUILDS) { + print "INFO: Build skipped because we are not yet testing a next LTS version of Quarkus." + currentBuild.result = 'NOT_BUILT' + return +} + +void runBuildOnNode(String label, Closure body) { + node( label ) { + pruneDockerContainers() + tryFinally(body, { + cleanWs() + pruneDockerContainers() + }) + } +} + +// 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 + } +} + +class BuildConfiguration { + String name + String projects + boolean nativeProfile = false +} + +// See data category from https://github.com/quarkusio/quarkus/blob/main/.github/native-tests.json +def configurations = [ + new BuildConfiguration( name: "JVM test", projects: "!integration-tests/kafka-oauth-keycloak,!integration-tests/kafka-sasl-elytron,!integration-tests/hibernate-search-orm-opensearch,!integration-tests/hibernate-search-orm-elasticsearch-outbox-polling,!integration-tests/hibernate-search-orm-elasticsearch-tenancy,!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"), + new BuildConfiguration( name: "Data1", nativeProfile: true, projects: "jpa-h2, jpa-h2-embedded, jpa-mariadb, jpa-mssql, jpa-without-entity, hibernate-orm-tenancy/datasource, hibernate-orm-tenancy/connection-resolver, hibernate-orm-tenancy/connection-resolver-legacy-qualifiers"), + new BuildConfiguration( name: "Data2", nativeProfile: true, projects: "jpa, jpa-mapping-xml/legacy-app, jpa-mapping-xml/modern-app, jpa-mysql, jpa-db2, jpa-oracle"), + new BuildConfiguration( name: "Data3", nativeProfile: true, projects: "hibernate-orm-panache, hibernate-orm-panache-kotlin, hibernate-orm-envers, hibernate-orm-rest-data-panache"), + // Skipped because we only care about one project, which we added to Data3 + //new BuildConfiguration( name: "Data4", nativeProfile: true, projects: "hibernate-orm-rest-data-panache"), + new BuildConfiguration( name: "Data5", nativeProfile: true, projects: "jpa-postgresql, jpa-postgresql-withxml, hibernate-reactive-postgresql, hibernate-orm-tenancy/schema, hibernate-orm-tenancy/schema-mariadb"), + // Ignoring, because they don't bring much here: hibernate-search-orm-elasticsearch-tenancy, hibernate-search-orm-opensearch, hibernate-search-standalone-elasticsearch, hibernate-search-standalone-opensearch + new BuildConfiguration( name: "Data6", nativeProfile: true, projects: "hibernate-search-orm-elasticsearch, hibernate-search-orm-elasticsearch-outbox-polling"), + new BuildConfiguration( name: "Data7", nativeProfile: true, projects: "hibernate-reactive-db2, hibernate-reactive-mariadb, hibernate-reactive-mssql, hibernate-reactive-mysql, hibernate-reactive-mysql-agroal-flyway, hibernate-reactive-panache, hibernate-reactive-panache-kotlin, hibernate-reactive-oracle") +] + +pipeline { + agent none + options { + buildDiscarder(logRotator(numToKeepStr: '3', artifactNumToKeepStr: '3')) + disableConcurrentBuilds(abortPrevious: true) + skipDefaultCheckout() + } + stages { + stage('Checks') { + steps { + requireApprovalForPullRequest 'hibernate' + } + } + stage('Build Hibernate ORM') { + agent { + label 'LongDuration' + } + tools { + jdk ORM_JDK_TOOL + } + steps { + script { + dir('hibernate') { + checkout scm + sh "./gradlew clean publishToMavenLocal -x test --no-scan --no-daemon --no-build-cache --stacktrace -PmavenMirror=nexus-load-balancer-c4cf05fd92f43ef8.elb.us-east-1.amazonaws.com -Dmaven.repo.local=${env.WORKSPACE_TMP}/.m2repository" + script { + env.HIBERNATE_VERSION = sh ( + script: "grep hibernateVersion gradle/version.properties|cut -d'=' -f2", + returnStdout: true + ).trim() + } + } + dir(env.WORKSPACE_TMP) { + stash name: 'repository', includes: ".m2repository/" + } + } + } + } + stage('Build Quarkus') { + agent { + label 'LongDuration' + } + tools { + jdk QUARKUS_JDK_TOOL + maven 'Apache Maven 3.9' + } + steps { + script { + Map executions = [:] + + configurations.each { BuildConfiguration configuration -> + executions.put(configuration.name, { + node( 'LongDuration' ) { + dir(env.WORKSPACE_TMP) { + unstash "repository" + } + // Workaround issues when path contains @ character + dir(env.WORKSPACE) { + // Remove previous soft-link if it is around + sh "rm .m2 || true" + sh "ln -s ${env.WORKSPACE_TMP}/.m2repository .m2" + } + dir('quarkus') { + sh "git clone -b ${QUARKUS_BRANCH_TO_TEST} --single-branch https://github.com/quarkusio/quarkus.git . || git reset --hard && git clean -fx && git pull" + script { + def sedStatus = sh (script: "sed -i 's@.*@${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 + '/.m2', publisherStrategy: 'EXPLICIT') { + def javaHome = tool(name: QUARKUS_JDK_TOOL, type: 'jdk') + // to account for script-only maven wrapper use in Quarkus: + withEnv(["JAVA_HOME=${javaHome}", "PATH+JAVA=${javaHome}/bin", "MAVEN_ARGS=${env.MAVEN_ARGS?:""} ${env.MAVEN_CONFIG}"]) { + 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 projects = configuration.projects + def additionalArguments + def additionalOptions + if ( configuration.nativeProfile ) { + additionalArguments = "-f integration-tests" + additionalOptions = "-Dquarkus.native.native-image-xmx=6g -Dnative -Dnative.surefire.skip -Dno-descriptor-tests" + } + else { + additionalArguments = "-pl :quarkus-hibernate-orm -amd" + additionalOptions = "" + } + sh "TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED=true ./mvnw -Dinsecure.repositories=WARN ${additionalArguments} -pl '${projects}' verify -Dstart-containers -Dtest-containers -Dskip.gradle.build ${additionalOptions}" + } + } + } + } + }) + } + parallel executions + } + } + } + } + post { + always { + notifyBuildResult maintainers: "andrea@hibernate.org steve@hibernate.org christian.beikov@gmail.com mbellade@redhat.com" + } + } +} diff --git a/ci/release/Jenkinsfile b/ci/release/Jenkinsfile index 5278d3463e4d..dc3d0f0bd9b1 100644 --- a/ci/release/Jenkinsfile +++ b/ci/release/Jenkinsfile @@ -222,15 +222,9 @@ pipeline { withEnv([ "DISABLE_REMOTE_GRADLE_CACHE=true" ]) { - def notesFiles = findFiles(glob: 'release_notes.md') - if ( notesFiles.length < 1 ) { - throw new IllegalStateException( "Could not locate `release_notes.md`" ) - } - if ( notesFiles.length > 1 ) { - throw new IllegalStateException( "Located more than 1 `release_notes.md`" ) - } + def ghReleaseNote = sh(script: 'realpath -e release_notes.md 2>/dev/null || true', returnStdout: true).trim() - sh ".release/scripts/publish.sh -j --notes=${notesFiles[0].path} ${env.SCRIPT_OPTIONS} ${env.PROJECT} ${env.RELEASE_VERSION} ${env.DEVELOPMENT_VERSION} ${env.GIT_BRANCH} " + sh ".release/scripts/publish.sh -j ${ghReleaseNote != '' ? '--notes=' + ghReleaseNote : ''} ${env.SCRIPT_OPTIONS} ${env.PROJECT} ${env.RELEASE_VERSION} ${env.DEVELOPMENT_VERSION} ${env.GIT_BRANCH} " } } } diff --git a/design/logger_id_ranges.adoc b/design/logger_id_ranges.adoc index 6ef47008a7ed..cfaaecd4dcb5 100644 --- a/design/logger_id_ranges.adoc +++ b/design/logger_id_ranges.adoc @@ -56,7 +56,7 @@ well as some helper info such as whether DEBUG or TRACE are enabled. E.g.: public interface MappingModelCreationLogger extends BasicLogger { String LOGGER_NAME = "org.hibernate.orm.model.mapping.creation"; - MappingModelCreationLogger LOGGER = Logger.getMessageLogger( MappingModelCreationLogger.class, LOGGER_NAME ); + MappingModelCreationLogger LOGGER = Logger.getMessageLogger( MappingModelCreationLogger.class, LOGGER_NAME, Locale.ROOT ); boolean TRACE_ENABLED = LOGGER.isTraceEnabled(); boolean DEBUG_ENABLED = LOGGER.isDebugEnabled(); diff --git a/docker_db.sh b/docker_db.sh index bf8d8b088076..ddbaad2d1685 100755 --- a/docker_db.sh +++ b/docker_db.sh @@ -33,7 +33,7 @@ else fi mysql() { - mysql_9_4 + mysql_9_6 } mysql_8_0() { @@ -61,6 +61,16 @@ mysql_9_4() { mysql_setup "9.4" "$init_connect" } +mysql_9_5() { + local init_connect="--init-connect=SET character_set_client='utf8mb4';SET character_set_results='utf8mb4';SET character_set_connection='utf8mb4';SET collation_connection='utf8mb4_0900_as_cs';" + mysql_setup "9.5" "$init_connect" +} + +mysql_9_6() { + local init_connect="--init-connect=SET character_set_client='utf8mb4';SET character_set_results='utf8mb4';SET character_set_connection='utf8mb4';SET collation_connection='utf8mb4_0900_as_cs';" + mysql_setup "9.6" "$init_connect" +} + # Generic MySQL function that handles all versions mysql_setup() { local version=$1 @@ -130,6 +140,10 @@ mysql_setup() { echo "MySQL is ready" fi + # Install components + # file://component_classic_hashing - This is for legacy hashing algorithms on MySQL 9.6: SHA1 and MD5. + $CONTAINER_CLI exec mysql bash -c "mysql -u root -phibernate_orm_test -e \"INSTALL COMPONENT 'file://component_classic_hashing'\"" 2>/dev/null + databases=() for n in $(seq 1 $DB_COUNT) do @@ -144,7 +158,7 @@ mysql_setup() { } mariadb() { - mariadb_12_0 + mariadb_12_2 } mariadb_wait_until_start() @@ -196,6 +210,18 @@ mariadb_12_0() { mariadb_setup } +mariadb_12_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 --tmpfs /var/lib/mysql -d ${DB_IMAGE_MARIADB_12_1:-docker.io/mariadb:12.1.2} --character-set-server=utf8mb4 --collation-server=utf8mb4_bin --skip-character-set-client-handshake --lower_case_table_names=2 + mariadb_setup +} + +mariadb_12_2() { + $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 --tmpfs /var/lib/mysql -d ${DB_IMAGE_MARIADB_12_2:-docker.io/mariadb:12.2.2} --character-set-server=utf8mb4 --collation-server=utf8mb4_bin --skip-character-set-client-handshake --lower_case_table_names=2 + mariadb_setup +} + 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 --tmpfs /var/lib/mysql -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 @@ -215,24 +241,22 @@ mariadb_setup() { create_cmd+="create database ${databases[i]}; grant all privileges on ${databases[i]}.* to 'hibernate_orm_test'@'%';" done $CONTAINER_CLI exec mariadb bash -c "mariadb -u root -phibernate_orm_test -e \"${create_cmd}\"" 2>/dev/null - echo "MySQL databases were successfully setup" + echo "MariaDB databases were successfully setup" } +POSTGRESQL_PLATFORM_OPTION="" +if [[ "$IS_OSX" == "true" ]]; then + # PostGIS images only support amd64, so we force emulation on macOS + POSTGRESQL_PLATFORM_OPTION="--platform linux/amd64" +fi + postgresql() { postgresql_18 } -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 --tmpfs /var/lib/postgresql/data -d ${DB_IMAGE_POSTGRESQL_13:-docker.io/postgis/postgis:13-3.1} \ - -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-13-pgvector' - postgresql_setup -} - 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 --tmpfs /var/lib/postgresql/data -d ${DB_IMAGE_POSTGRESQL_14:-docker.io/postgis/postgis:14-3.3} \ + $CONTAINER_CLI run --name postgres ${POSTGRESQL_PLATFORM_OPTION} -e POSTGRES_USER=hibernate_orm_test -e POSTGRES_PASSWORD=hibernate_orm_test -e POSTGRES_DB=hibernate_orm_test -p5432:5432 --tmpfs /var/lib/postgresql/data -d ${DB_IMAGE_POSTGRESQL_14:-docker.io/postgis/postgis:14-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-14-pgvector' postgresql_setup @@ -240,7 +264,7 @@ postgresql_14() { 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 /var/lib/postgresql/data -d ${DB_IMAGE_POSTGRESQL_15:-docker.io/postgis/postgis:15-3.3} \ + $CONTAINER_CLI run --name postgres ${POSTGRESQL_PLATFORM_OPTION} -e POSTGRES_USER=hibernate_orm_test -e POSTGRES_PASSWORD=hibernate_orm_test -e POSTGRES_DB=hibernate_orm_test -p5432:5432 --tmpfs /var/lib/postgresql/data -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' postgresql_setup @@ -248,7 +272,7 @@ postgresql_15() { 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 /var/lib/postgresql/data -d ${DB_IMAGE_POSTGRESQL_16:-docker.io/postgis/postgis:16-3.4} \ + $CONTAINER_CLI run --name postgres ${POSTGRESQL_PLATFORM_OPTION} -e POSTGRES_USER=hibernate_orm_test -e POSTGRES_PASSWORD=hibernate_orm_test -e POSTGRES_DB=hibernate_orm_test -p5432:5432 --tmpfs /var/lib/postgresql/data -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' postgresql_setup @@ -256,7 +280,7 @@ postgresql_16() { postgresql_17() { $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 /var/lib/postgresql/data -d ${DB_IMAGE_POSTGRESQL_17:-docker.io/postgis/postgis:17-3.5} \ + $CONTAINER_CLI run --name postgres ${POSTGRESQL_PLATFORM_OPTION} -e POSTGRES_USER=hibernate_orm_test -e POSTGRES_PASSWORD=hibernate_orm_test -e POSTGRES_DB=hibernate_orm_test -p5432:5432 --tmpfs /var/lib/postgresql/data -d ${DB_IMAGE_POSTGRESQL_17:-docker.io/postgis/postgis:17-3.5} \ -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-17-pgvector' postgresql_setup @@ -264,7 +288,7 @@ postgresql_17() { postgresql_18() { $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 /var/lib/postgresql -d ${DB_IMAGE_POSTGRESQL_17:-docker.io/postgis/postgis:18-3.6} \ + $CONTAINER_CLI run --name postgres ${POSTGRESQL_PLATFORM_OPTION} -e POSTGRES_USER=hibernate_orm_test -e POSTGRES_PASSWORD=hibernate_orm_test -e POSTGRES_DB=hibernate_orm_test -p5432:5432 --tmpfs /var/lib/postgresql -d ${DB_IMAGE_POSTGRESQL_18:-docker.io/postgis/postgis:18-3.6} \ -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-18-pgvector' postgresql_setup @@ -331,17 +355,6 @@ edb() { edb_17 } -edb_13() { - $CONTAINER_CLI rm -f edb || true - if [[ -z "${DB_IMAGE_EDB}" ]]; then - DB_IMAGE_EDB="edb-test:13" - # We need to build a derived image because the existing image is mainly made for use by a kubernetes operator - (cd edb; $CONTAINER_CLI build -t edb-test:13 -f edb13.Dockerfile .) - fi - $CONTAINER_CLI run --name edb -e POSTGRES_USER=hibernate_orm_test -e POSTGRES_PASSWORD=hibernate_orm_test -e POSTGRES_DB=hibernate_orm_test -p 5444:5444 -d $DB_IMAGE_EDB - edb_setup -} - edb_14() { $CONTAINER_CLI rm -f edb || true if [[ -z "${DB_IMAGE_EDB}" ]]; then @@ -813,6 +826,7 @@ oracle_setup() { for i in "${!users[@]}";do create_cmd+=" create user ${users[i]} identified by hibernate_orm_test quota unlimited on users; +grant create session to ${users[i]}; grant all privileges to ${users[i]};" done @@ -876,6 +890,7 @@ alter tablespace SYSTEM nologging; alter tablespace SYSAUX nologging; create user hibernate_orm_test identified by hibernate_orm_test quota unlimited on users; +grant create session to hibernate_orm_test; grant all privileges to hibernate_orm_test; ${create_cmd} EOF\"" @@ -907,7 +922,9 @@ oracle_free_setup() { for i in "${!users[@]}";do create_cmd+=" create user ${users[i]} identified by hibernate_orm_test quota unlimited on users; -grant all privileges to ${users[i]};" +grant create session to ${users[i]}; +grant create domain to ${users[i]}; +grant all privileges on schema ${users[i]} to ${users[i]};" done # We increase file sizes to avoid online resizes as that requires lots of CPU which is restricted in XE @@ -969,7 +986,9 @@ alter tablespace SYSTEM nologging; alter tablespace SYSAUX nologging; create user hibernate_orm_test identified by hibernate_orm_test quota unlimited on users; -grant all privileges to hibernate_orm_test; +grant create session to hibernate_orm_test; +grant create domain to hibernate_orm_test; +grant all privileges on schema hibernate_orm_test to hibernate_orm_test; ${create_cmd} EOF\"" } @@ -1081,7 +1100,7 @@ oracle_18() { --health-interval 5s \ --health-timeout 5s \ --health-retries 10 \ - ${DB_IMAGE_ORACLE_21:-docker.io/gvenzl/oracle-xe:18.4.0} + ${DB_IMAGE_ORACLE_18:-docker.io/gvenzl/oracle-xe:18.4.0} oracle_setup } @@ -1137,11 +1156,71 @@ hana() { sleep 10 OUTPUT=$($PRIVILEGED_CLI $CONTAINER_CLI logs hana 2>&1) done + hana_setup echo "HANA successfully started" } +hana_setup() { + databases=() + for n in $(seq 1 $DB_COUNT) + do + databases+=("hibernate_orm_test_${n}") + done + create_cmd= + for i in "${!databases[@]}";do + create_cmd+=" +create user ${databases[i]} password H1bernate_test NO FORCE_FIRST_PASSWORD_CHANGE; +grant create schema to ${databases[i]};" + done + # The first command seems to be ignored?! So let's just run something useless + $CONTAINER_CLI exec hana bash -c "cat <&1) + done + echo "Enabling experimental box2d operators and some optimized settings for running the tests" + #settings documented in https://www.cockroachlabs.com/docs/v24.1/local-testing#use-a-local-single-node-cluster-with-in-memory-storage + $CONTAINER_CLI exec cockroach bash -c "cat <&1) + if [[ $OUTPUT == *"server is running"* ]]; then + break; + fi + n=$((n+1)) + echo "Waiting for TiDB to start..." + sleep 5 + done + + if [ "$n" -gt 15 ]; then + echo "TiDB failed to start after 75 seconds" + exit 1 + else + echo "TiDB successfully started" + fi + + # Wait for TiDB to accept connections + n=0 + until [ "$n" -gt 10 ]; do + if $CONTAINER_CLI run --rm --network container:tidb docker.io/mysql:8.0 \ + mysqladmin -h 127.0.0.1 -P 4000 -uroot ping >/dev/null 2>&1; then + break; + fi + n=$((n+1)) + echo "Waiting for TiDB to be ready..." + sleep 3 + done + + if [ "$n" -gt 10 ]; then + echo "TiDB failed to become ready after 30 seconds" + exit 1 + else + echo "TiDB is ready" + fi + + # Create databases + databases=() + for n in $(seq 1 $DB_COUNT) + do + databases+=("hibernate_orm_test_${n}") + done + create_cmd= + + # Since v7.2 + # https://docs.pingcap.com/tidb/stable/system-variables/#tidb_enable_check_constraint-new-in-v720 + create_cmd+="SET GLOBAL tidb_enable_check_constraint=ON;" + + # Since v8.3 + # https://docs.pingcap.com/tidb/stable/system-variables/#tidb_enable_shared_lock_promotion-new-in-v830 + create_cmd+="SET GLOBAL tidb_enable_shared_lock_promotion=ON;" + + create_cmd+="CREATE DATABASE IF NOT EXISTS hibernate_orm_test;" + create_cmd+="CREATE USER IF NOT EXISTS 'hibernate_orm_test'@'%' IDENTIFIED BY 'hibernate_orm_test';" + create_cmd+="GRANT ALL ON hibernate_orm_test.* TO 'hibernate_orm_test'@'%';" + for i in "${!databases[@]}";do + create_cmd+="CREATE DATABASE IF NOT EXISTS ${databases[i]}; GRANT ALL ON ${databases[i]}.* TO 'hibernate_orm_test'@'%';" + done + $CONTAINER_CLI run --rm --network container:tidb docker.io/mysql:8.0 \ + mysql -h 127.0.0.1 -P 4000 -uroot -e "${create_cmd}" 2>/dev/null + echo "TiDB databases were successfully setup" } tidb_5_4() { @@ -1312,12 +1461,43 @@ tidb_5_4() { } informix() { - informix_14_10 + informix_15 +} + +informix_15() { + temp_dir=$(mktemp -d) + echo "ALLOW_NEWLINE 1 +USEOSTIME 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 -v $temp_dir:/opt/ibm/config -e LICENSE=accept -e GL_USEGLU=1 -d ${DB_IMAGE_INFORMIX_15:-icr.io/informix/informix-developer-edition-database:15.0.0.0} + echo "Starting Informix. This can take a few minutes" + # Give the container some time to start + OUTPUT= + n=0 + until [ "$n" -ge 5 ] + do + OUTPUT=$($PRIVILEGED_CLI $CONTAINER_CLI logs informix 2>&1) + if [[ $OUTPUT == *"Server Started"* ]]; then + sleep 15 + $PRIVILEGED_CLI $CONTAINER_CLI exec informix bash -l -c "export DB_LOCALE=en_US.utf8;export CLIENT_LOCALE=en_US.utf8;echo \"execute function task('create dbspace from storagepool', 'datadbs', '100 MB', '4');execute function task('create sbspace from storagepool', 'sbspace', '20 M', '0');create database dev in datadbs with log;\" > post_init.sql;dbaccess sysadmin post_init.sql" + break; + fi + n=$((n+1)) + echo "Waiting for Informix to start..." + sleep 30 + done + if [ "$n" -ge 5 ]; then + echo "Informix failed to start and configure after 5 minutes" + else + echo "Informix successfully started" + fi } informix_14_10() { temp_dir=$(mktemp -d) - echo "ALLOW_NEWLINE 1" >$temp_dir/onconfig.mod + echo "ALLOW_NEWLINE 1 +USEOSTIME 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 -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} @@ -1370,13 +1550,122 @@ informix_12_10() { fi } +spanner() { + spanner_emulator GOOGLE_STANDARD_SQL +} + +spanner_pg() { + spanner_emulator POSTGRESQL +} + +spanner_emulator() { + local dialect=${1:-GOOGLE_STANDARD_SQL} + local emulator_image=${SPANNER_EMULATOR:-gcr.io/cloud-spanner-emulator/emulator:1.5.52} + if [[ $DB_COUNT -gt 8 ]]; then + DB_COUNT=4 + else + DB_COUNT=$(( DB_COUNT / 2 )) + fi + # Start all emulator containers first + for n in $(seq 1 ${DB_COUNT}); do + local container_name="spanner_${n}" + local port=$((9010 + n)) + local rest_port=$((9020 + n)) + + echo "Starting Spanner emulator instance ${n} on port ${port}..." + $CONTAINER_CLI rm -f ${container_name} || true + + $CONTAINER_CLI run --name ${container_name} -d \ + -p ${port}:9010 \ + -p ${rest_port}:9020 \ + ${emulator_image} + done + + # Wait for all emulators to be ready + for n in $(seq 1 ${DB_COUNT}); do + local container_name="spanner_${n}" + local port=$((9010 + n)) + + local retries=0 + until [ "$retries" -ge 20 ]; do + local logs="$($CONTAINER_CLI logs ${container_name} 2>&1 || true)" + if [[ "$logs" == *"gRPC server listening"* ]] || [[ "$logs" == *"Cloud Spanner emulator running"* ]]; then + echo "Cloud Spanner emulator (${container_name}) started on port ${port}." + break + fi + echo "Waiting for Cloud Spanner emulator (${container_name}) to start..." + retries=$((retries+1)) + sleep 3 + done + + if [ "$retries" -ge 20 ]; then + echo "Cloud Spanner emulator (${container_name}) failed to start" + exit 1 + fi + done + + # Configure Instance + for n in $(seq 1 ${DB_COUNT}); do + local rest_port=$((9020 + n)) + echo "Configuring Spanner emulator instance ${n} on port ${rest_port}..." + local host="localhost:${rest_port}" + local create_statement + + # Create Instance + curl -s -X POST "http://${host}/v1/projects/orm-test-project/instances" \ + -H "Content-Type: application/json" \ + -d '{ + "instanceId": "orm-test-instance", + "instance": { + "config": "emulator-config", + "displayName": "Test Instance", + "nodeCount": 1 + } + }' >/dev/null || true + + # Determine Create Database statement based on dialect + if [[ "$dialect" == "POSTGRESQL" ]]; then + create_statement="CREATE DATABASE \"orm-test-db\"" + else + create_statement="CREATE DATABASE \`orm-test-db\`" + fi + + # Create Database + curl -s -X POST "http://${host}/v1/projects/orm-test-project/instances/orm-test-instance/databases" \ + -H "Content-Type: application/json" \ + -d "{ + \"createStatement\": \"${create_statement//\"/\\\"}\", + \"databaseDialect\": \"${dialect}\" + }" >/dev/null + + # Update DDL (for Timezone) + local update_statements="" + if [[ "$dialect" == "POSTGRESQL" ]]; then + update_statements='"ALTER DATABASE \"orm-test-db\" SET \"spanner.default_time_zone\" = '"'UTC'"'", "ALTER DATABASE \"orm-test-db\" SET \"spanner.version_retention_period\" = '"'10s'"'"' + else + update_statements='"ALTER DATABASE `orm-test-db` SET OPTIONS ( default_time_zone = '"'UTC'"', version_retention_period = '"'10s'"' )"' + fi + + curl -s -X PATCH "http://${host}/v1/projects/orm-test-project/instances/orm-test-instance/databases/orm-test-db/ddl" \ + -H "Content-Type: application/json" \ + -d '{ + "statements": [ + '"${update_statements}"' + ] + }' >/dev/null || true + done +} + if [ -z ${1} ]; then echo "No db name provided" echo "Provide one of:" echo -e "\tcockroachdb" + echo -e "\tcockroachdb_25_4" + echo -e "\tcockroachdb_24_3" echo -e "\tcockroachdb_24_1" - echo -e "\tcockroachdb_23_1" + echo -e "\tcockroachdb_23_2" echo -e "\tdb2" + echo -e "\tdb2_12_1" echo -e "\tdb2_11_5" echo -e "\tdb2_spatial" echo -e "\tedb" @@ -1384,18 +1673,23 @@ if [ -z ${1} ]; then echo -e "\tedb_16" echo -e "\tedb_15" echo -e "\tedb_14" - echo -e "\tedb_13" echo -e "\thana" echo -e "\tmariadb" echo -e "\tmariadb_verylatest" + echo -e "\tmariadb_12_2" + echo -e "\tmariadb_12_1" + echo -e "\tmariadb_12_0" echo -e "\tmariadb_11_8" echo -e "\tmariadb_11_4" echo -e "\tmariadb_10_11" echo -e "\tmariadb_10_6" echo -e "\tmssql" + echo -e "\tmssql_2025" echo -e "\tmssql_2022" echo -e "\tmssql_2017" echo -e "\tmysql" + echo -e "\tmysql_9_6" + echo -e "\tmysql_9_5" echo -e "\tmysql_9_4" echo -e "\tmysql_9_2" echo -e "\tmysql_8_2" @@ -1404,19 +1698,30 @@ if [ -z ${1} ]; then echo -e "\toracle" echo -e "\toracle_23" echo -e "\toracle_21" + echo -e "\toracle_18" + echo -e "\toracle_atps" + echo -e "\toracle_atps_tls" + echo -e "\toracle_db19c" + echo -e "\toracle_db21c" + echo -e "\toracle_db23c" echo -e "\tgaussdb" echo -e "\tpostgresql" + echo -e "\tpostgresql_18" echo -e "\tpostgresql_17" echo -e "\tpostgresql_16" echo -e "\tpostgresql_15" echo -e "\tpostgresql_14" - echo -e "\tpostgresql_13" echo -e "\tsybase" echo -e "\ttidb" + echo -e "\ttidb_8_5" echo -e "\ttidb_5_4" - echo -e "\informix" - echo -e "\informix_14_10" - echo -e "\informix_12_10" + echo -e "\tinformix" + echo -e "\tinformix_15" + echo -e "\tinformix_14_10" + echo -e "\tinformix_12_10" + echo -e "\tspanner" + echo -e "\tspanner_pg" + echo -e "\tspanner_emulator" else ${1} fi diff --git a/documentation/documentation.gradle b/documentation/documentation.gradle index 0501ba2172b4..2609d1e5a959 100644 --- a/documentation/documentation.gradle +++ b/documentation/documentation.gradle @@ -39,6 +39,13 @@ def jpaVersion = ormBuildDetails.jpaVersion defaultTasks 'buildDocs' configurations { + // hibernate-ant -> hibernate-reveng -> google-java-format -> guava + // guava publishes JRE/Android variants that Gradle's reportAggregation + // configuration cannot resolve; guava is not needed for report aggregation + reportAggregation { + exclude group: 'com.google.guava', module: 'guava' + } + core testing @@ -168,8 +175,10 @@ dependencies { javadocClasspath jdbcLibs.postgresql javadocClasspath jdbcLibs.edb javadocClasspath libs.jackson + javadocClasspath libs.jackson3 javadocClasspath gradleApi() javadocClasspath libs.jacksonXml + javadocClasspath libs.jackson3Xml javadocClasspath jdbcLibs.oracle javadocClasspath jdbcLibs.oracleJdbcJacksonOsonExtension } diff --git a/documentation/src/main/asciidoc/introduction/Advanced.adoc b/documentation/src/main/asciidoc/introduction/Advanced.adoc index e874f867d68d..02a43e0b5556 100644 --- a/documentation/src/main/asciidoc/introduction/Advanced.adoc +++ b/documentation/src/main/asciidoc/introduction/Advanced.adoc @@ -277,6 +277,7 @@ To make use of multi-tenancy, we'll usually need to set at least one of these co | link:{doc-javadoc-url}org/hibernate/cfg/MultiTenancySettings.html#MULTI_TENANT_IDENTIFIER_RESOLVER[`hibernate.tenant_identifier_resolver`] | Specifies the `CurrentTenantIdentifierResolver` | link:{doc-javadoc-url}org/hibernate/cfg/MultiTenancySettings.html#MULTI_TENANT_SCHEMA_MAPPER[`hibernate.multi_tenant.schema_mapper`] | Specifies the `TenantSchemaMapper` for schema-based multi-tenancy +| link:{doc-javadoc-url}org/hibernate/cfg/MultiTenancySettings.html#MULTI_TENANT_CREDENTIALS_MAPPER[`hibernate.multi_tenant.credentials_mapper`] | Specifies the `TenantCredentialsMapper` for schema-based or discriminator-based multi-tenancy | link:{doc-javadoc-url}org/hibernate/cfg/MultiTenancySettings.html#MULTI_TENANT_CONNECTION_PROVIDER[`hibernate.multi_tenant_connection_provider`] | Specifies the `MultiTenantConnectionProvider` for database-based multi-tenancy |=== @@ -311,7 +312,10 @@ If your source of JDBC connections is a set of JNDI-bound ``DataSource``s, you m The second option is to keep all the data for different tenants in the same database, giving each tenant a different named database schema with its own set of tables. -In this case we must supply a link:{doc-javadoc-url}org/hibernate/context/spi/TenantSchemaMapper.html[`TenantSchemaMapper`] which is responsible for mapping from tenant ids to schema names. +- If all tenants connect to the database using the same credentials, we must supply a link:{doc-javadoc-url}org/hibernate/context/spi/TenantSchemaMapper.html[`TenantSchemaMapper`] which is responsible for mapping tenant ids to schema names. + +- If each tenant should use distinct credentials to connect to the database, and if your JDBC `DataSource` supports this (not all do!) then we can use a link:{doc-javadoc-url}org/hibernate/context/spi/TenantCredentialsMapper.html[`TenantCredentialsMapper`] to supply the username and password of each tenant. +If each database user has a different default schema, we might not need a `TenantSchemaMapper`. [discrete] ==== Discriminator-based multi-tenancy @@ -886,6 +890,17 @@ On the other hand, the following annotations specify how a collection should be Under the covers, Hibernate uses a `TreeSet` or `TreeMap` to maintain the collection in sorted order. +[CAUTION] +==== +The unowned (`mappedBy`) side of a bidirectional association is not responsible for specifying column mappings. +So it's wrong in principle to use `@OrderColumn` or `@MapKeyColumn` on the unowned side of an association mapping. +But for unowned collections, we may use `@OrderBy` or `@MapKey` instead. +That is: + +- You can use `@OrderColumn` or `@MapKeyColumn` with an `@ElementCollection`, owned `@ManyToMany`, or owned `@OneToMany`. +- But use `@OrderBy` or `@MapKey` when it's an _unowned_ `@ManyToMany` or `@OneToMany`. +==== + [[any]] === Any mappings @@ -954,22 +969,9 @@ There are a number of annotations which are useful to express this sort of compl | `@JoinColumn` | Specifies the foreign key column |=== -Of course, `@Any` mappings are disfavored, except in extremely special cases, since it's much more difficult to enforce referential integrity at the database level. - -There's also currently some limitations around querying `@Any` associations in HQL. -This is allowed: +As a general principle, an `@Any` association shares the semantics of `@ManyToAny`, and, since Hibernate 7.4, may be used just like a `@ManyToOne` in HQL and so on. -[source,hql] ----- -from Order ord - join CashPayment cash - on id(ord.payment) = cash.id ----- - -[CAUTION] -==== -Polymorphic association joins for `@Any` mappings are not currently implemented. -==== +Of course, `@Any` mappings are disfavored, except in extremely special cases, since it's much more difficult to enforce referential integrity at the database level. Further information may be found in the {any-doc}[User Guide]. @@ -1039,6 +1041,9 @@ Hibernate's {enhancer}[bytecode enhancer] enables the following features: - _attribute-level lazy fetching_ for basic attributes annotated `@Basic(fetch=LAZY)` and for lazy non-polymorphic associations, - _interception-based_—instead of the usual _snapshot-based_—detection of modifications. +In addition, use of the bytecode enhancer relaxes the usual requirement that entity and embeddable classes have default constructors. +If a class annotated `@Entity`, `@MappedSuperclass`, or `@Embeddable` has no default constructor, the bytecode enhancer will add it. + To use the bytecode enhancer, we must add the Hibernate plugin to our gradle build: [source,groovy,subs="attributes+"] diff --git a/documentation/src/main/asciidoc/introduction/Configuration.adoc b/documentation/src/main/asciidoc/introduction/Configuration.adoc index 26af305533cd..e9a412b316e0 100644 --- a/documentation/src/main/asciidoc/introduction/Configuration.adoc +++ b/documentation/src/main/asciidoc/introduction/Configuration.adoc @@ -116,8 +116,8 @@ and `org.ehcache:ehcache` and `com.github.ben-manes.caffeine:jcache` | Distributed second-level cache support via {infinispan}[Infinispan] | `org.infinispan:infinispan-hibernate-cache-v60` // | SCRAM authentication support for PostgreSQL | `com.ongres.scram:client:2.1` -| A JSON serialization library for working with JSON datatypes, for example, {jackson}[Jackson] or {yasson}[Yasson] | -`com.fasterxml.jackson.core:jackson-databind` + +| A JSON serialization library for working with JSON datatypes, for example, {jackson}[Jackson 2], {jackson3}[Jackson 3] or {yasson}[Yasson] | +`com.fasterxml.jackson.core:jackson-databind`, `tools.jackson.core:jackson-databind` + or `org.eclipse:yasson` | <> | `org.hibernate.orm:hibernate-spatial` | <>, for auditing historical data | `org.hibernate.orm:hibernate-envers` diff --git a/documentation/src/main/asciidoc/introduction/Entities.adoc b/documentation/src/main/asciidoc/introduction/Entities.adoc index 8ad97a494825..f972c41b63f6 100644 --- a/documentation/src/main/asciidoc/introduction/Entities.adoc +++ b/documentation/src/main/asciidoc/introduction/Entities.adoc @@ -39,6 +39,11 @@ On the other hand, the entity class may be either concrete or `abstract`, and it An entity class may be a `static` inner class. ==== +[TIP] +==== +The requirement for a default constructor is relaxed when the <> is used. +==== + Every entity class must be annotated `@Entity`. [source,java] @@ -526,6 +531,21 @@ Hibernate automatically generates a `UNIQUE` constraint on the columns mapped by Consider using the natural id attributes to implement <>. ==== +In cases where the natural id is defined by multiple attributes, Hibernate also offers the link:{doc-javadoc-url}org/hibernate/annotations/NaturalIdClass.html[`@NaturalIdClass`] annotation which acts similarly to the Jakarta Persistence `@IdClass` annotation for <> - + +[source,java] +---- +record BookKey(String isbn, int printing) {} + +@Entity +@NaturalIdClass(BookKey.class) +class Book { + ... +} +---- + +See the link:{doc-user-guide-url}#naturalid[User Guide] for more details about natural ids. + The payoff for doing this extra work, as we will see <>, is that we can take advantage of optimized natural id lookups that make use of the second-level cache. Note that even when you've identified a natural key, we still recommend the use of a generated surrogate key in foreign keys, since this makes your data model _much_ easier to change. @@ -586,6 +606,7 @@ Hibernate slightly extends this list with the following types: | Additional date/time types | `java.time` | `Duration`, `ZoneId`, `ZoneOffset`, and even `ZonedDateTime` | JDBC LOB types | `java.sql` | `Blob`, `Clob`, `NClob` | Java class object | `java.lang` | `Class` +| Internet addresses | `java.net` | `InetAddress` | Miscellaneous types | `java.util` | `Currency`, `Locale`, `URL`, `TimeZone` |==== @@ -1235,14 +1256,6 @@ For example, if the collection `Publisher.books` was stored in the second-level That said, it's _not_ a hard requirement to update the unowned side, at least if you're sure you know what you're doing. -[TIP] -// .Unidirectional `@OneToMany`? -==== -In principle Hibernate _does_ allow you to have a unidirectional one-to-many, that is, a `@OneToMany` with no matching `@ManyToOne` on the other side. -In practice, this mapping is unnatural, and just doesn't work very well. -Avoid it. -==== - Here we've used `Set` as the type of the collection, but Hibernate also allows the use of `List` or `Collection` here, with almost no difference in semantics. In particular, the `List` may not contain duplicate elements, and its order will not be persistent. @@ -1385,7 +1398,7 @@ If the association is bidirectional, we annotate the unowned side `@OneToOne(map === Many-to-many A unidirectional many-to-many association is represented as a collection-valued attribute. -It always maps to a separate _association table_ in the database. +It always maps to a separate <> in the database. It tends to happen that a many-to-many association eventually turns out to be an entity in disguise. @@ -1459,6 +1472,34 @@ We don't much employ the word "never" when it comes to object/relational mapping **never** write `@ManyToMany(fetch=EAGER)` unless you're deliberately looking for trouble. ==== +[[unidirectional-one-to-many]] +=== Unidirectional one-to-many + +Finally, Hibernate allows a unidirectional one-to-many association, that is, a `@OneToMany` with no matching `@ManyToOne` on the other side. + +[source,java] +---- +@OneToMany // no matching @ManyToOne +Set books; +---- + +By default, a unidirectional one-to-many association maps to a separate <>. +It therefore more closely resembles a many-to-many association than a many-to-one association. + +[CAUTION] +==== +If you're new to Hibernate, this is not the association mapping strategy you're looking for. +Use a <> instead. +Previous versions of this Guide advised against the use of these mappings completely. +==== + +Unidirectional one-to-many association mappings are only useful in one very specific circumstance. +Suppose we have a `Comment` entity, which has many different "parents" -- there can be comments on ``Document``s, comments on ``Issue``s, comments on ``Request``s, and so on. +These aren't many-to-many associations; each given `Comment` belongs to exactly one parent. +But on the other hand, it doesn't make sense to add a field for each kind of parent entity to the `Comment` class. +Nor does it make sense to add a foreign key column for each parent entity to the `COMMENTS` table. +This is the only scenario in which we can imagine ourselves using a unidirectional one-to-many. + [[collections]] === Collections of basic values and embeddable objects @@ -1521,7 +1562,7 @@ Almost every many-valued relationship should map to a foreign key association be And almost every table should be mapped by an entity class. The features we're about to meet in the next two subsections are used much more often by beginners than they're used by experts. -So if you're a beginner, you'll save yourself same hassle by staying away from these features for now. +So if you're a beginner, you'll save yourself some hassle by staying away from these features for now. ==== We'll talk about `@Array` mappings first. @@ -1642,11 +1683,51 @@ The code above results in a table with three columns: Instead of a surrogate primary key, it has a composite key comprising the foreign key of `Event` and the order column. -When—inevitably—we find that we need to add a fourth column to that table, our Java code must change completely. +When--inevitably--we find that we need to add a fourth column to that table, our Java code must change completely. Most likely, we'll realize that we need to add a separate entity after all. So this mapping isn't very robust in the face of minor changes to our data model. ==== +[CAUTION] +==== +Consider the following very simple mapping: +[source,java] +---- +@ElementCollection +Set topics; +---- +Hibernate would generate the following DDL: +[source,sql] +---- +create table Book_topics ( + Book_isbn varchar(255) not null, + topics varchar(255) +) +---- +Here, there's no `not null` constraint on one of the columns. +As a result, Hibernate cannot add a primary key to the table. +This is not a great data model. + +We can fix this problem with a `@Column` annotation: +[source,java] +---- +@ElementCollection +@Column(nullable=false) +Set topics; +---- +Now the generated DDL looks a lot better: +[source,sql] +---- +create table Book_topics ( + Book_isbn varchar(255) not null, + topics varchar(255) not null, + primary key (Book_isbn, topics) +) +---- +Of course, with this mapping we can't have `null` as an element of our `Set`. +But we can't think of a good reason why you might want that or what it would even mean for a set to contain null. +==== + There's much more we could say about "element collections", but we won't say it, because we don't want to hand you the gun you'll shoot your foot with. [[entities-summary]] diff --git a/documentation/src/main/asciidoc/introduction/Interacting.adoc b/documentation/src/main/asciidoc/introduction/Interacting.adoc index f6ac49148954..68eb7284528f 100644 --- a/documentation/src/main/asciidoc/introduction/Interacting.adoc +++ b/documentation/src/main/asciidoc/introduction/Interacting.adoc @@ -276,6 +276,7 @@ Modifications are automatically detected when the session is <>. On the other hand, except for `getReference()`, the following operations all result in immediate access to the database: +[[methods-for-reading]] .Methods for reading and locking data [%breakable,cols="30,~"] |=== @@ -368,9 +369,30 @@ The following code results in a single SQL `select` statement: List books = session.findMultiple(Book.class, bookIds); ---- +[[load-by-natural-id]] +As discussed <>, Hibernate offers the ability to map a natural id and perform load operations using that natural id. +This is accomplished using the `KeyType#NATURAL` `FindOption` - + +[source,java] +---- +var bookKey = new BookKey(...); +var book = session.find(Book.class, bookKey, NATURAL); +var books = session.findMultiple(Book.class, List.of(bookKey), NATURAL); +---- + +When loading by natural id, the type of value accepted depends on the type of natural id. +For single-attribute natural ids, whether defined by a basic or embedded type, the attribute type should be used. +For multi-attribute natural ids, Hibernate will accept a number of forms: + +* If a link:{doc-javadoc-url}org/hibernate/annotations/NaturalIdClass.html[`@NaturalIdClass`] is defined, an instance of the natural id class may be used. +* An array of the individual attribute values, ordered alphabetically by name, may be used. +* A `Map` of the individual attribute values, keyed by the attribute name, may be used. + Each of the operations we've seen so far affects a single entity instance passed as an argument. But there's a way to set things up so that an operation will propagate to associated entities. +See the link:{doc-user-guide-url}#find-by-natural-id[User Guide] for more details about loading by natural ids. + [[cascade]] === Cascading persistence operations @@ -595,7 +617,8 @@ Therefore, Hibernate has some APIs that streamline certain more complicated look [NOTE] ==== -Since the introduction of `FindOption` in JPA 3.2, `byId()` is now much less useful. +Since the introduction of `FindOption` in JPA 3.2, `byId()` is now much less useful and deprecated. +Instead, use `find()` and `findMultiple()` as discussed <>. ==== Batch loading is very useful when we need to retrieve multiple instances of the same entity class by id: @@ -627,6 +650,8 @@ We also have some operations for working with lookups by <>. + +[source,java] +---- +@OneToMany // no matching @ManyToOne +@JoinTable(name="PublishedBooks") +Set books; +---- + +.Show DDL +[%collapsible%closed] +==== +[source,sql] +create table PublishedBooks ( + Publisher_id bigint not null, + books_isbn varchar(255) not null, + primary key (books_isbn) +) +==== + +Here there is a `UNIQUE` or `PRIMARY KEY` constraint on the foreign key of the `Book` table in the join table `PublishedBooks`. .`@JoinTable` annotation members [%breakable,cols="20,~"] @@ -402,7 +457,8 @@ Here we see four different ways to use the `@Column` annotation: ---- @Entity @Table(name="Books") -@SecondaryTable(name="Editions") +@SecondaryTable(name="Editions", + foreignKey = @ForeignKey(name="BooksToEditionsById")) class Book { @Id @GeneratedValue @Column(name="bookId") // customize column name @@ -419,6 +475,29 @@ class Book { } ---- +.Show DDL +[%collapsible%closed] +==== +[source,sql] +create table Books ( + bookId bigint not null, + isbn varchar(17) not null unique, + title varchar(100) not null, + primary key (bookId) +) +create table Editions ( + edition integer, + bookId bigint not null, + primary key (bookId) +) +alter table if exists Editions + add constraint BooksToEditionsById + foreign key (bookId) + references Books +==== + +Note that we have used the `@ForeignKey` annotation to assign a name to the foreign key constraint. + We don't use `@Column` to map associations. [[join-column-mappings]] @@ -465,6 +544,21 @@ class Item { } ---- +.Show DDL +[%collapsible%closed] +==== +[source,sql] +create table Items ( + id bigint not null, + bookIsbn varchar(17) not null, + primary key (id) +) +alter table if exists Items + add constraint ItemsToBooksBySsn + foreign key (bookIsbn) + references Books (isbn) +==== + In case this is confusing: - `bookIsbn` is the name of the foreign key column in the `Items` table, @@ -749,8 +843,7 @@ There's a couple of alternative ways to represent an embeddable type on the data ==== Embeddables as UDTs First, a really nice option, at least in the case of Java record types, and for databases which support _user-defined types_ (UDTs), is to define a UDT which represents the record type. -Hibernate 6 makes this really easy. -Just annotate the record type, or the attribute which holds a reference to it, with the new `@Struct` annotation: +This is easy: just annotate the record type, or the attribute which holds a reference to it, with the new `@Struct` annotation: [source,java] ---- @@ -809,13 +902,20 @@ class Person { ---- We also need to add Jackson or an implementation of JSONB—for example, Yasson—to our runtime classpath. -To use Jackson we could add this line to our Gradle build: +To use Jackson 2 we could add this line to our Gradle build: [source,groovy] ---- runtimeOnly 'com.fasterxml.jackson.core:jackson-databind:{jacksonVersion}' ---- +To use Jackson 3 we could add this line to our Gradle build: + +[source,groovy] +---- +runtimeOnly 'tools.jackson.core:jackson-databind:{jackson3Version}' +---- + Now the `name` column of the `Author` table will have the type `jsonb`, and Hibernate will automatically use Jackson to serialize a `Name` to and from JSON format. [[miscellaneous-mappings]] diff --git a/documentation/src/main/asciidoc/querylanguage/Concepts.adoc b/documentation/src/main/asciidoc/querylanguage/Concepts.adoc index 7626703e883c..68d3799a829b 100644 --- a/documentation/src/main/asciidoc/querylanguage/Concepts.adoc +++ b/documentation/src/main/asciidoc/querylanguage/Concepts.adoc @@ -500,13 +500,6 @@ For example, the simplest query in HQL has no `select` clause at all: from Book ---- -But we don't necessarily _recommend_ leaving off the `select` list. - -[NOTE] -==== -HQL doesn't require a `select` clause, but JPQL _does_. -==== - Naturally, the previous query may be written with a `select` clause: [source, hql] @@ -533,7 +526,7 @@ for (Book book: books) { ---- // result type Object[], both Book and Author selected List booksWithAuthors = - session.createQuery("from Book join authors", Book.class, Object[].class) + session.createQuery("from Book join authors", Object[].class) .getResultList(); for (var bookWithAuthor: booksWithAuthors) { Book book = (Book) bookWithAuthor[0]; @@ -787,5 +780,3 @@ alias ---- Where the list of ``selection``s in an `instantiation` is essentially a nested projection list. - - diff --git a/documentation/src/main/asciidoc/repositories/Configuration.adoc b/documentation/src/main/asciidoc/repositories/Configuration.adoc index c2219ccb9aa4..bd67a6ae6804 100644 --- a/documentation/src/main/asciidoc/repositories/Configuration.adoc +++ b/documentation/src/main/asciidoc/repositories/Configuration.adoc @@ -50,12 +50,19 @@ and `org.glassfish.expressly:expressly` | Hibernate Validator | `org.jboss.weld:weld-core-impl` | Weld CDI |=== -You'll need to configure the annotation processor to run when your project is compiled. -In Gradle, for example, you'll need to use `annotationProcessor`. +You'll need to configure the annotation processor to run when your project is compiled: -[source,groovy] +* In Gradle, you'll need to use `annotationProcessor`. ++ +[source,groovy,subs="attributes+"] ---- -annotationProcessor 'org.hibernate.orm:hibernate-processor:7.0.0.Final' +annotationProcessor 'org.hibernate.orm:hibernate-processor:{fullVersion}' +---- +* In Maven, you will need to configure the `maven-compiler-plugin`. ++ +[source,xml,subs="attributes+"] +---- +include::../userguide/chapters/tooling/extras/maven-example-metamodel.pom[] ---- === Excluding classes from processing diff --git a/documentation/src/main/asciidoc/repositories/Reactive.adoc b/documentation/src/main/asciidoc/repositories/Reactive.adoc index 687ceeda4c92..6493a2c99ff4 100644 --- a/documentation/src/main/asciidoc/repositories/Reactive.adoc +++ b/documentation/src/main/asciidoc/repositories/Reactive.adoc @@ -48,7 +48,7 @@ interface Library { Uni add(Book book); @Find - Uni> books(@By("isbn") String[] ibsns); + Uni> books(@By(_Book.ISBN) String[] ibsns); } ---- diff --git a/documentation/src/main/asciidoc/repositories/Repositories.adoc b/documentation/src/main/asciidoc/repositories/Repositories.adoc index 7f3df2040ffa..5e6e9a2bd871 100644 --- a/documentation/src/main/asciidoc/repositories/Repositories.adoc +++ b/documentation/src/main/asciidoc/repositories/Repositories.adoc @@ -562,7 +562,7 @@ The `@By` annotation lets us work around this problem: [source,java] ---- @Find -List books(@By("isbn") String[] ibsns); +List books(@By(_Book.ISBN) String[] ibsns); ---- Naturally, the name and type of the parameter are still checked at compile time; there's no loss of typesafety here, despite the string. diff --git a/documentation/src/main/asciidoc/shared/url-attributes.adoc b/documentation/src/main/asciidoc/shared/url-attributes.adoc index e636c20277b9..84740696d094 100644 --- a/documentation/src/main/asciidoc/shared/url-attributes.adoc +++ b/documentation/src/main/asciidoc/shared/url-attributes.adoc @@ -6,12 +6,12 @@ include::./common-attributes.adoc[] :doc-base-url: https://docs.hibernate.org/orm :doc-version-base-url: {doc-base-url}/{majorMinorVersion} -:doc-migration-guide-url: {doc-version-base-url}/migration-guide/migration-guide.html +:doc-migration-guide-url: {doc-version-base-url}/migration-guide/ :doc-quick-start-url: {doc-version-base-url}/quickstart/html_single/ -:doc-query-language-url: {doc-version-base-url}/querylanguage/html_single/Hibernate_Query_Language.html -:doc-introduction-url: {doc-version-base-url}/introduction/html_single/Hibernate_Introduction.html -:doc-data-repositories-url: {doc-version-base-url}/repositories/html_single/Hibernate_Data_Repositories.html -:doc-user-guide-url: {doc-version-base-url}/userguide/html_single/Hibernate_User_Guide.html +:doc-query-language-url: {doc-version-base-url}/querylanguage/html_single/ +:doc-introduction-url: {doc-version-base-url}/introduction/html_single/ +:doc-data-repositories-url: {doc-version-base-url}/repositories/html_single/ +:doc-user-guide-url: {doc-version-base-url}/userguide/html_single/ :doc-javadoc-url: {doc-version-base-url}/javadocs/ :doc-topical-url: {doc-version-base-url}/topical/html_single/ :doc-dialect-url: {doc-version-base-url}/dialect/ diff --git a/documentation/src/main/asciidoc/userguide/chapters/assistant/Assistant.adoc b/documentation/src/main/asciidoc/userguide/chapters/assistant/Assistant.adoc new file mode 100644 index 000000000000..84635c4b3029 --- /dev/null +++ b/documentation/src/main/asciidoc/userguide/chapters/assistant/Assistant.adoc @@ -0,0 +1,31 @@ +[[hibernate-assistant]] +== Hibernate Assistant +:assistant-project-dir: {root-project-dir}/tooling/hibernate-assistant + +[WARNING] +==== +This entire module is currently incubating and may experience breaking changes at any time, including in a micro (patch) release. +==== + +[[assistant-overview]] +=== Overview + +The Hibernate Assistant module serves as a bridge between your existing Hibernate ORM application and generative AI services. It provides the foundational components needed to expose your domain model and database operations to Large Language Models (LLMs), enabling natural language interactions with your data layer. Rather than prescribing a specific AI provider or implementation, this module focuses on providing flexible, reusable building blocks that can be integrated with any LLM service or framework. + +This module contains: + +1. The `HibernateAssistant` interface: to provide a simple, provider-agnostic, natural-language focused API to Hibernate ORM's persistence capabilities. +2. Serialization utilities: to ease the use of Hibernate ORM in the context of generative AI, for example when implementing the above. + +No implementation is included, but the above provides the building blocks for integration with generative AI services/APIs. + +[[assistant-serialization]] +=== Serialization Components + +To facilitate communication between Hibernate and LLM providers, the module includes two key Service Provider Interfaces (SPIs): + +`MetamodelSerializer`:: Generates a structured textual representation of your Hibernate mapping model, including entity classes, relationships, properties, and constraints. This allows the LLM to understand your domain model's structure and semantics. + +`ResultsSerializer`:: Converts query results and data into a structured textual format suitable for LLM consumption and interpretation. This enables the AI to reason about actual data from your database. + +Default JSON-based implementations of both serializers are provided, offering a ready-to-use foundation for most integration scenarios. \ No newline at end of file diff --git a/documentation/src/main/asciidoc/userguide/chapters/domain/collections.adoc b/documentation/src/main/asciidoc/userguide/chapters/domain/collections.adoc index c59f24821898..6ab1267c5c6e 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/domain/collections.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/domain/collections.adoc @@ -683,7 +683,9 @@ include::{extrasdir}/collections-unidirectional-ordered-list-order-column-select ---- ==== -With the `order_id` column in place, Hibernate can order the list in-memory after it's being fetched from the database. +With the `order_id` column in place, Hibernate can order the list in-memory after it's fetched from the database. + +The <> may be used to choose between 0-based and 1-based indexing on the database side. [[collections-bidirectional-ordered-list]] ===== Bidirectional ordered lists @@ -701,54 +703,6 @@ include::{example-dir-collection}/BidirectionalOrderByListTest.java[tags=collect Just like with the unidirectional `@OrderBy` list, the `number` column is used to order the statement on the SQL level. -When using the `@OrderColumn` annotation, the `order_id` column is going to be embedded in the child table: - -[[collections-bidirectional-ordered-list-order-column-example]] -.Bidirectional `@OrderColumn` list -==== -[source,java] ----- -include::{example-dir-collection}/BidirectionalOrderColumnListTest.java[tags=collections-bidirectional-ordered-list-order-column-example,indent=0] ----- - -[source,sql] ----- -include::{extrasdir}/collections-bidirectional-ordered-list-order-column-example.sql[] ----- -==== - -When fetching the collection, Hibernate will use the fetched ordered columns to sort the elements according to the `@OrderColumn` mapping. - -[[collections-customizing-ordered-list-ordinal]] -===== Customizing ordered list ordinal - -You can customize the ordinal of the underlying ordered list by using the https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/annotations/ListIndexBase.html[`@ListIndexBase`] annotation. - -[[collections-customizing-ordered-list-ordinal-mapping-example]] -.`@ListIndexBase` mapping example -==== -[source,java] ----- -include::{example-dir-collection}/OrderColumnListIndexBaseTest.java[tags=collections-customizing-ordered-list-ordinal-mapping-example,indent=0] ----- -==== - -When inserting two `Phone` records, Hibernate is going to start the List index from 100 this time. - -[[collections-customizing-ordered-list-ordinal-persist-example]] -.`@ListIndexBase` persist example -==== -[source,java] ----- -include::{example-dir-collection}/OrderColumnListIndexBaseTest.java[tags=collections-customizing-ordered-list-ordinal-persist-example,indent=0] ----- - -[source,sql] ----- -include::{extrasdir}/collections-customizing-ordered-list-ordinal-persist-example.sql[] ----- -==== - [[collections-customizing-ordered-by-sql-clause]] ===== Customizing ORDER BY SQL clause diff --git a/documentation/src/main/asciidoc/userguide/chapters/domain/identifiers.adoc b/documentation/src/main/asciidoc/userguide/chapters/domain/identifiers.adoc index f7f101e83858..9a564aff8839 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/domain/identifiers.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/domain/identifiers.adoc @@ -701,45 +701,6 @@ attribute, etc. for identifier generators. - -[[identifiers-generators-GenericGenerator]] -==== Using `@GenericGenerator` - -[TIP] -==== -`@GenericGenerator` is generally considered deprecated in favor of <> -==== - -`@GenericGenerator` allows integration of any Hibernate `org.hibernate.id.IdentifierGenerator` implementation, including any of the specific ones discussed here and any custom ones. - -[[identifiers-generators-pooled-lo-optimizer-mapping-example]] -.Pooled-lo optimizer mapping using `@GenericGenerator` mapping -==== -[source, java, indent=0] ----- -include::{example-dir-identifier}/PooledOptimizerTest.java[tag=identifiers-generators-pooled-lo-optimizer-mapping-example] ----- -==== - -Now, when saving 5 `Person` entities and flushing the Persistence Context after every 3 entities: - -[[identifiers-generators-pooled-lo-optimizer-persist-example]] -.Pooled-lo optimizer mapping using `@GenericGenerator` mapping -==== -[source, java, indent=0] ----- -include::{example-dir-identifier}/PooledOptimizerTest.java[tag=identifiers-generators-pooled-lo-optimizer-persist-example] ----- - -[source, sql, indent=0] ----- -include::{extrasdir}/id/identifiers-generators-pooled-lo-optimizer-persist-example.sql[] ----- -==== - -As you can see from the list of generated SQL statements, you can insert 3 entities with just one database sequence call. -This way, the pooled and the pooled-lo optimizers allow you to reduce the number of database round trips, therefore reducing the overall transaction response time. - [[identifiers-derived]] ==== Derived Identifiers diff --git a/documentation/src/main/asciidoc/userguide/chapters/domain/natural_id.adoc b/documentation/src/main/asciidoc/userguide/chapters/domain/natural_id.adoc index 5641caec6619..142ea62774b1 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/domain/natural_id.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/domain/natural_id.adoc @@ -1,22 +1,15 @@ [[naturalid]] === Natural Ids :core-project-dir: {root-project-dir}/hibernate-core -:example-dir-naturalid: {core-project-dir}/src/test/java/org/hibernate/orm/test/mapping/identifier +:core-test-base-dir: {core-project-dir}/src/test/java/org/hibernate/orm/test +:example-dir-naturalid: {core-test-base-dir}/mapping/identifier :jcache-project-dir: {root-project-dir}/hibernate-jcache :example-dir-caching: {jcache-project-dir}/src/test/java/org/hibernate/orm/test/caching :extrasdir: extras -Natural ids represent domain model unique identifiers that have a meaning in the real world too. +Natural ids are unique identifiers in the domain model that have a meaning in the real world too. Even if a natural id does not make a good primary key (surrogate keys being usually preferred), it's still useful to tell Hibernate about it. -As we will see later, Hibernate provides a dedicated, efficient API for loading an entity by its natural id much like it offers for loading by identifier (PK). - -[IMPORTANT] -==== -All values used in a natural id must be non-nullable. - -For natural id mappings using a to-one association, this precludes the use of not-found -mappings which effectively define a nullable mapping. -==== +As we will see <>, Hibernate provides an efficient means for loading an entity by its natural id much like it offers for loading by identifier (PK). [[naturalid-mapping]] ==== Natural Id Mapping @@ -50,104 +43,97 @@ include::{example-dir-naturalid}/MultipleNaturalIdTest.java[tags=naturalid-multi ---- ==== -[[naturalid-api]] -==== Natural Id API - -As stated before, Hibernate provides an API for loading entities by their associated natural id. -This is represented by the `org.hibernate.NaturalIdLoadAccess` contract obtained via Session#byNaturalId. +Natural ids defined using multiple persistent attributes may also define a link:{doc-javadoc-url}org/hibernate/annotations/NaturalIdClass.html[`@NaturalIdClass`] which can be used for <>. -[NOTE] -==== -If the entity does not define a natural id, trying to load an entity by its natural id will throw an exception. -==== - -[[naturalid-load-access-example]] -.Using NaturalIdLoadAccess +[[naturalidclass-example]] +.Natural id with @NaturalIdClass ==== [source,java] ---- -include::{example-dir-naturalid}/SimpleNaturalIdTest.java[tags=naturalid-load-access-example,indent=0] +include::{core-test-base-dir}/mapping/naturalid/idclass/SimpleNaturalIdClassTests.java[tags=naturalidclass-mapping-example,indent=0] ---- +==== -[source,java] ----- -include::{example-dir-naturalid}/CompositeNaturalIdTest.java[tags=naturalid-load-access-example,indent=0] ----- +[[natural-id-mutability]] +==== Natural id mutability + +A natural id may be mutable or immutable. By default, the `@NaturalId` annotation marks the attribute as immutable. +An immutable natural id is expected to never change its value. +In fact, Hibernate will check at flush-time to ensure that the value has not been altered. + +If the value(s) of the natural id attribute(s) may change, `@NaturalId(mutable = true)` should be used instead. + +[[naturalid-mutable-mapping-example]] +.Mutable natural id mapping +==== [source,java] ---- -include::{example-dir-naturalid}/MultipleNaturalIdTest.java[tags=naturalid-load-access-example,indent=0] +include::{example-dir-naturalid}/MutableNaturalIdTest.java[tags=naturalid-mutable-mapping-example,indent=0] ---- ==== -NaturalIdLoadAccess offers 2 distinct methods for obtaining the entity: - -`load()`:: obtains a reference to the entity, making sure that the entity state is initialized. -`getReference()`:: obtains a reference to the entity. The state may or may not be initialized. -If the entity is already associated with the current running Session, that reference (loaded or not) is returned. -If the entity is not loaded in the current Session and the entity supports proxy generation, an uninitialized proxy is generated and returned, otherwise the entity is loaded from the database and returned. - -`NaturalIdLoadAccess` allows loading an entity by natural id and at the same time applies a pessimistic lock. -For additional details on locking, see the <> chapter. -We will discuss the last method available on NaturalIdLoadAccess ( `setSynchronizationEnabled()` ) in <>. +[[natural-id-caching]] +==== Natural id resolution caching -Because the `Book` entities in the first two examples define "simple" natural ids, we can load them as follows: +Within the Session, Hibernate maintains a cross reference of the resolutions from natural id values to entity identifier (PK) values. +We can also have this value resolution cached in the second level cache if second level caching is enabled. -[[naturalid-simple-load-access-example]] -.Loading by simple natural id +[[naturalid-caching]] +.Natural id caching ==== [source,java] ---- -include::{example-dir-naturalid}/SimpleNaturalIdTest.java[tags=naturalid-simple-load-access-example,indent=0] +include::{example-dir-caching}/CacheableNaturalIdTest.java[tags=naturalid-cacheable-mapping-example,indent=0] ---- +==== -[source,java] ----- -include::{example-dir-naturalid}/CompositeNaturalIdTest.java[tags=naturalid-simple-load-access-example,indent=0] ----- +[IMPORTANT] +==== +Think carefully before caching resolutions for natural ids which are partially or fully <> in the second level cache as this will often have a negative impact on performance. ==== -Here we see the use of the `org.hibernate.SimpleNaturalIdLoadAccess` contract, -obtained via `Session#bySimpleNaturalId()`. -`SimpleNaturalIdLoadAccess` is similar to `NaturalIdLoadAccess` except that it does not define the using method. -Instead, because these _simple_ natural ids are defined based on just one attribute we can directly pass -the corresponding natural id attribute value directly to the `load()` and `getReference()` methods. + +[[find-by-natural-id]] +[[naturalid-api]] +==== Loading by natural id + +Hibernate provides a means to load one or more entities by natural id using the `KeyType.NATURAL` `FindOption` passed to `find()` or `findMultiple()`. [NOTE] ==== -If the entity does not define a natural id, or if the natural id is not of a "simple" type, an exception will be thrown there. +Hibernate historically offered the dedicated `byNaturalId()`, `bySimpleNaturalId()` and `byMultipleNaturalId()` APIs for loading one or more entities by natural id using its legacy "load access" approach. However, with JPA 3.2 and the introduction of `FindOption`, etc., these "load access" approaches are considered deprecated and are not discussed here. ==== -[[naturalid-mutability-caching]] -==== Natural Id - Mutability and Caching - -A natural id may be mutable or immutable. By default the `@NaturalId` annotation marks an immutable natural id attribute. -An immutable natural id is expected to never change its value. -If the value(s) of the natural id attribute(s) change, `@NaturalId(mutable = true)` should be used instead. - -[[naturalid-mutable-mapping-example]] -.Mutable natural id mapping +[[find-by-natural-id-example]] +.Loading by natural id ==== [source,java] ---- -include::{example-dir-naturalid}/MutableNaturalIdTest.java[tags=naturalid-mutable-mapping-example,indent=0] +include::{example-dir-naturalid}/SimpleNaturalIdTest.java[tags=naturalid-loading-example,indent=0] ---- ==== -Within the Session, Hibernate maintains a mapping from natural id values to entity identifiers (PK) values. -If natural ids values changed, it is possible for this mapping to become out of date until a flush occurs. +When loading by natural id, the type of value accepted depends on the definition of the natural id. -To work around this condition, Hibernate will attempt to discover any such pending changes and adjust them when the `load()` or `getReference()` methods are executed. -To be clear: this is only pertinent for mutable natural ids. +* For single-attribute natural ids, whether defined by a basic or embedded type, the attribute type should be used. +* For multi-attribute natural ids, Hibernate will accept a number of forms: + +** If a link:{doc-javadoc-url}org/hibernate/annotations/NaturalIdClass.html[`@NaturalIdClass`] is defined, an instance of the natural id class may be used. +** An array of the individual attribute values, ordered alphabetically by name, may be used. +** A `Map` of the individual attribute values, keyed by the attribute name, may be used. + +There are a few differences to be aware of when loading by natural id compared to loading by primary key. Most importantly, if the natural id is mutable and its values have changed, it is possible for the resolution caching to become out of date until a flush occurs resulting in incorrect results. +To work around this condition, Hibernate will attempt to discover any such pending changes and adjust them prior to performing the load. [IMPORTANT] ==== -This _discovery and adjustment_ have a performance impact. -If you are certain that none of the mutable natural ids already associated with the current `Session` have changed, you can disable this checking by calling `setSynchronizationEnabled(false)` (the default is `true`). -This will force Hibernate to circumvent the checking of mutable natural ids. +This _discovery and adjustment_ (synchronization) has a performance impact. +If you are certain that none of the mutable natural ids already associated with the current `Session` have changed, you can disable this using the `NaturalIdSynchronization.DISABLED` option which will force Hibernate to skip the checking of mutable natural ids. +To be clear: this is only pertinent for mutable natural ids. ==== [[naturalid-mutable-synchronized-example]] @@ -159,13 +145,3 @@ include::{example-dir-naturalid}/MutableNaturalIdTest.java[tags=naturalid-mutabl ---- ==== -Not only can this NaturalId-to-PK resolution be cached in the Session, but we can also have it cached in the second-level cache if second level caching is enabled. - -[[naturalid-caching]] -.Natural id caching -==== -[source,java] ----- -include::{example-dir-caching}/CacheableNaturalIdTest.java[tags=naturalid-cacheable-mapping-example,indent=0] ----- -==== diff --git a/documentation/src/main/asciidoc/userguide/chapters/fetching/Fetching.adoc b/documentation/src/main/asciidoc/userguide/chapters/fetching/Fetching.adoc index e9ca78b19744..7e2d515895b4 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/fetching/Fetching.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/fetching/Fetching.adoc @@ -285,7 +285,7 @@ Hibernate allows the creation of Jakarta Persistence fetch/load graphs by parsin of the graph. Generally speaking, the textual representation of a graph is a comma-separated list of attribute names, optionally including any subgraph specifications. The starting point for such parsing operations is either `org.hibernate.graph.GraphParser` -or `SessionFactory#parseEntityGraph` +or `SessionFactory#parseEntityGraph`. [NOTE] ==== @@ -294,6 +294,21 @@ syntax described here is specific to Hibernate. We do hope to eventually make th the Jakarta Persistence specification proper. ==== +===== Graph parser mode configuration + +Since Hibernate 7.3, a new configuration setting controls which syntax the `LegacyGraphParser` uses: + +`hibernate.graph_parser_mode`:: +Determines which parsing syntax is active. ++ +* `legacy` — The historical syntax used before Hibernate 7.3. +* `modern` — Enables the new syntax that supports root-subtype subgraphs and improved readability. + +.Default value +[source,properties] +---- +hibernate.graph_parser_mode = legacy +---- .Parsing a simple graph ==== @@ -318,10 +333,14 @@ include::{example-dir-fetching}/GraphParsingTest.java[tags=fetching-strategies-d ---- ==== +===== Subtype-specific subgraphs (examples showing both supported forms) + Parsing can also handle subtype specific subgraphs. For example, given an entity hierarchy of `LegalEntity` <- (`Corporation` | `Person` | `NonProfit`) and an attribute named `responsibleParty` whose type is the `LegalEntity` base type we might have: +* **Legacy form (hibernate.graph_parser_mode = legacy)** + ==== [source, java, indent=0] ---- @@ -329,8 +348,21 @@ responsibleParty(Corporation: ceo) ---- ==== +* **Modern form (hibernate.graph_parser_mode = modern)** + +==== +[source, java, indent=0] +---- +responsibleParty:Corporation(ceo) +---- +==== + +Both forms produce the same runtime EntityGraph structure. + We can even duplicate the attribute names to apply different subtype subgraphs: +* **Legacy form (hibernate.graph_parser_mode = legacy)** + ==== [source, java, indent=0] ---- @@ -338,6 +370,15 @@ responsibleParty(taxIdNumber), responsibleParty(Corporation: ceo), responsiblePa ---- ==== +* **Modern form (hibernate.graph_parser_mode = modern)** + +==== +[source, java, indent=0] +---- +responsibleParty(taxIdNumber), responsibleParty:Corporation(ceo), responsibleParty:NonProfit(sector) +---- +==== + The duplicated attribute names are handled according to the Jakarta Persistence specification which says that duplicate specification of the attribute node results in the originally registered AttributeNode to be re-used effectively merging the 2 AttributeNode specifications together. In other words, the above specification @@ -355,6 +396,20 @@ invoiceGraph.addSubgraph( "responsibleParty", NonProfit.class ).addAttributeNode ---- ==== +===== Root-subtype subgraphs (modern-only feature) + +The modern parser mode also supports defining subgraphs that originate at a subtype of the **root entity**. +This feature is **only available** when `hibernate.graph_parser_mode=modern`. +Given an entity hierarchy of `LegalEntity` <- (`Corporation` | `Person` | `NonProfit`) with an attribute `ceo` that exists only on `Corporation` +and an attribute `sector` that exists only on `NonProfit`, you can define such subgraphs directly in a `LegalEntity` graph definition: + +==== +[source,java,indent=0] +---- +:Corporation(ceo), :NonProfit(sector) +---- +==== + [[fetching-strategies-dynamic-fetching-entity-graph-merging]] ==== Combining multiple Jakarta Persistence entity graphs into one @@ -391,6 +446,29 @@ class Book { ---- ==== +Since Hibernate 7.3 a `root` attribute is available on the Hibernate-specific `@NamedEntityGraph` annotation +to explicitly indicate the entity type that is the root of the graph. + +When `@NamedEntityGraph` is placed on a package, the `root` attribute **must** be specified. +If the annotation is placed on a package and the `root` is omitted: +* In `legacy` parser mode a deprecation warning will be emitted. +* In `modern` parser mode the omission is **not allowed** and results in an error. + +.Package-level @NamedEntityGraph example +==== +[source, java, indent=0] +---- +@org.hibernate.annotations.NamedEntityGraph( + name = "Book.graph", + root = Book.class, + graph = "title,isbn,author(name,phoneNumber)" +) +package com.example.model; +---- +==== + +This annotation works in conjunction with the `LegacyGraphParser` and respects the syntax +defined by `hibernate.graph_parser_mode`. [[fetching-strategies-dynamic-fetching-profile]] === Dynamic fetching via Hibernate profiles diff --git a/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc b/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc index ae6daa913311..e311d2211d6b 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc @@ -117,12 +117,6 @@ include::{example-dir-hql}/HQLTest.java[tags=hql-select-simplest-example] ---- ==== -We don't necessarily _recommend_ leaving off the `select` list. - -[NOTE] -==== -HQL doesn't require a `select` clause, but JPQL _does_. - Naturally, the previous query may be written with a `select` clause: [source, java, indent=0] @@ -138,7 +132,6 @@ include::{example-dir-hql}/HQLTest.java[tags=hql-select-no-from] ---- For complicated queries, it's probably best to explicitly specify a `select` list. -==== An alternative "simplest" query has _only_ a `select` list: @@ -188,7 +181,7 @@ For example: ==== [source, java, indent=0] ---- -include::{example-dir-hql}/HQLTest.java[tags=hql-update-example] +include::{example-dir-hql}/HQLInsertAndUpdateTest.java[tags=hql-update-example] ---- ==== @@ -328,7 +321,7 @@ For example: ==== [source, sql, indent=0] ---- -include::{example-dir-hql}/HQLTest.java[tags=hql-insert-example] +include::{example-dir-hql}/HQLInsertAndUpdateTest.java[tags=hql-insert-example] ---- [source, sql, indent=0] @@ -1220,6 +1213,8 @@ The following functions deal with SQL array types, which are not supported on ev | <> | Creates a sub-array of the based on lower and upper index | <> | Creates array copy replacing a given element with another | <> | Creates array copy trimming the last _N_ elements +| <> | Returns a copy of the array with elements in reverse order +| <> | Returns a sorted copy of the array | <> | Creates array filled with the same element _N_ times | <> | Like `array_fill`, but returns the result as `List` | <> | String representation of array @@ -1596,6 +1591,46 @@ include::{array-example-dir-hql}/ArrayTrimTest.java[tags=hql-array-trim-example] ---- ==== +[[hql-array-reverse-functions]] +===== `array_reverse()` + +Returns a copy of the array with elements in reverse order. Returns `null` if the argument is `null`. + +[[hql-array-reverse-example]] +==== +[source, java, indent=0] +---- +include::{array-example-dir-hql}/ArrayReverseTest.java[tags=hql-array-reverse-example] +---- +==== + +[[hql-array-sort-functions]] +===== `array_sort()` + +Returns a sorted copy of the array. When called with no optional arguments, elements are sorted in ascending order with `null` elements placed last. +The optional second argument allows specifying descending order, and the optional third argument controls the position of `null` elements. +Returns `null` if the first argument is `null`. + +[[hql-array-sort-example]] +==== +[source, java, indent=0] +---- +include::{array-example-dir-hql}/ArraySortTest.java[tags=hql-array-sort-example] +---- +==== + +The second argument controls sort direction: `false` for ascending (default), `true` for descending. +The third argument controls `null` placement: `false` for nulls last, `true` for nulls first. +When the third argument is omitted, it defaults to the value of the second argument. + +[[hql-array-sort-descending-nulls-last-example]] +==== +[source, java, indent=0] +---- +include::{array-example-dir-hql}/ArraySortTest.java[tags=hql-array-sort-descending-nulls-last-example] +---- +==== + [[hql-array-fill-functions]] ===== `array_fill()` and `array_fill_list()` @@ -1766,11 +1801,11 @@ Extracts a scalar value by https://www.ietf.org/archive/id/draft-goessner-dispat include::{extrasdir}/json_value_bnf.txt[] ---- -The first argument is an expression to a JSON document. The second argument is a JSON path as String expression. +The first argument is an expression to a JSON document. The second argument is a JSON path as a string expression. WARNING: Some databases might also allow extracting non-scalar values. Beware that this behavior is not portable. -NOTE: It is recommended to only us the dot notation for JSON paths instead of the bracket notation, +NOTE: It is recommended to use the dot notation for JSON paths instead of the bracket notation, since most databases support only that. [[hql-json-value-example]] diff --git a/documentation/src/main/asciidoc/userguide/chapters/tooling/Tooling.adoc b/documentation/src/main/asciidoc/userguide/chapters/tooling/Tooling.adoc index 8f312cbd7f45..c4d0fbe50866 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/tooling/Tooling.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/tooling/Tooling.adoc @@ -14,10 +14,12 @@ These services include * <> * <> * <> +* <> include::enhancement.adoc[] include::modelgen.adoc[] include::schema.adoc[] +include::reveng.adoc[] include::gradle.adoc[] include::maven.adoc[] diff --git a/documentation/src/main/asciidoc/userguide/chapters/tooling/ant.adoc b/documentation/src/main/asciidoc/userguide/chapters/tooling/ant.adoc index c9e4c2823018..570b15e8fc84 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/tooling/ant.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/tooling/ant.adoc @@ -4,7 +4,8 @@ Hibernate provides https://ant.apache.org/[Ant] support. Everything Ant related is available from the https://central.sonatype.com/artifact/org.hibernate.orm/hibernate-ant[hibernate-ant] -library. +library. The Ant integration supports <>, +<>, and <>. [[tooling-ant-enhancement]] ==== Bytecode Enhancement ==== @@ -25,7 +26,7 @@ of the enhancement task. [[enhance-task-example]] .Enhance Task Example ==== -[source, xml]] +[source, xml,subs="attributes+"] ---- include::extras/ant-enhance-example.xml[] ---- @@ -176,7 +177,7 @@ Ant's https://ant.apache.org/manual/Tasks/javac.html[javac] task. [[javac-task-example]] .Javac task configuration ==== -[source, xml] +[source,xml,subs="attributes+"] ---- - + ---- @@ -192,6 +193,134 @@ Ant's https://ant.apache.org/manual/Tasks/javac.html[javac] task. ==== +[[tooling-ant-reveng]] +==== Reverse Engineering + +Hibernate provides an Ant task for <> an existing database +into Java entities, mapping files, DAOs, configuration files, and DDL scripts. + + +[[tooling-ant-reveng-task]] +===== Task Definition + +The reverse engineering task is implemented by `org.hibernate.tool.ant.HibernateToolTask`. +Define it in your `build.xml` using a `taskdef`: + +[source,xml,subs="attributes+"] +---- + +---- + +The `toolslib` classpath must include the +https://central.sonatype.com/artifact/org.hibernate.orm/hibernate-ant[hibernate-ant] +artifact and its dependencies, as well as the JDBC driver for your database. + + +[[tooling-ant-reveng-hibernatetool]] +===== The `` Task + +The `` task is the main entry point. It accepts the following attributes: + +`destdir`:: The base output directory for generated files. Required (can be set on the task or on individual exporters). +`templatepath`:: Path to a directory containing custom FreeMarker templates. + +It also accepts `` child elements for passing additional properties, and +a `` element for configuring the classpath. + + +[[tooling-ant-reveng-configurations]] +===== Configuration Elements + +A `` task requires exactly one configuration element that defines how metadata is obtained. +For reverse engineering from a database, use ``: + +``:: Reads database metadata via JDBC for reverse engineering. Attributes: ++ +-- +`propertyfile`;; Path to a properties file containing JDBC connection settings. +`configurationfile`;; Path to a `hibernate.cfg.xml` file. +`packagename`;; The default Java package name for generated classes. +`reversestrategy`;; Fully qualified class name of a custom reverse engineering strategy. +`revengfile`;; Path containing reverse engineering configuration XML file(s) for customizing schema selection and type mappings. +`detectmanytomany`;; Whether to detect many-to-many link tables. Default: `true`. +`detectonetoone`;; Whether to detect one-to-one associations. Default: `true`. +`detectoptimisticlock`;; Whether to detect `VERSION`/`TIMESTAMP` columns for optimistic locking. Default: `true`. +-- + +Other configuration elements are also available for non-JDBC scenarios: + +``:: Uses a `hibernate.cfg.xml` and/or HBM mapping files as input. +``:: Uses a JPA `persistence.xml` as input. + + +[[tooling-ant-reveng-exporters]] +===== Exporter Elements + +One or more exporter elements can be nested inside `` to specify what artifacts to generate. +Each exporter supports a `destdir` attribute to override the output directory. + +``:: Generates Java entity classes. +``:: Generates `hbm.xml` mapping files. +``:: Generates Data Access Object (DAO) classes. +``:: Generates DDL scripts (schema creation/drop). +``:: Generates a `hibernate.cfg.xml` configuration file. +``:: Generates HTML documentation of the database schema. +``:: Runs a custom FreeMarker template for user-defined code generation. +``:: Executes HQL queries against the configured metadata. + + +[[tooling-ant-reveng-example]] +===== Example + +The following `build.xml` generates JPA entity classes from a database: + +[source,xml,subs="attributes+"] +---- + + + + + + + + + + + + + + + + + + + + + +---- + +To generate multiple artifact types in a single run, add additional exporter elements: + +[source,xml] +---- + + + + + + +---- + + [[tooling-ant-schema]] ==== Schema Management diff --git a/documentation/src/main/asciidoc/userguide/chapters/tooling/extras/ant-enhance-example.xml b/documentation/src/main/asciidoc/userguide/chapters/tooling/extras/ant-enhance-example.xml index 2e9f378f09c2..8066d2aabf41 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/tooling/extras/ant-enhance-example.xml +++ b/documentation/src/main/asciidoc/userguide/chapters/tooling/extras/ant-enhance-example.xml @@ -3,7 +3,7 @@ [...] diff --git a/documentation/src/main/asciidoc/userguide/chapters/tooling/extras/maven-example-metamodel.pom b/documentation/src/main/asciidoc/userguide/chapters/tooling/extras/maven-example-metamodel.pom index 73a4c40dd408..8be8568b167e 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/tooling/extras/maven-example-metamodel.pom +++ b/documentation/src/main/asciidoc/userguide/chapters/tooling/extras/maven-example-metamodel.pom @@ -10,16 +10,14 @@ org.hibernate.orm hibernate-processor - $currentHibernateVersion - - - - org.sample - sample-dependency - - + {fullVersion} + + true [...] diff --git a/documentation/src/main/asciidoc/userguide/chapters/tooling/extras/maven-example.pom b/documentation/src/main/asciidoc/userguide/chapters/tooling/extras/maven-example.pom index b14aaef670be..4b84e8fe230a 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/tooling/extras/maven-example.pom +++ b/documentation/src/main/asciidoc/userguide/chapters/tooling/extras/maven-example.pom @@ -4,7 +4,7 @@ org.hibernate.orm hibernate-maven-plugin - $currentHibernateVersion + {fullVersion} diff --git a/documentation/src/main/asciidoc/userguide/chapters/tooling/gradle.adoc b/documentation/src/main/asciidoc/userguide/chapters/tooling/gradle.adoc index 0033759d9e3e..4934016d3579 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/tooling/gradle.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/tooling/gradle.adoc @@ -1,8 +1,9 @@ [[tooling-gradle]] === Gradle -Hibernate provides the ability to integrate both -<> and <> capabilities into Gradle builds. +Hibernate provides the ability to integrate +<>, <>, +and <> capabilities into Gradle builds. [[tooling-gradle-enhancement]] ==== Bytecode Enhancement @@ -14,7 +15,7 @@ To apply the plugin, use Gradle's `plugins {}` block: [source,gradle] ---- plugins { - id "org.hibernate.orm" version "" + id("org.hibernate.orm") version "" } ---- @@ -29,16 +30,16 @@ hibernate { Enhancement is configured through the `enhancement` extension. -NOTE: `hibernate {}` and `enhancement {}` are separate to allow for schema tooling capabilities to be added later. +NOTE: `enhancement {}` and `reveng {}` are nested inside `hibernate {}` to configure different capabilities of the plugin. [source,gradle] ---- hibernate { enhancement { // for illustration, enable them all - lazyInitialization true - dirtyTracking true - associationManagement true + enableLazyInitialization = true + enableDirtyTracking = true + enableAssociationManagement = true } } ---- @@ -47,26 +48,123 @@ The extension is of type `EnhancementSpec` which exposes the following propertie enableLazyInitialization:: Whether to incorporate lazy loading support into the enhanced bytecode. Defaults to `true`. This setting is deprecated for removal without a replacement. See <> enableDirtyTracking:: Whether to incorporate dirty tracking into the enhanced bytecode. Defaults to `true`. This setting is deprecated for removal without a replacement. See <>. -enableAssociationManagement:: Whether to add bidirectional association management into the enhanced bytecode. See <>. +enableAssociationManagement:: Whether to add bidirectional association management into the enhanced bytecode. Defaults to `false`. See <>. -It also exposes the following method forms: - -* lazyInitialization(boolean) -* dirtyTracking(boolean) -* associationManagement(boolean) +All options are deprecated for removal. [[tooling-gradle-modelgen]] -==== Static Metamodel Generation +==== Annotation processor for static metamodel and repositories -Static metamodel generation can be incorporated into Gradle builds via the +<> generation can be incorporated into Gradle builds via the annotation processor provided by the `org.hibernate.orm:hibernate-processor` artifact. Applying -an annotation processor in Gradle is super easy - +an annotation processor in Gradle is as easy as: + + +[source,gradle,subs="attributes+"] +---- +dependencies { + annotationProcessor "org.hibernate.orm:hibernate-processor:{fullVersion}" +} +---- + + +[[tooling-gradle-reveng]] +==== Reverse Engineering + +The <> also provides +<> capabilities to generate Java entities, mapping files, +DAOs, configuration files, and DDL scripts from an existing database. + +NOTE: The Gradle configuration cache must be disabled when using reverse engineering tasks. +Add `org.gradle.configuration-cache=false` to your `gradle.properties` file. +[[tooling-gradle-reveng-tasks]] +===== Available Tasks + +The plugin registers the following tasks: + +`generateJava`:: Generates Java entity classes from the database schema. +`generateDao`:: Generates Data Access Object (DAO) classes. +`generateHbm`:: Generates `hbm.xml` mapping files. +`generateCfg`:: Generates a `hibernate.cfg.xml` configuration file. +`runSql`:: Executes the SQL statement specified by the `sqlToRun` extension property against the database. + + +[[tooling-gradle-reveng-configuration]] +===== Extension Configuration + +Reverse engineering is configured through the `reveng` block inside the `hibernate` extension: + [source,gradle] ---- +hibernate { + reveng { + hibernateProperties = 'hibernate.properties' + outputFolder = 'generated-sources' + packageName = 'com.example.model' + generateAnnotations = true + useGenerics = true + } +} +---- + +The extension supports the following properties: + +`hibernateProperties`:: Name of the properties file containing JDBC connection settings. The file is looked up from the project's main resource set. Default: `hibernate.properties`. +`outputFolder`:: Relative path (from the project directory) where generated files are written. Default: `generated-sources`. +`packageName`:: The default Java package name for generated classes. Default: empty (default package). +`generateAnnotations`:: Whether to generate Jakarta Persistence annotations on entity classes. Default: `true`. +`useGenerics`:: Whether to use Java generics in generated code. Default: `true`. +`revengStrategy`:: Fully qualified class name of a custom reverse engineering strategy. Default: `null` (uses the default strategy). +`revengFile`:: Name of a reverse engineering configuration XML file (looked up from the main resource set). Default: `null`. +`templatePath`:: Path to a directory containing custom FreeMarker templates for code generation. Default: `null`. +`sqlToRun`:: The SQL statement to execute when running the `runSql` task. Default: empty string. + + +[[tooling-gradle-reveng-properties]] +===== Hibernate Properties + +Create a `hibernate.properties` file in `src/main/resources/` with your JDBC connection settings: + +[source,properties] +---- +hibernate.connection.driver_class=org.h2.Driver +hibernate.connection.url=jdbc:h2:tcp://localhost/./mydb +hibernate.connection.username=sa +hibernate.default_catalog=MY_CATALOG +hibernate.default_schema=PUBLIC +---- + +The JDBC driver must be declared as a project dependency. + + +[[tooling-gradle-reveng-example]] +===== Example + +The following `build.gradle` generates JPA entity classes from an H2 database: + +[source,gradle,subs="attributes+"] +---- +plugins { + id 'java' + id 'org.hibernate.orm' version '{fullVersion}' +} + +repositories { + mavenCentral() +} + dependencies { - annotationProcessor "org.hibernate.orm:hibernate-processor:${hibernateVersion}" + implementation 'com.h2database:h2:2.2.224' +} + +hibernate { + reveng { + hibernateProperties = 'hibernate.properties' + packageName = 'com.example.model' + outputFolder = 'generated-sources' + } } ---- diff --git a/documentation/src/main/asciidoc/userguide/chapters/tooling/maven.adoc b/documentation/src/main/asciidoc/userguide/chapters/tooling/maven.adoc index 1ffb78032caa..e12a8473cc1a 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/tooling/maven.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/tooling/maven.adoc @@ -1,7 +1,10 @@ [[tooling-maven]] === Maven -The following sections illustrate how both <> and <> capabilities can be integrated into Maven builds. +The following sections illustrate how <>, +<>, <>, +and <> +capabilities can be integrated into Maven builds. [[tooling-maven-enhancement]] ==== Bytecode Enhancement @@ -20,7 +23,7 @@ for more details on the available parameters. .Apply the Bytecode Enhancement plugin ==== -[source,xml] +[source,xml,subs="attributes+"] ---- include::extras/maven-example.pom[] ---- @@ -159,15 +162,172 @@ even when accessing entity fields directly.. [[tooling-maven-modelgen]] -==== Static Metamodel Generation +==== Annotation processor for static metamodel and repositories -Static metamodel generation should be integrated into a maven project through the annotation processor +<> generation should be integrated into a maven project through the annotation processor paths of the maven compiler plugin. .Integrate the metamodel generator ==== -[source,xml] +[source,xml,subs="attributes+"] ---- include::extras/maven-example-metamodel.pom[] ---- ==== + + +[[tooling-maven-reveng]] +==== Reverse Engineering + +Hibernate provides a Maven plugin for <> an existing database +into Java entities, mapping files, DAOs, and DDL scripts. + + +[[tooling-maven-reveng-plugin]] +===== Plugin Declaration + +Add the `hibernate-maven-plugin` to your POM: + +[source,xml,subs="attributes+"] +---- + + + + org.hibernate.orm + hibernate-maven-plugin + {fullVersion} + + + generate-entities + generate-sources + + hbm2java + + + + + + +---- + + +[[tooling-maven-reveng-goals]] +===== Available Goals + +The plugin provides the following goals: + +`hbm2java`:: Generates Java entity classes from the database schema. Binds to the `generate-sources` phase by default. +`hbm2ddl`:: Generates DDL scripts from the database schema. Binds to the `generate-resources` phase by default. +`hbm2dao`:: Generates Data Access Object (DAO) classes. Binds to the `generate-sources` phase by default. +`generateHbm`:: Generates `hbm.xml` mapping files from the database schema. Binds to the `generate-sources` phase by default. + + +[[tooling-maven-reveng-common-params]] +===== Common Configuration Parameters + +The following parameters are shared by the `hbm2java`, `hbm2ddl`, `hbm2dao`, and `generateHbm` goals: + +`propertyFile`:: Path to a properties file containing JDBC connection settings. Default: `${project.basedir}/src/main/resources/hibernate.properties`. +`packageName`:: The default Java package name for generated classes. +`revengFile`:: Path to a reverse engineering configuration XML file for customizing schema selection, type mappings, and filters. +`revengStrategy`:: Fully qualified class name of a custom reverse engineering strategy. Extend `DefaultReverseEngineeringStrategy` and override methods to customize class names, type mappings, etc. +`detectManyToMany`:: If `true`, pure many-to-many link tables (tables whose primary key consists of exactly two foreign keys and no other columns) are mapped as many-to-many associations. Default: `true`. +`detectOneToOne`:: If `true`, a one-to-one association is created for each foreign key found. Default: `true`. +`detectOptimisticLock`:: If `true`, columns named `VERSION` or `TIMESTAMP` with appropriate types are mapped as `@Version` properties. Default: `true`. +`createCollectionForForeignKey`:: If `true`, a collection mapping is generated for each foreign key. Default: `true`. +`createManyToOneForForeignKey`:: If `true`, a many-to-one association is created for each foreign key found. Default: `true`. + + +[[tooling-maven-reveng-hbm2java-params]] +===== hbm2java Parameters + +`outputDirectory`:: The directory into which entity classes are generated. Default: `${project.build.directory}/generated-sources/`. +`ejb3`:: Whether to generate Jakarta Persistence annotations. Default: `true`. +`jdk5`:: Whether to use JDK 5+ constructs such as generics and static imports. Default: `true`. +`templatePath`:: Path to a directory containing custom FreeMarker templates. + + +[[tooling-maven-reveng-hbm2ddl-params]] +===== hbm2ddl Parameters + +`outputDirectory`:: The directory into which DDL scripts are generated. Default: `${project.build.directory}/generated-resources/`. +`outputFileName`:: The filename of the generated DDL script. Default: `schema.ddl`. +`targetTypes`:: The type of output to produce: `DATABASE` (export to the database), `SCRIPT` (write to a script file), or `STDOUT` (write to standard output). Default: `SCRIPT`. +`schemaExportAction`:: The DDL statements to create: `NONE`, `CREATE`, `DROP`, or `BOTH` (drop then create). Default: `CREATE`. +`delimiter`:: The end-of-statement delimiter. Default: `;`. +`format`:: Whether to format the SQL output. Default: `true`. +`haltOnError`:: Whether to stop on the first error. Default: `true`. + + +[[tooling-maven-reveng-hbm2dao-params]] +===== hbm2dao Parameters + +`outputDirectory`:: The directory into which DAO classes are generated. Default: `${project.build.directory}/generated-sources/`. +`ejb3`:: Whether to use Jakarta Persistence features. Default: `false`. +`jdk5`:: Whether to use JDK 5+ constructs. Default: `false`. +`templatePath`:: Path to a directory containing custom FreeMarker templates. + + +[[tooling-maven-reveng-example]] +===== Example + +The following POM generates JPA entity classes from an H2 database: + +[source,xml,subs="attributes+"] +---- + + 4.0.0 + com.example + my-project + 1.0-SNAPSHOT + + + + com.h2database + h2 + 2.2.224 + + + + + + + org.hibernate.orm + hibernate-maven-plugin + {fullVersion} + + + generate-entities + generate-sources + + hbm2java + + + + + + + +---- + +By default, the plugin reads JDBC connection settings from `src/main/resources/hibernate.properties`. +Run the generation with: + +[source,bash] +---- +mvn generate-sources +---- + + +[[tooling-maven-transformhbm]] +==== HBM XML Transformation + +The `hibernate-maven-plugin` provides a `transformHbm` goal that transforms legacy `hbm.xml` mapping files +into ORM `mapping.xml` files. This goal does not connect to a database -- it converts existing HBM mappings. + + +[[tooling-maven-transformhbm-params]] +===== transformHbm Parameters + +`inputFolder`:: The folder containing `hbm.xml` files to transform. Files are discovered recursively. Default: `${project.basedir}/src/main/resources`. +`format`:: Whether to format the output XML. Default: `true`. diff --git a/documentation/src/main/asciidoc/userguide/chapters/tooling/modelgen.adoc b/documentation/src/main/asciidoc/userguide/chapters/tooling/modelgen.adoc index 2c760773c7a3..8aba41d569b9 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/tooling/modelgen.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/tooling/modelgen.adoc @@ -1,5 +1,5 @@ [[tooling-modelgen]] -=== Static Metamodel Generator +=== Static metamodel and repositories :testing-project-dir: {root-project-dir}/hibernate-testing :example-dir-model: {testing-project-dir}/src/main/java/org/hibernate/testing/orm/domain/userguide :example-dir-metamodelgen-generated: {testing-project-dir}/target/generated/sources/annotationProcessor/java/main/org/hibernate/testing/orm/domain/userguide @@ -8,7 +8,6 @@ :ann-proc: https://docs.oracle.com/en/java/javase/11/tools/javac.html#GUID-082C33A5-CBCA-471A-845E-E77F79B7B049__GUID-3FA757C8-B67B-46BC-AEF9-7C3FFB126A93 :ann-proc-path: https://docs.oracle.com/en/java/javase/11/tools/javac.html#GUID-AEEC9F07-CB49-4E96-8BC7-BCC2C7F725C9__GUID-214E175F-0F06-4CDC-B511-5BA469955F5A :ann-proc-options: https://docs.oracle.com/en/java/javase/11/tools/javac.html#GUID-AEEC9F07-CB49-4E96-8BC7-BCC2C7F725C9__GUID-6CC814A4-8A29-434A-B7E1-DF8234784E7C -:intg-guide: https://docs.hibernate.org/orm/{majorMinorVersion}/introduction/html_single/#generator Jakarta Persistence defines a typesafe Criteria API which allows <> queries to be constructed in a strongly-typed manner, utilizing so-called static metamodel @@ -19,8 +18,9 @@ used to generate these static metamodel classes. [NOTE] ==== The Hibernate Static Metamodel Generator has many additional capabilities beyond static metamodel -class generation. See the link:{intg-guide}[Introduction Guide] for a complete discussion of its -capabilities. The rest of the discussion here is limited to the Jakarta Persistence static metamodel. +class generation. See the link:{doc-introduction-url}#generator[Introduction Guide] for a complete discussion of its +capabilities, and link:{doc-data-repositories-url}[Repositories Guide] for the specifics of Jakarta Data and repositories. +The rest of the discussion here is limited to the Jakarta Persistence static metamodel. The generator is expected to be run using the `javac` link:{ann-proc-path}[-processorpath] option. See the tool-specific discussions (<>, <> @@ -38,7 +38,7 @@ a static metamodel class based on the following rules: * For each managed class `X` in package `p`, a metamodel class `X_` is created in package `p`. * The name of the metamodel class is derived from the name of the managed class by appending "_" to the managed class name. * The metamodel class `X_` must be annotated with the `jakarta.persistence.StaticMetamodel` annotation. The generation -can also be configured to add the `javax.annotation.processing.Generated` annotation. +also adds the `jakarta.annotation.Generated` annotation (this can be disabled with a <>). * If class `X` extends another class `S`, where `S` is the most derived managed class extended by `X`, then class `X_` must extend class `S_`, where `S_` is the metamodel class created for `S`. * For every persistent singular attribute `y` declared by class `X`, where the type of `y` is `Y`, @@ -112,12 +112,16 @@ include::{toolingTestsDir}/modelgen/ModelGenTests.java[tags=tooling-modelgen-usa The Hibernate Static Metamodel Generator accepts a number of configuration options, which are specified as part of the `javac` execution using standard link:{ann-proc-options}[-A] options - -`-Adebug=[true|false]`:: Enables debug logging from the generator. -`-AfullyAnnotationConfigured=[true|false]`:: Controls whether `orm.xml` mapping should be considered. +`-Adebug=[true|false]`:: Enables debug logging from the generator. (Default: `false`) +`-AfullyAnnotationConfigured=[true|false]`:: Controls whether `orm.xml` mapping should be considered. (Default: `false`) `-ApersistenceXml=[path]`:: Specifies the path to the `persistence.xml` file. `-AormXml=[path]`:: Specifies the path to an `orm.xml` file. -`-AlazyXmlParsing=[true|false]`:: Controls whether the processor should attempt to determine whether any `orm.xml` files have changed. -`-AaddGeneratedAnnotation=[true|false]`:: Controls whether the processor should add `@jakarta.annotation.Generated` to the generated classes. -`-addGenerationDate=[true|false]`:: Controls whether the processor should add `@jakarta.annotation.Generated#date`. -`-addSuppressWarningsAnnotation=[warning[,warning]*|true]`:: A comma-separated list of warnings to suppress, or simply `true` if `@SuppressWarnings({"deprecation","rawtypes"})` should be added to the generated classes. - +`-AlazyXmlParsing=[true|false]`:: Controls whether the processor should attempt to determine whether any `orm.xml` files have changed. (Default: `false`) +`-AaddGeneratedAnnotation=[true|false]`:: Controls whether the processor should add `@jakarta.annotation.Generated` to the generated classes. (Default: `true`) +`-AaddGenerationDate=[true|false]`:: Controls whether the processor should add `@jakarta.annotation.Generated#date`. (Default: `false`) +`-AaddSuppressWarningsAnnotation=[warning[,warning]*|true]`:: A comma-separated list of warnings to suppress, or simply `true` if `@SuppressWarnings({"deprecation","rawtypes"})` should be added to the generated classes. (Default: No suppression) +`-AsuppressJakartaDataMetamodel=[true|false]`:: Controls whether the processor should suppress Jakarta Data Metamodel even if Jakarta Data is available on the build path. (Default: `false`) +`-Ainclude=[pattern[,pattern]*]`:: A comma-separated list of type patterns that should be included with `*` as wildcard. (Default: `*`) +`-Aexclude=[pattern[,pattern]*]`:: A comma-separated list of type patterns that should be excluded with `*` as wildcard. (Default: empty) +`-Aindex=[true|false]`:: Controls whether the processor should use a filesystem-based index of entity types and enums for use by the query validator to speed up query validation and thus compilation. (Default: `true`) +`-AjakartaDataSortCompliance=[true|false]`:: Controls whether the processor should allow Jakarta Data repository interfaces that use `jakarta.data.Sort` types with a null type argument, which the Jakarta Data specification allows. (Default: `false`) diff --git a/documentation/src/main/asciidoc/userguide/chapters/tooling/reveng.adoc b/documentation/src/main/asciidoc/userguide/chapters/tooling/reveng.adoc new file mode 100644 index 000000000000..632954f2fde8 --- /dev/null +++ b/documentation/src/main/asciidoc/userguide/chapters/tooling/reveng.adoc @@ -0,0 +1,261 @@ +[[tooling-reveng]] +=== Reverse Engineering + +Reverse engineering reads an existing database schema via JDBC and generates various artifacts +from it. This is useful for bootstrapping a Hibernate project from a legacy database, or for +keeping generated code in sync as a schema evolves. + +See the tool-specific discussions (<>, <> +and <>) for details on integrating reverse engineering into those environments. + + +[[tooling-reveng-exporters]] +==== Available Exporters + +The following artifacts can be generated from a database schema: + +Java entity classes:: Annotated Jakarta Persistence entity classes. +Data Access Objects (DAOs):: DAO classes providing basic CRUD operations. +`hbm.xml` mapping files:: Hibernate XML mapping files. +`hibernate.cfg.xml`:: A configuration file listing the generated mappings. +DDL scripts:: SQL schema creation and drop scripts. +HTML documentation:: An HTML report describing the database schema. +Custom FreeMarker templates:: User-defined templates for arbitrary code generation. + +Which exporters are available and how they are configured depends on the build tool. +See <>, <>, +or <> for specifics. + + +[[tooling-reveng-connection]] +==== JDBC Connection Settings + +Reverse engineering connects to the database using settings from a `hibernate.properties` file: + +[source,properties] +---- +hibernate.connection.driver_class=org.h2.Driver +hibernate.connection.url=jdbc:h2:tcp://localhost/./mydb +hibernate.connection.username=sa +hibernate.connection.password= +hibernate.default_catalog=MY_CATALOG +hibernate.default_schema=PUBLIC +---- + +The JDBC driver must be available on the classpath (typically declared as a project dependency). +The location of the properties file is configured in the build tool plugin -- see +<>, <>, +or <> for details. + + +[[tooling-reveng-config]] +==== Reverse Engineering Configuration File + +An optional XML configuration file can be used to customize how the database schema is interpreted. +The file uses the `hibernate-reverse-engineering` document type: + +[source,xml] +---- + + + + + + +---- + +The path to this file is configured via the build tool plugin (e.g. `revengFile` in Gradle and Maven, +`revengfile` attribute on `` in Ant). + + +[[tooling-reveng-schema-selection]] +===== Schema Selection + +By default, reverse engineering reads all tables visible to the JDBC connection. +Use `` elements to restrict which catalogs, schemas, or tables are processed: + +[source,xml] +---- + + + + + + + +---- + +The `match-catalog`, `match-schema`, and `match-table` attributes accept SQL `LIKE` patterns +(e.g. `match-table="PROD_%"`). + + +[[tooling-reveng-type-mapping]] +===== Type Mappings + +The `` element overrides how SQL types are mapped to Hibernate types: + +[source,xml] +---- + + + + + + + +---- + +Each `` element supports the following attributes: + +`jdbc-type`:: The JDBC type name (e.g. `VARCHAR`, `INTEGER`, `NUMERIC`). Required. +`hibernate-type`:: The Hibernate type to use. Required. +`length`:: Match only columns with this length. +`precision`:: Match only columns with this precision. +`scale`:: Match only columns with this scale. +`not-null`:: Match only columns with this nullability (`true` or `false`). + +More specific mappings (those with more attributes) take precedence over less specific ones. + + +[[tooling-reveng-table-filter]] +===== Table Filters + +`` elements control which tables are included or excluded, and can assign +a package name to the generated classes: + +[source,xml] +---- + + + + + +---- + +The `match-catalog`, `match-schema`, and `match-name` attributes accept regular expression patterns. + + +[[tooling-reveng-table]] +===== Per-Table Configuration + +The `` element provides fine-grained control over individual tables: + +[source,xml] +---- +
+ + + + + CUST_SEQ + + + + + + + + + + + + + +
+---- + +The `` element supports the following attributes: + +`name`:: The table name. Required. +`catalog`:: The catalog name. +`schema`:: The schema name. +`class`:: The Java class name to use instead of the default derived name. + +The `` child element controls identifier generation. It accepts a `property` attribute +for the property name, and nested `` and `` elements. + +The `` child element overrides individual column mappings. It accepts `name` (required), +`property`, `type`, and `exclude` attributes. + +The `` child element customizes association mappings for a specific foreign key +constraint. It accepts `constraint-name` and `foreign-table` attributes, and can contain +``, ``, ``, and `` child elements to control +the generated association properties. + + +[[tooling-reveng-detection]] +==== Detection Settings + +The build tool plugins expose several boolean settings that control how relationships and special +columns are detected: + +`detectManyToMany`:: When `true`, pure link tables (tables whose primary key consists entirely of +two foreign keys and no other columns) are mapped as `@ManyToMany` associations instead of generating +an entity for the link table. Default: `true`. + +`detectOneToOne`:: When `true`, foreign keys that are also the primary key of their table are mapped +as `@OneToOne` associations. Default: `true`. + +`detectOptimisticLock`:: When `true`, columns named `VERSION`, `TIMESTAMP`, or `DBTIMESTAMP` with +appropriate types are mapped as `@Version` properties. Default: `true`. + +These settings are configured in the build tool plugin. See +<> or <> for details. + + +[[tooling-reveng-custom-strategy]] +==== Custom Strategies + +For programmatic control over reverse engineering, implement the +`org.hibernate.tool.reveng.api.core.RevengStrategy` interface. This interface provides callbacks +for customizing class names, property names, type mappings, association handling, and more. + +The simplest approach is to extend `DelegatingStrategy`, which wraps another strategy and +delegates all methods to it. Override only the methods you need to customize: + +[source,java] +---- +import org.hibernate.tool.reveng.api.core.RevengStrategy; +import org.hibernate.tool.reveng.internal.core.strategy.DelegatingStrategy; +import org.hibernate.tool.reveng.api.core.TableIdentifier; + +public class MyStrategy extends DelegatingStrategy { + + public MyStrategy(RevengStrategy delegate) { + super(delegate); + } + + @Override + public String tableToClassName(TableIdentifier tableIdentifier) { + String className = super.tableToClassName(tableIdentifier); + // Add "Entity" suffix to all generated class names + return className + "Entity"; + } + + @Override + public boolean excludeTable(TableIdentifier ti) { + // Skip tables starting with "TMP_" + return ti.getName().startsWith("TMP_") + || super.excludeTable(ti); + } +} +---- + +The custom strategy class must provide either a no-argument constructor or a constructor that +accepts a `RevengStrategy` parameter (used as the delegate). It is specified via the +`revengStrategy` configuration property in the build tool plugin. + +Key methods available for override: + +`tableToClassName`:: Controls the entity class name for a given table. +`columnToPropertyName`:: Controls the property name for a given column. +`columnToHibernateTypeName`:: Controls the Hibernate type mapping for a column. +`excludeTable`:: Returns `true` to skip a table entirely. +`excludeColumn`:: Returns `true` to skip a specific column. +`isManyToManyTable`:: Returns `true` if a table should be treated as a many-to-many link table. +`isOneToOne`:: Returns `true` if a foreign key represents a one-to-one relationship. +`getTableIdentifierStrategyName`:: Controls the identifier generation strategy for a table. +`foreignKeyToCollectionName`:: Controls the property name for a collection association. +`foreignKeyToEntityName`:: Controls the property name for a many-to-one association. diff --git a/documentation/src/main/asciidoc/userguide/index.adoc b/documentation/src/main/asciidoc/userguide/index.adoc index b438b52ab2c4..c5144accff72 100644 --- a/documentation/src/main/asciidoc/userguide/index.adoc +++ b/documentation/src/main/asciidoc/userguide/index.adoc @@ -45,6 +45,7 @@ include::chapters/beans/Beans.adoc[] include::chapters/portability/Portability.adoc[] include::chapters/statistics/Statistics.adoc[] include::chapters/tooling/Tooling.adoc[] +include::chapters/assistant/Assistant.adoc[] include::appendices/BestPractices.adoc[] include::Credits.adoc[] diff --git a/edb/edb13.Dockerfile b/edb/edb13.Dockerfile deleted file mode 100644 index 3467d71715bc..000000000000 --- a/edb/edb13.Dockerfile +++ /dev/null @@ -1,48 +0,0 @@ -FROM quay.io/enterprisedb/edb-postgres-advanced:13.20-3.5-postgis -USER root -# this 777 will be replaced by 700 at runtime (allows semi-arbitrary "--user" values) -RUN chown -R postgres:postgres /var/lib/edb && chmod 777 /var/lib/edb && rm /docker-entrypoint-initdb.d/10_postgis.sh - -USER postgres -ENV LANG en_US.utf8 -ENV PG_MAJOR 13 -ENV PG_VERSION 13 -ENV PGPORT 5444 -ENV PGDATA /var/lib/edb/as$PG_MAJOR/data/ -VOLUME /var/lib/edb/as$PG_MAJOR/data/ - -COPY docker-entrypoint.sh /usr/local/bin/ -ENTRYPOINT ["docker-entrypoint.sh"] - -# We set the default STOPSIGNAL to SIGINT, which corresponds to what PostgreSQL -# calls "Fast Shutdown mode" wherein new connections are disallowed and any -# in-progress transactions are aborted, allowing PostgreSQL to stop cleanly and -# flush tables to disk, which is the best compromise available to avoid data -# corruption. -# -# Users who know their applications do not keep open long-lived idle connections -# may way to use a value of SIGTERM instead, which corresponds to "Smart -# Shutdown mode" in which any existing sessions are allowed to finish and the -# server stops when all sessions are terminated. -# -# See https://www.postgresql.org/docs/12/server-shutdown.html for more details -# about available PostgreSQL server shutdown signals. -# -# See also https://www.postgresql.org/docs/12/server-start.html for further -# justification of this as the default value, namely that the example (and -# shipped) systemd service files use the "Fast Shutdown mode" for service -# termination. -# -STOPSIGNAL SIGINT -# -# An additional setting that is recommended for all users regardless of this -# value is the runtime "--stop-timeout" (or your orchestrator/runtime's -# equivalent) for controlling how long to wait between sending the defined -# STOPSIGNAL and sending SIGKILL (which is likely to cause data corruption). -# -# The default in most runtimes (such as Docker) is 10 seconds, and the -# documentation at https://www.postgresql.org/docs/12/server-start.html notes -# that even 90 seconds may not be long enough in many instances. - -EXPOSE 5444 -CMD ["postgres"] \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 55e1337400f0..308945c3e852 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,5 @@ db=h2 +dbVersion=latest # Keep all these properties in sync unless you know what you are doing! # We set '-Dlog4j2.disableJmx=true' to prevent classloader leaks triggered by the logger. diff --git a/gradle/version.properties b/gradle/version.properties index 8f31599e762f..55648eea5b12 100644 --- a/gradle/version.properties +++ b/gradle/version.properties @@ -1 +1 @@ -hibernateVersion=7.2.0-SNAPSHOT \ No newline at end of file +hibernateVersion=7.4.0-SNAPSHOT \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 8bdaf60c75ab..61285a659d17 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2e1113280ef1..dbc3ce4a040f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.0-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index ef07e0162b18..adff685a0348 100755 --- a/gradlew +++ b/gradlew @@ -114,7 +114,6 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH="\\\"\\\"" # Determine the Java command to use to start the JVM. @@ -172,7 +171,6 @@ fi # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) @@ -212,7 +210,6 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" diff --git a/gradlew.bat b/gradlew.bat index 5eed7ee84528..e509b2dd8fe5 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -70,11 +70,10 @@ goto fail :execute @rem Setup the command line -set CLASSPATH= @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell 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 ddbc596fdd0b..32773385011b 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 @@ -7,6 +7,7 @@ import java.io.Serial; import java.sql.Connection; import java.sql.SQLException; +import java.time.Duration; import java.util.Map; import java.util.function.Consumer; import java.util.function.Function; @@ -17,7 +18,6 @@ import org.hibernate.cfg.AvailableSettings; import org.hibernate.cfg.JdbcSettings; import org.hibernate.dialect.Dialect; -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.ConnectionProviderConfigurationException; @@ -36,6 +36,7 @@ import static java.util.function.Function.identity; import static java.util.stream.Collectors.toMap; import static org.hibernate.cfg.AgroalSettings.AGROAL_CONFIG_PREFIX; +import static org.hibernate.engine.jdbc.connections.internal.ConnectionProviderInitiator.extractIsolation; import static org.hibernate.engine.jdbc.connections.internal.ConnectionProviderInitiator.toIsolationNiceName; import static org.hibernate.engine.jdbc.connections.internal.DatabaseConnectionInfoImpl.getCatalog; import static org.hibernate.engine.jdbc.connections.internal.DatabaseConnectionInfoImpl.getDriverName; @@ -82,7 +83,7 @@ public class AgroalConnectionProvider implements ConnectionProvider, Configurabl // --- Configurable private static String extractIsolationAsString(Map properties) { - final Integer isolation = ConnectionProviderInitiator.extractIsolation( properties ); + final Integer isolation = extractIsolation( properties ); return isolation != null // Agroal resolves transaction isolation from the 'nice' name ? toIsolationNiceName( isolation ) @@ -115,7 +116,8 @@ public void configure(Map properties) throws HibernateException : String.valueOf( 10 ); config.put( AgroalSettings.AGROAL_MAX_SIZE, maxSize ); } - final var agroalProperties = new AgroalPropertiesReader( CONFIG_PREFIX ).readProperties( config ); + final var agroalProperties = + new AgroalPropertiesReader( CONFIG_PREFIX ).readProperties( config ); agroalProperties.modify() .connectionPoolConfiguration( cp -> cp.connectionFactoryConfiguration( cf -> { copyProperty( properties, JdbcSettings.DRIVER, cf::connectionProviderClassName, identity() ); @@ -123,6 +125,8 @@ public void configure(Map properties) throws HibernateException copyProperty( properties, JdbcSettings.USER, cf::principal, NamePrincipal::new ); copyProperty( properties, JdbcSettings.PASS, cf::credential, SimplePassword::new ); copyProperty( properties, JdbcSettings.AUTOCOMMIT, cf::autoCommit, Boolean::valueOf ); + copyProperty( properties, JdbcSettings.LOGIN_TIMEOUT, cf::loginTimeout, + value -> Duration.ofSeconds( Integer.parseInt( value ) ) ); resolveIsolationSetting( properties, cf ); return cf; } ) ); diff --git a/hibernate-c3p0/src/main/java/org/hibernate/c3p0/internal/C3P0ConnectionProvider.java b/hibernate-c3p0/src/main/java/org/hibernate/c3p0/internal/C3P0ConnectionProvider.java index 6d85d2adc478..2cd5ef65ea0d 100644 --- a/hibernate-c3p0/src/main/java/org/hibernate/c3p0/internal/C3P0ConnectionProvider.java +++ b/hibernate-c3p0/src/main/java/org/hibernate/c3p0/internal/C3P0ConnectionProvider.java @@ -39,6 +39,7 @@ import static org.hibernate.cfg.C3p0Settings.C3P0_MAX_STATEMENTS; import static org.hibernate.cfg.C3p0Settings.C3P0_MIN_SIZE; import static org.hibernate.cfg.C3p0Settings.C3P0_TIMEOUT; +import static org.hibernate.cfg.JdbcSettings.LOGIN_TIMEOUT; import static org.hibernate.engine.jdbc.connections.internal.ConnectionProviderInitiator.extractIsolation; import static org.hibernate.engine.jdbc.connections.internal.ConnectionProviderInitiator.extractSetting; import static org.hibernate.engine.jdbc.connections.internal.ConnectionProviderInitiator.getConnectionProperties; @@ -98,14 +99,25 @@ public class C3P0ConnectionProvider @Override public Connection getConnection() throws SQLException { - final Connection connection = dataSource.getConnection(); + final var connection = dataSource.getConnection(); + prepareConnection( connection ); + return connection; + } + + @Override + public Connection getConnection(String user, String password) throws SQLException { + final var connection = dataSource.getConnection( user, password ); + prepareConnection( connection ); + return connection; + } + + private void prepareConnection(Connection connection) throws SQLException { if ( isolation != null && isolation != connection.getTransactionIsolation() ) { connection.setTransactionIsolation( isolation ); } if ( connection.getAutoCommit() != autocommit ) { connection.setAutoCommit( autocommit ); } - return connection; } @Override @@ -159,10 +171,20 @@ public void configure(Map properties) { autocommit = getBoolean( JdbcSettings.AUTOCOMMIT, properties ); // defaults to false isolation = extractIsolation( properties ); - final Properties connectionProps = getConnectionProperties( properties ); + final var connectionProps = getConnectionProperties( properties ); final var poolSettings = poolSettings( properties ); dataSource = createDataSource( jdbcUrl, connectionProps, poolSettings ); + final Integer loginTimeout = getInteger( LOGIN_TIMEOUT, properties ); + if ( loginTimeout != null ) { + try { + dataSource.setLoginTimeout( loginTimeout ); + } + catch (SQLException e) { + CONNECTION_INFO_LOGGER.couldNotSetLoginTimeout( e ); + } + } + try ( var connection = dataSource.getConnection() ) { final Integer fetchSize = getFetchSize( connection ); final boolean hasSchema = hasSchema( connection ); diff --git a/hibernate-c3p0/src/main/java/org/hibernate/c3p0/internal/C3P0MessageLogger.java b/hibernate-c3p0/src/main/java/org/hibernate/c3p0/internal/C3P0MessageLogger.java index fde5cf50a2db..dd10aa428eac 100644 --- a/hibernate-c3p0/src/main/java/org/hibernate/c3p0/internal/C3P0MessageLogger.java +++ b/hibernate-c3p0/src/main/java/org/hibernate/c3p0/internal/C3P0MessageLogger.java @@ -14,6 +14,7 @@ import org.jboss.logging.annotations.ValidIdRange; import java.lang.invoke.MethodHandles; +import java.util.Locale; import static org.jboss.logging.Logger.Level.WARN; @@ -32,7 +33,7 @@ public interface C3P0MessageLogger extends ConnectionInfoLogger { String NAME = ConnectionInfoLogger.LOGGER_NAME + ".c3p0"; - C3P0MessageLogger C3P0_MSG_LOGGER = Logger.getMessageLogger( MethodHandles.lookup(), C3P0MessageLogger.class, NAME ); + C3P0MessageLogger C3P0_MSG_LOGGER = Logger.getMessageLogger( MethodHandles.lookup(), C3P0MessageLogger.class, NAME, Locale.ROOT ); /** * Log a message (WARN) about conflicting {@code hibernate.c3p0.XYZ} and {@code c3p0.XYZ} settings diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java index 83b691ec7557..07832acc1339 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java @@ -141,6 +141,7 @@ * A {@linkplain Dialect SQL dialect} for CockroachDB. * * @author Gavin King + * @author Yoobin Yoon */ public class CockroachLegacyDialect extends Dialect { @@ -272,6 +273,11 @@ public JdbcType resolveSqlTypeDescriptor( int scale, JdbcTypeRegistry jdbcTypeRegistry) { switch ( jdbcTypeCode ) { + case VARCHAR: + if ( "text".equals( columnTypeName ) && precision == Integer.MAX_VALUE ) { + jdbcTypeCode = LONG32VARCHAR; + } + break; case OTHER: switch ( columnTypeName ) { case "uuid": @@ -322,6 +328,20 @@ public JdbcType resolveSqlTypeDescriptor( return jdbcTypeRegistry.getDescriptor( jdbcTypeCode ); } + @Override + public boolean equivalentTypes(int typeCode1, int typeCode2) { + switch ( typeCode1 ) { + // On CockroachDB, we use the same DDL type, so treat the types as equivalent + case LONG32VARCHAR, LONG32NVARCHAR, CLOB, NCLOB: + switch ( typeCode2 ) { + case LONG32VARCHAR, LONG32NVARCHAR, CLOB, NCLOB: + return true; + } + default: + return super.equivalentTypes( typeCode1, typeCode2 ); + } + } + @Override protected Integer resolveSqlTypeCode(String columnTypeName, TypeConfiguration typeConfiguration) { return switch ( columnTypeName ) { @@ -405,7 +425,7 @@ protected void contributeCockroachTypes(TypeContributions typeContributions, Ser ObjectNullAsBinaryTypeJdbcType.INSTANCE, typeContributions.getTypeConfiguration() .getJavaTypeRegistry() - .getDescriptor( Object.class ) + .resolveDescriptor( Object.class ) ) ); @@ -485,6 +505,8 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.arraySlice_operator(); functionFactory.arrayReplace(); functionFactory.arrayTrim_unnest(); + functionFactory.arrayReverse_unnest(); + functionFactory.arraySort_unnest(); functionFactory.arrayFill_cockroachdb(); functionFactory.arrayToString_postgresql(); @@ -517,6 +539,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionContributions.getFunctionRegistry().register( "trunc", new PostgreSQLTruncFunction( + true, getVersion().isSameOrAfter( 22, 2 ), functionContributions.getTypeConfiguration() ) @@ -560,6 +583,11 @@ public String getCurrentTimestampSelectString() { return "select now()"; } + @Override + public boolean isCurrentTimestampStable() { + return true; + } + @Override public boolean supportsDistinctFromPredicate() { return true; @@ -590,6 +618,17 @@ public IdentityColumnSupport getIdentityColumnSupport() { return CockroachDBIdentityColumnSupport.INSTANCE; } + @Override + public String getAlterColumnTypeString(String columnName, String columnType, String columnDefinition) { + // would need multiple statements to 'set not null'/'drop not null', 'set default'/'drop default', 'set generated', etc + return "alter column " + columnName + " set data type " + columnType; + } + + @Override + public boolean supportsAlterColumnType() { + return true; + } + @Override public boolean supportsValuesList() { return true; diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CommunityDatabase.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CommunityDatabase.java index 38704ad505ff..f6445fa271d2 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CommunityDatabase.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CommunityDatabase.java @@ -228,6 +228,22 @@ public String getDriverClassName(String jdbcUrl) { } }, + SPANNER_PG { + @Override + public Dialect createDialect(DialectResolutionInfo info) { + return new SpannerPostgreSQLDialect( info ); + } + @Override + public boolean productNameMatches(String databaseName) { + return databaseName.equals( "Google Cloud Spanner PostgreSQL" ); + } + @Override + public String getDriverClassName(String jdbcUrl) { + return "com.google.cloud.spanner.jdbc.JdbcDriver"; + } + }, + + DERBY { @Override public Dialect createDialect(DialectResolutionInfo info) { 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 82810235d43e..04270bd3699b 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 @@ -1135,7 +1135,7 @@ public void contributeTypes(TypeContributions typeContributions, ServiceRegistry ObjectNullResolvingJdbcType.INSTANCE, typeContributions.getTypeConfiguration() .getJavaTypeRegistry() - .getDescriptor( Object.class ) + .resolveDescriptor( Object.class ) ) ); diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2iLegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2iLegacySqlAstTranslator.java index 0b317ad48713..957fff784fa9 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2iLegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2iLegacySqlAstTranslator.java @@ -4,12 +4,15 @@ */ package org.hibernate.community.dialect; +import java.util.List; + import org.hibernate.dialect.DatabaseVersion; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.sql.ast.tree.Statement; import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.Literal; +import org.hibernate.sql.ast.tree.expression.SqlTupleContainer; import org.hibernate.sql.ast.tree.select.QueryPart; import org.hibernate.sql.exec.spi.JdbcOperation; @@ -56,6 +59,20 @@ protected void renderComparison(Expression lhs, ComparisonOperator operator, Exp renderComparisonStandard( lhs, operator, rhs ); } + @Override + protected void renderExpressionsAsValuesSubquery(int tupleSize, List listExpressions) { + // DB2 for i supports type-inference in this special VALUES expression, but not if it's wrapped as SELECT + appendSql( "values" ); + char separator = ' '; + for ( Expression expression : listExpressions ) { + appendSql( separator ); + appendSql( OPEN_PARENTHESIS ); + renderCommaSeparated( SqlTupleContainer.getSqlTuple( expression ).getExpressions() ); + appendSql( CLOSE_PARENTHESIS ); + separator = ','; + } + } + @Override public DatabaseVersion getDB2Version() { return DB2_LUW_VERSION9; diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2zLegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2zLegacySqlAstTranslator.java index 7bbd45a1cc84..b218e7ad8e18 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2zLegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2zLegacySqlAstTranslator.java @@ -66,6 +66,12 @@ protected String getNewTableChangeModifier() { return "final"; } + @Override + protected boolean preferUnionQueryForTupleInListPredicate() { + // DB2 z/OS can't use an index when rendering a union query + return false; + } + @Override public DatabaseVersion getDB2Version() { return DB2_LUW_VERSION9; diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DerbyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DerbyDialect.java index 5cb7bc92015d..ab10429cd830 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DerbyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DerbyDialect.java @@ -671,7 +671,7 @@ public void contributeTypes(TypeContributions typeContributions, ServiceRegistry ObjectNullResolvingJdbcType.INSTANCE, typeContributions.getTypeConfiguration() .getJavaTypeRegistry() - .getDescriptor( Object.class ) + .resolveDescriptor( Object.class ) ) ); } 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 38c77c1a2b65..2d31d2f6b2bf 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 @@ -666,7 +666,7 @@ public void contributeTypes(TypeContributions typeContributions, ServiceRegistry ObjectNullResolvingJdbcType.INSTANCE, typeContributions.getTypeConfiguration() .getJavaTypeRegistry() - .getDescriptor( Object.class ) + .resolveDescriptor( Object.class ) ) ); } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/GaussDBArrayJdbcType.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/GaussDBArrayJdbcType.java index 0f8f03695b30..f40daa2aa0d9 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/GaussDBArrayJdbcType.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/GaussDBArrayJdbcType.java @@ -10,7 +10,6 @@ import java.sql.Types; import org.hibernate.HibernateException; -import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.type.descriptor.ValueBinder; import org.hibernate.type.descriptor.WrapperOptions; import org.hibernate.type.descriptor.java.BasicPluralJavaType; @@ -35,63 +34,68 @@ public GaussDBArrayJdbcType(JdbcType elementJdbcType) { @Override public ValueBinder getBinder(final JavaType javaTypeDescriptor) { - @SuppressWarnings("unchecked") - final BasicPluralJavaType pluralJavaType = (BasicPluralJavaType) javaTypeDescriptor; - final ValueBinder elementBinder = getElementJdbcType().getBinder( pluralJavaType.getElementJavaType() ); - return new BasicBinder<>( javaTypeDescriptor, this ) { + return new Binder<>( javaTypeDescriptor, + (BasicPluralJavaType) javaTypeDescriptor ); + } - @Override - protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) throws SQLException { - st.setArray( index, getArray( value, options ) ); - } + private class Binder extends BasicBinder { + private final BasicPluralJavaType pluralJavaType; - @Override - protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) - throws SQLException { - final java.sql.Array arr = getArray( value, options ); - try { - st.setObject( name, arr, java.sql.Types.ARRAY ); - } - catch (SQLException ex) { - throw new HibernateException( "JDBC driver does not support named parameters for setArray. Use positional.", ex ); - } - } + private Binder(JavaType javaType, BasicPluralJavaType pluralJavaType) { + super( javaType, GaussDBArrayJdbcType.this ); + this.pluralJavaType = pluralJavaType; + } + + @Override + protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) + throws SQLException { + st.setArray( index, getArray( value, options ) ); + } - @Override - public Object getBindValue(X value, WrapperOptions options) throws SQLException { - return ( (GaussDBArrayJdbcType) getJdbcType() ).getArray( this, elementBinder, value, options ); + @Override + protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) + throws SQLException { + final java.sql.Array arr = getArray( value, options ); + try { + st.setObject( name, arr, Types.ARRAY ); } + catch (SQLException ex) { + throw new HibernateException( + "JDBC driver does not support named parameters for setArray. Use positional.", ex ); + } + } + + @Override + public Object[] getBindValue(X value, WrapperOptions options) throws SQLException { + final var elementBinder = getElementJdbcType().getBinder( pluralJavaType.getElementJavaType() ); + return convertToArray( this, elementBinder, pluralJavaType, value, options ); + } - private java.sql.Array getArray(X value, WrapperOptions options) throws SQLException { - final GaussDBArrayJdbcType arrayJdbcType = (GaussDBArrayJdbcType) getJdbcType(); - final Object[] objects; + private java.sql.Array getArray(X value, WrapperOptions options) throws SQLException { + final var session = options.getSession(); + return session.getJdbcCoordinator().getLogicalConnection().getPhysicalConnection() + .createArrayOf( getElementTypeName( getJavaType(), session ), + elements( value, options, GaussDBArrayJdbcType.this ) ); + } - final JdbcType elementJdbcType = arrayJdbcType.getElementJdbcType(); - if ( elementJdbcType instanceof AggregateJdbcType ) { - // The GaussDB JDBC driver does not support arrays of structs, which contain byte[] - final AggregateJdbcType aggregateJdbcType = (AggregateJdbcType) elementJdbcType; - final Object[] domainObjects = getJavaType().unwrap( - value, - Object[].class, - options - ); - objects = new Object[domainObjects.length]; - for ( int i = 0; i < domainObjects.length; i++ ) { - if ( domainObjects[i] != null ) { - objects[i] = aggregateJdbcType.createJdbcValue( domainObjects[i], options ); - } + private Object[] elements(X value, WrapperOptions options, GaussDBArrayJdbcType arrayJdbcType) + throws SQLException { + final var elementJdbcType = arrayJdbcType.getElementJdbcType(); + if ( elementJdbcType instanceof AggregateJdbcType aggregateJdbcType ) { + // The GaussDB JDBC driver does not support arrays of structs, which contain byte[] + final var domainObjects = getJavaType().unwrap( value, Object[].class, options ); + final var objects = new Object[domainObjects.length]; + for ( int i = 0; i < domainObjects.length; i++ ) { + if ( domainObjects[i] != null ) { + objects[i] = aggregateJdbcType.createJdbcValue( domainObjects[i], options ); } } - else { - objects = arrayJdbcType.getArray( this, elementBinder, value, options ); - } - - final SharedSessionContractImplementor session = options.getSession(); - final String typeName = arrayJdbcType.getElementTypeName( getJavaType(), session ); - return session.getJdbcCoordinator().getLogicalConnection().getPhysicalConnection() - .createArrayOf( typeName, objects ); + return objects; + } + else { + return getBindValue( value, options ); } - }; + } } @Override diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/GaussDBDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/GaussDBDialect.java index ec1882995618..c4abd86b5dc1 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/GaussDBDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/GaussDBDialect.java @@ -1270,7 +1270,7 @@ protected void contributeGaussDBTypes(TypeContributions typeContributions) { ObjectNullAsBinaryTypeJdbcType.INSTANCE, typeContributions.getTypeConfiguration() .getJavaTypeRegistry() - .getDescriptor( Object.class ) + .resolveDescriptor( Object.class ) ) ); 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 8df31e5d4d5d..115595f6683a 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 @@ -128,6 +128,7 @@ * * @author Thomas Mueller * @author JÃŧrgen Kreitler + * @author Yoobin Yoon */ public class H2LegacyDialect extends Dialect { @@ -409,6 +410,8 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.arraySlice(); functionFactory.arrayReplace_h2( getMaximumArraySize() ); functionFactory.arrayTrim_trim_array(); + functionFactory.arrayReverse_h2( getMaximumArraySize() ); + functionFactory.arraySort_h2( getMaximumArraySize() ); functionFactory.arrayFill_h2(); functionFactory.arrayToString_h2( getMaximumArraySize() ); @@ -888,6 +891,11 @@ public String getCurrentTimestampSelectString() { return "call current_timestamp()"; } + @Override + public boolean isCurrentTimestampStable() { + return true; + } + // Overridden informational metadata ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HANALegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HANALegacyDialect.java index 2f93f805a350..c531111832c9 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HANALegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HANALegacyDialect.java @@ -935,6 +935,11 @@ public Identifier toIdentifier(String text, boolean quoted) { return normalizeQuoting( Identifier.toIdentifier( text, quoted ) ); } + @Override + public Identifier toIdentifier(String text, boolean quoted, boolean isExplicit) { + return normalizeQuoting( Identifier.toIdentifier( text, quoted, false, isExplicit ) ); + } + @Override public Identifier normalizeQuoting(Identifier identifier) { Identifier normalizedIdentifier = this.helper.normalizeQuoting( identifier ); 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 6b53fe514060..c55d6f81a2d0 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 @@ -95,6 +95,7 @@ * @author Christoph Sturm * @author Phillip Baird * @author Fred Toussi + * @author Yoobin Yoon */ public class HSQLLegacyDialect extends Dialect { @@ -269,6 +270,8 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.arraySlice_unnest(); functionFactory.arrayReplace_unnest(); functionFactory.arrayTrim_trim_array(); + functionFactory.arrayReverse_unnest(); + functionFactory.arraySort_hsql(); functionFactory.arrayFill_hsql(); functionFactory.arrayToString_hsql(); 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 18b4ba25d4a7..6a1239f0cf0b 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 @@ -29,7 +29,9 @@ import org.hibernate.dialect.NullOrdering; import org.hibernate.dialect.Replacer; import org.hibernate.dialect.SelectItemReferenceStrategy; +import org.hibernate.dialect.function.CountFunction; import org.hibernate.dialect.function.InsertSubstringOverlayEmulation; +import org.hibernate.dialect.function.TruncFunction; import org.hibernate.dialect.function.TrimFunction; import org.hibernate.community.dialect.temptable.InformixLocalTemporaryTableStrategy; import org.hibernate.dialect.temptable.TemporaryTableStrategy; @@ -45,7 +47,6 @@ import org.hibernate.query.sqm.function.SqmFunctionRegistry; import org.hibernate.query.sqm.produce.function.StandardFunctionArgumentTypeResolvers; import org.hibernate.type.BasicType; -import org.hibernate.dialect.lock.internal.LockingSupportSimple; import org.hibernate.dialect.lock.spi.LockingSupport; import org.hibernate.type.descriptor.jdbc.VarcharUUIDJdbcType; import org.hibernate.dialect.function.CaseLeastGreatestEmulation; @@ -341,7 +342,6 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.instr(); functionFactory.substr(); functionFactory.substringFromFor(); - functionFactory.trunc(); functionFactory.trim2(); functionFactory.space(); functionFactory.reverse(); @@ -396,7 +396,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio if ( supportsWindowFunctions() ) { functionFactory.windowFunctions(); - functionFactory.hypotheticalOrderedSetAggregates(); + functionFactory.hypotheticalOrderedSetAggregates_windowEmulation(); } functionRegistry.register( "overlay", @@ -415,6 +415,35 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio new TrimFunction( this, typeConfiguration, SqlAstNodeRenderingMode.NO_UNTYPED ) ); functionRegistry.register( "regexp_like", new InformixRegexpLikeFunction( typeConfiguration ) ); + + functionRegistry.register( + "trunc", + new TruncFunction( + "trunc(?1)", + "trunc(?1*pow(10,?2))/pow(10,?2)", + null, + null, + typeConfiguration + ) + ); + functionRegistry.registerAlternateKey( "truncate", "trunc" ); + + // For the count distinct emulation distinct + functionContributions.getFunctionRegistry().register( + "count", + new CountFunction( + this, + functionContributions.getTypeConfiguration(), + SqlAstNodeRenderingMode.DEFAULT, + "count", + "||", + null, + false, + null, + // Use chr(1), because chr(0) produces NULL + 1 + ) + ); } @Override @@ -473,7 +502,9 @@ protected SqlAstTranslator buildTranslator( @Override public String extractPattern(TemporalUnit unit) { return switch ( unit ) { - case SECOND -> getVersion().isBefore( 11, 70 ) ? "to_number(to_char(?2,'%S%F3'))" : "to_number(to_char(?2,'%S.%F3'))"; + case SECOND -> getVersion().isBefore( 11, 70 ) + ? "to_number(to_char(?2,'%S%F3'))" + : "to_number(to_char(?2,'%S.%F3'))"; case MINUTE -> "to_number(to_char(?2,'%M'))"; case HOUR -> "to_number(to_char(?2,'%H'))"; case DAY_OF_WEEK -> "(weekday(?2)+1)"; @@ -541,10 +572,21 @@ public String getAddPrimaryKeyConstraintString(String constraintName) { return " add constraint primary key constraint " + constraintName + " "; } + @Override + public String getAlterColumnTypeString(String columnName, String columnType, String columnDefinition) { + return "modify (" + columnName + " " + columnDefinition + ")"; + } + + @Override + public boolean supportsAlterColumnType() { + return true; + } + @Override public String getTruncateTableStatement(String tableName) { - return super.getTruncateTableStatement( tableName ) + " reuse storage" - + ( getVersion().isSameOrAfter( 12, 10 ) ? " keep statistics" : "" ); + // Use delete instead of truncate, because truncate will fail if another connection still holds a lock + // https://www.ibm.com/docs/en/informix-servers/12.10.0?topic=statement-restrictions-truncate + return "delete from " + tableName; } @Override @@ -579,16 +621,7 @@ public LimitHandler getLimitHandler() { @Override public LockingSupport getLockingSupport() { - // TODO: need a custom impl, because: - // 1. Informix does not support 'skip locked' - // 2. Informix does not allow 'for update' with joins - return LockingSupportSimple.STANDARD_SUPPORT; - } - - // TODO: remove once we have a custom LockingSupport impl - @Override @Deprecated(forRemoval = true) - public boolean supportsSkipLocked() { - return false; + return InformixLockingSupport.LOCKING_SUPPORT; } @Override @@ -778,7 +811,9 @@ public boolean isCurrentTimestampSelectStringCallable() { @Override public String getCurrentTimestampSelectString() { - return "select sysdate" + (getVersion().isBefore( 12, 10 ) ? " from informix.systables where tabid=1" : ""); + return getVersion().isBefore( 12, 10 ) + ? "select sysdate from informix.systables where tabid=1" + : "select sysdate"; } @Override @SuppressWarnings("deprecation") @@ -968,11 +1003,17 @@ public String currentDate() { @Override public String currentTime() { + // means 'current hour to fraction(3)' + // but note that subsecond precision + // requires USEOSTIME config parameter return "current hour to fraction"; } @Override public String currentTimestamp() { + // means 'current year to fraction(3)' + // but note that subsecond precision + // requires USEOSTIME config parameter return "current"; } @@ -1128,7 +1169,7 @@ public void contributeTypes(TypeContributions typeContributions, ServiceRegistry ObjectNullAsBinaryTypeJdbcType.INSTANCE, typeContributions.getTypeConfiguration() .getJavaTypeRegistry() - .getDescriptor( Object.class ) + .resolveDescriptor( Object.class ) ) ); } @@ -1173,6 +1214,11 @@ public boolean supportsRowValueConstructorSyntaxInInList() { return false; } + @Override + public boolean supportsTupleDistinctCounts() { + return false; + } + @Override public boolean supportsWithClause() { return getVersion().isSameOrAfter( 14,10 ); @@ -1194,6 +1240,8 @@ public IdentifierHelper buildIdentifierHelper(IdentifierHelperBuilder builder, @ @Override public DmlTargetColumnQualifierSupport getDmlTargetColumnQualifierSupport() { - return getVersion().isSameOrAfter( 12,10 ) ? DmlTargetColumnQualifierSupport.TABLE_ALIAS : DmlTargetColumnQualifierSupport.NONE; + return getVersion().isSameOrAfter( 12,10 ) + ? DmlTargetColumnQualifierSupport.TABLE_ALIAS + : DmlTargetColumnQualifierSupport.NONE; } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/InformixLockingSupport.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/InformixLockingSupport.java new file mode 100644 index 000000000000..6aaafa0a9ecf --- /dev/null +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/InformixLockingSupport.java @@ -0,0 +1,110 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.community.dialect; + +import jakarta.persistence.Timeout; +import org.hibernate.HibernateException; +import org.hibernate.Timeouts; +import org.hibernate.dialect.RowLockStrategy; +import org.hibernate.dialect.lock.internal.Helper; +import org.hibernate.dialect.lock.spi.ConnectionLockTimeoutStrategy; +import org.hibernate.dialect.lock.spi.LockTimeoutType; +import org.hibernate.dialect.lock.spi.LockingSupport; +import org.hibernate.dialect.lock.spi.OuterJoinLockingType; +import org.hibernate.engine.spi.SessionFactoryImplementor; + +import java.sql.Connection; + +import static org.hibernate.Timeouts.NO_WAIT_MILLI; +import static org.hibernate.Timeouts.SKIP_LOCKED_MILLI; +import static org.hibernate.Timeouts.WAIT_FOREVER_MILLI; + +public class InformixLockingSupport implements LockingSupport, LockingSupport.Metadata, ConnectionLockTimeoutStrategy { + public static final LockingSupport LOCKING_SUPPORT = new InformixLockingSupport(); + + public InformixLockingSupport() { + } + + @Override + public Metadata getMetadata() { + return this; + } + + @Override + public RowLockStrategy getWriteRowLockStrategy() { + return RowLockStrategy.COLUMN; + } + + @Override + public LockTimeoutType getLockTimeoutType(Timeout timeout) { + return switch ( timeout.milliseconds() ) { + case SKIP_LOCKED_MILLI -> LockTimeoutType.NONE; + // we can apply a timeout via the connection + default -> LockTimeoutType.CONNECTION; + }; + } + + @Override + public OuterJoinLockingType getOuterJoinLockingType() { + return OuterJoinLockingType.UNSUPPORTED; + } + + @Override + public ConnectionLockTimeoutStrategy getConnectionLockTimeoutStrategy() { + return this; + } + + @Override + public Level getSupportedLevel() { + return Level.SUPPORTED; + } + + @Override + public Timeout getLockTimeout(Connection connection, SessionFactoryImplementor factory) { + return Helper.getLockTimeout( + "select scs_lockmode from sysmaster:syssqlcurses where scs_sessionid = dbinfo('sessionid')", + (resultSet) -> { + final int seconds = resultSet.getInt( 1 ); + return switch ( seconds ) { + case -1 -> Timeouts.WAIT_FOREVER; + case 0 -> Timeouts.NO_WAIT; + default -> Timeout.seconds( seconds ); + }; + }, + connection, + factory + ); + } + + @Override + public void setLockTimeout(Timeout timeout, Connection connection, SessionFactoryImplementor factory) { + final int milliseconds = timeout.milliseconds(); + if ( milliseconds == SKIP_LOCKED_MILLI ) { + throw new HibernateException( "Connection lock-timeout does not accept skip-locked" ); + } + if ( milliseconds == WAIT_FOREVER_MILLI ) { + Helper.setLockTimeout( + "set lock mode to wait", + connection, + factory + ); + } + else if ( milliseconds == NO_WAIT_MILLI ) { + Helper.setLockTimeout( + "set lock mode to not wait", + connection, + factory + ); + } + else { + Helper.setLockTimeout( + (int) Math.ceil( (double) milliseconds / 1000), + "set lock mode to wait %s", + connection, + factory + ); + } + } +} 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 1ae2b05df9f3..01f16e93487b 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 @@ -6,11 +6,13 @@ import java.util.List; +import org.hibernate.Locking; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.query.IllegalQueryOperationException; import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.query.sqm.sql.internal.SqmParameterInterpretation; import org.hibernate.sql.ast.Clause; +import org.hibernate.sql.ast.spi.LockingClauseStrategy; import org.hibernate.sql.ast.spi.SqlAstTranslatorWithMerge; import org.hibernate.sql.ast.spi.SqlSelection; import org.hibernate.sql.ast.tree.Statement; @@ -338,4 +340,25 @@ protected void visitUpdateStatementOnly(UpdateStatement statement) { super.visitUpdateStatementOnly( statement ); } } + + @Override + protected LockStrategy determineLockingStrategy(QuerySpec querySpec, Locking.FollowOn followOnStrategy) { + final LockStrategy lockStrategy = super.determineLockingStrategy( querySpec, followOnStrategy ); + final LockingClauseStrategy lockingClauseStrategy = getLockingClauseStrategy(); + if ( lockingClauseStrategy != null && lockingClauseStrategy.containsJoins() ) { + // Informix does not allow FOR UPDATE when the query also contains joins + if ( followOnStrategy == Locking.FollowOn.DISALLOW ) { + throw new IllegalQueryOperationException( "Locking with joins is not supported" ); + } + else if ( followOnStrategy == Locking.FollowOn.IGNORE ) { + return LockStrategy.NONE; + } + else { + return LockStrategy.FOLLOW_ON; + } + } + else { + return lockStrategy; + } + } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/InterSystemsIRISDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/InterSystemsIRISDialect.java new file mode 100644 index 000000000000..304ce6f4c02c --- /dev/null +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/InterSystemsIRISDialect.java @@ -0,0 +1,914 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.community.dialect; + +import jakarta.persistence.TemporalType; +import org.hibernate.LockMode; +import org.hibernate.LockOptions; +import org.hibernate.ScrollMode; +import org.hibernate.boot.model.FunctionContributions; +import org.hibernate.boot.model.TypeContributions; +import org.hibernate.cfg.Environment; +import org.hibernate.community.dialect.function.InterSystemsIRISLogFunction; +import org.hibernate.community.dialect.identity.InterSystemsIRISIdentityColumnSupport; +import org.hibernate.community.dialect.pagination.InterSystemsIRISLimitHandler; +import org.hibernate.dialect.DatabaseVersion; +import org.hibernate.dialect.Dialect; +import org.hibernate.dialect.DmlTargetColumnQualifierSupport; +import org.hibernate.dialect.NationalizationSupport; +import org.hibernate.dialect.Replacer; +import org.hibernate.dialect.function.CommonFunctionFactory; +import org.hibernate.dialect.function.ExtractFunction; +import org.hibernate.dialect.function.LengthFunction; +import org.hibernate.dialect.function.TruncFunction; +import org.hibernate.dialect.identity.IdentityColumnSupport; +import org.hibernate.dialect.lock.LockingStrategy; +import org.hibernate.dialect.lock.OptimisticForceIncrementLockingStrategy; +import org.hibernate.dialect.lock.OptimisticLockingStrategy; +import org.hibernate.dialect.lock.PessimisticForceIncrementLockingStrategy; +import org.hibernate.dialect.lock.PessimisticReadSelectLockingStrategy; +import org.hibernate.dialect.lock.PessimisticReadUpdateLockingStrategy; +import org.hibernate.dialect.lock.PessimisticWriteSelectLockingStrategy; +import org.hibernate.dialect.lock.PessimisticWriteUpdateLockingStrategy; +import org.hibernate.dialect.lock.SelectLockingStrategy; +import org.hibernate.dialect.lock.UpdateLockingStrategy; +import org.hibernate.dialect.lock.internal.NoLockingSupport; +import org.hibernate.dialect.lock.spi.LockingSupport; +import org.hibernate.dialect.pagination.LimitHandler; +import org.hibernate.dialect.temptable.TemporaryTableKind; +import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo; +import org.hibernate.engine.jdbc.env.spi.IdentifierCaseStrategy; +import org.hibernate.engine.jdbc.env.spi.IdentifierHelper; +import org.hibernate.engine.jdbc.env.spi.IdentifierHelperBuilder; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.exception.ConstraintViolationException; +import org.hibernate.exception.DataException; +import org.hibernate.exception.LockAcquisitionException; +import org.hibernate.exception.LockTimeoutException; +import org.hibernate.exception.SQLGrammarException; +import org.hibernate.exception.spi.SQLExceptionConversionDelegate; +import org.hibernate.exception.spi.TemplatedViolatedConstraintNameExtractor; +import org.hibernate.exception.spi.ViolatedConstraintNameExtractor; +import org.hibernate.metamodel.mapping.EntityMappingType; +import org.hibernate.metamodel.spi.RuntimeModelCreationContext; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.query.common.FetchClauseType; +import org.hibernate.query.common.TemporalUnit; +import org.hibernate.query.sqm.IntervalType; +import org.hibernate.query.sqm.mutation.internal.temptable.GlobalTemporaryTableInsertStrategy; +import org.hibernate.query.sqm.mutation.internal.temptable.GlobalTemporaryTableMutationStrategy; +import org.hibernate.query.sqm.mutation.spi.AfterUseAction; +import org.hibernate.query.sqm.mutation.spi.BeforeUseAction; +import org.hibernate.query.sqm.mutation.spi.SqmMultiTableInsertStrategy; +import org.hibernate.query.sqm.mutation.spi.SqmMultiTableMutationStrategy; +import org.hibernate.service.ServiceRegistry; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.SqlAstTranslatorFactory; +import org.hibernate.sql.ast.spi.LockingClauseStrategy; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.spi.StandardSqlAstTranslatorFactory; +import org.hibernate.sql.ast.tree.Statement; +import org.hibernate.sql.ast.tree.select.QuerySpec; +import org.hibernate.sql.exec.spi.JdbcOperation; +import org.hibernate.type.StandardBasicTypes; +import org.hibernate.type.descriptor.jdbc.BlobJdbcType; +import org.hibernate.type.descriptor.jdbc.ClobJdbcType; +import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; + +import java.sql.CallableStatement; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.time.temporal.TemporalAccessor; +import java.util.TimeZone; + +import static org.hibernate.exception.spi.TemplatedViolatedConstraintNameExtractor.extractUsingTemplate; +import static org.hibernate.sql.ast.internal.NonLockingClauseStrategy.NON_CLAUSE_STRATEGY; +import static org.hibernate.type.SqlTypes.BIT; +import static org.hibernate.type.SqlTypes.BLOB; +import static org.hibernate.type.SqlTypes.CLOB; +import static org.hibernate.type.SqlTypes.BOOLEAN; +import static org.hibernate.type.SqlTypes.LONG32VARBINARY; +import static org.hibernate.type.SqlTypes.LONG32VARCHAR; +import static org.hibernate.type.SqlTypes.NCLOB; +import static org.hibernate.type.SqlTypes.TIMESTAMP; +import static org.hibernate.type.SqlTypes.TIMESTAMP_UTC; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsDate; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTime; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTimestampWithNanos; + + +/** + * A Hibernate dialect for InterSystems IRIS + * intended for Hibernate 7.1+ and jdk 1.8+ + */ +public class InterSystemsIRISDialect extends Dialect { + + private static final DatabaseVersion MINIMUM_VERSION = DatabaseVersion.make( 2025, 3 ); + + public InterSystemsIRISDialect() { + this( MINIMUM_VERSION ); + } + + public InterSystemsIRISDialect(DatabaseVersion version) { + super( version ); + } + + public InterSystemsIRISDialect(DialectResolutionInfo info) { + super( info ); + } + + @Override + protected DatabaseVersion getMinimumSupportedVersion() { + return MINIMUM_VERSION; + } + + /** + * Register SQL Functions + */ + @Override + public void initializeFunctionRegistry(FunctionContributions functionContributions) { + super.initializeFunctionRegistry( functionContributions ); + final var typeConfiguration = functionContributions.getTypeConfiguration(); + final var functionRegistry = functionContributions.getFunctionRegistry(); + final var functionFactory = new CommonFunctionFactory( functionContributions ); + final var basicTypeRegistry = typeConfiguration.getBasicTypeRegistry(); + final var doubleType = basicTypeRegistry.resolve( StandardBasicTypes.DOUBLE ); + + functionFactory.ascii(); + functionFactory.bitLength_pattern( "length(?1)*8" ); + functionFactory.char_chr(); + functionFactory.chr_char(); + functionFactory.cot(); + functionFactory.concat_pipeOperator(); + functionFactory.datepartDatename(); + functionFactory.dayofweekmonthyear(); + + functionRegistry.patternDescriptorBuilder( "log10", "log10(?1)" ) + .setInvariantType( doubleType ) + .setExactArgumentCount( 1 ) + .register(); + + functionFactory.lowerUpper(); + functionFactory.nullif(); + functionFactory.round_round(); + + functionRegistry.register( + "trunc", + new TruncFunction( "truncate(?1,0)", "truncate(?1,?2)", + TruncFunction.DatetimeTrunc.FORMAT, "to_timestamp", typeConfiguration ) + ); + + functionRegistry.registerAlternateKey( "truncate", "trunc" ); + functionContributions.getFunctionRegistry().register( + "extract", + new ExtractFunction( this, typeConfiguration ) + ); + + functionFactory.locate_positionSubstring(); + functionContributions.getFunctionRegistry() + .register( "log", new InterSystemsIRISLogFunction( typeConfiguration ) ); + functionRegistry.registerAlternateKey( "ln", "log" ); + functionFactory.characterLength_len(); + functionFactory.hourMinuteSecond(); + functionFactory.yearMonthDay(); + functionFactory.daynameMonthname(); + functionFactory.nowCurdateCurtime(); + functionFactory.substr(); + functionFactory.sysdate(); + functionFactory.weekQuarter(); + functionFactory.position(); + functionFactory.repeat_replicate(); + functionFactory.trim1(); + functionFactory.pi(); + functionFactory.space(); + functionFactory.degrees(); + functionFactory.radians(); + functionFactory.concat_pipeOperator( "SUBSTRING(?1,1) || SUBSTRING(?2,1)" ); + functionRegistry.register( + "bit_length", + new LengthFunction( "bit_length", "LENGTH(?1)*8", "(CHARACTER_LENGTH(?1) * 8)", typeConfiguration ) + ); + functionFactory.characterLength_length( "character_length(?1)" ); + functionFactory.octetLength_pattern( "length(?1)", "character_length(?1)" ); + + } + + + @Override + protected void initDefaultProperties() { + getDefaultProperties().setProperty( Environment.USE_SQL_COMMENTS, "false" ); + } + + @Override + public void contributeTypes(TypeContributions typeContributions, ServiceRegistry serviceRegistry) { + super.contributeTypes( typeContributions, serviceRegistry ); + JdbcTypeRegistry jdbcTypeRegistry = typeContributions.getTypeConfiguration().getJdbcTypeRegistry(); + jdbcTypeRegistry.addDescriptor( BLOB, BlobJdbcType.MATERIALIZED ); + jdbcTypeRegistry.addDescriptor( CLOB, ClobJdbcType.MATERIALIZED ); + } + + //sql type to column type mapping + @Override + protected String columnType(int sqlTypeCode) { + switch (sqlTypeCode) { + case BOOLEAN: + case BIT: + return "bit"; + case LONG32VARBINARY: + return "longvarbinary"; + case LONG32VARCHAR: + return "longvarchar"; + case NCLOB: + return "clob"; + case TIMESTAMP: + return "timestamp2"; + case TIMESTAMP_UTC: + return "timestamp"; + } + return super.columnType( sqlTypeCode ); + } + + + @Override + public boolean supportsSubselectAsInPredicateLHS() { + return false; + } + + public boolean supportsSubqueryOnMutatingTable() { + return false; + } + + @Override + public NationalizationSupport getNationalizationSupport() { + return NationalizationSupport.IMPLICIT; + } + + + @Override + public SqlAstTranslatorFactory getSqlAstTranslatorFactory() { + return new StandardSqlAstTranslatorFactory() { + @Override + protected SqlAstTranslator buildTranslator( + SessionFactoryImplementor sessionFactory, Statement statement) { + return new InterSystemsIRISSqlAstTranslator<>( sessionFactory, statement ); + } + }; + } + + + @Override + protected void registerDefaultKeywords() { + super.registerDefaultKeywords(); + String[] irisExtraKeywords = { + "ASSERTION","AVG","BIT","BIT_LENGTH","CHARACTER_LENGTH", + "CHAR_LENGTH","COALESCE","CONNECTION","CONSTRAINTS","CONVERT","COUNT","DEFERRABLE","DEFERRED","DESCRIPTOR","DIAGNOSTICS", + "DOMAIN","ENDEXEC","EXCEPTION","EXTRACT","FOUND","INITIALLY","ISOLATION", + "LEVEL","LOWER","MAX","MIN","NAMES","NULLIF","OCTET_LENGTH","OPTION","PAD","PARTIAL","PRIOR","PRIVILEGES","PUBLIC","READ","RELATIVE", + "RESTRICT","SCHEMA","SESSION_USER","SHARD", + "SPACE","SQLERROR","STATISTICS","SUBSTRING","SUM","SYSDATE", + "TEMPORARY","TOP","TRIM", + "UPPER","WORK","WRITE" + }; + + for ( String kw : irisExtraKeywords ) { + registerKeyword( kw ); + } + } + + // DDL support ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + @Override + public boolean hasAlterTable() { + // Does this dialect support the ALTER TABLE syntax? + return true; + } + + @Override + public boolean qualifyIndexName() { + return false; + } + + @Override + public boolean dropConstraints() { + return true; + } + + @Override + public boolean supportsCascadeDelete() { + return true; + } + + @Override + public boolean hasSelfReferentialForeignKeyBug() { + return true; + } + + @Override + public SqmMultiTableMutationStrategy getFallbackSqmMutationStrategy( + EntityMappingType rootEntityDescriptor, + RuntimeModelCreationContext runtimeModelCreationContext) { + + return new GlobalTemporaryTableMutationStrategy( rootEntityDescriptor, runtimeModelCreationContext ); + } + + + @Override + public boolean canCreateSchema() { + return false; + } + + @Override + public SqmMultiTableInsertStrategy getFallbackSqmInsertStrategy( + EntityMappingType rootEntityDescriptor, + RuntimeModelCreationContext runtimeModelCreationContext) { + + return new GlobalTemporaryTableInsertStrategy( rootEntityDescriptor, runtimeModelCreationContext ); + } + + + @Override + public TemporaryTableKind getSupportedTemporaryTableKind() { + return TemporaryTableKind.GLOBAL; + } + + @Override + public String getTemporaryTableCreateCommand() { + return "create global temporary table if not exists"; + } + + @Override + public String getTemporaryTableDropCommand() { + return "drop table"; + } + + @Override + public AfterUseAction getTemporaryTableAfterUseAction() { + return AfterUseAction.CLEAN; + } + + + @Override + public BeforeUseAction getTemporaryTableBeforeUseAction() { + return BeforeUseAction.CREATE; + } + + + // IDENTITY support ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + @Override + public IdentityColumnSupport getIdentityColumnSupport() { + return new InterSystemsIRISIdentityColumnSupport(); + } + + + @Override + public boolean supportsOuterJoinForUpdate() { + return false; + } + + @Override + public LockingStrategy getLockingStrategy(EntityPersister lockable, LockMode lockMode) { + + // Just to make some tests happy, but InterSystems IRIS doesn't really support this. + // need to use READ_COMMITTED as isolation level + + //InterSystemsIRIS does not current support "SELECT ... FOR UPDATE" syntax... + // Set your transaction mode to READ_COMMITTED before using + if ( lockMode == LockMode.PESSIMISTIC_FORCE_INCREMENT ) { + return new PessimisticForceIncrementLockingStrategy( lockable, lockMode ); + } + else if ( lockMode == LockMode.PESSIMISTIC_WRITE ) { + return lockable.isVersioned() + ? new PessimisticWriteUpdateLockingStrategy( lockable, lockMode ) + : new PessimisticWriteSelectLockingStrategy( lockable, lockMode ); + } + else if ( lockMode == LockMode.PESSIMISTIC_READ ) { + return lockable.isVersioned() + ? new PessimisticReadUpdateLockingStrategy( lockable, lockMode ) + : new PessimisticReadSelectLockingStrategy( lockable, lockMode ); + } + else if ( lockMode == LockMode.OPTIMISTIC ) { + return new OptimisticLockingStrategy( lockable, lockMode ); + } + else if ( lockMode == LockMode.OPTIMISTIC_FORCE_INCREMENT ) { + return new OptimisticForceIncrementLockingStrategy( lockable, lockMode ); + } + else if ( lockMode.greaterThan( LockMode.READ ) ) { + return new UpdateLockingStrategy( lockable, lockMode ); + } + else { + return new SelectLockingStrategy( lockable, lockMode ); + } + } + + + // The syntax used to add a foreign key constraint to a table. + @Override + public String getAddForeignKeyConstraintString( + String constraintName, + String[] foreignKey, + String referencedTable, + String[] primaryKey, + boolean referencesPrimaryKey) { + final String cols = String.join( ", ", foreignKey ); + final String referencedCols = String.join( ", ", primaryKey ); + return String.format( + " add constraint %s foreign key (%s) references %s (%s)", + constraintName, + cols, + referencedTable, + referencedCols + ); + } + + + // LIMIT support (also TOP) ~~~~~~~~~~~~~~~~~~~ + + @Override + public LimitHandler getLimitHandler() { + return InterSystemsIRISLimitHandler.INSTANCE; + } + + + // callable statement support ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + @Override + public int registerResultSetOutParameter(CallableStatement statement, int col) throws SQLException { + return col; + } + + @Override + public ResultSet getResultSet(CallableStatement ps) throws SQLException { + boolean isResultSet = ps.execute(); + while ( !isResultSet && ps.getUpdateCount() != -1 ) { + isResultSet = ps.getMoreResults(); + } + return ps.getResultSet(); + } + + + // miscellaneous support ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + @Override + public String getNoColumnsInsertString() { + // The keyword used to insert a row without specifying + // any column values + return " default values"; + } + + @Override + public SQLExceptionConversionDelegate buildSQLExceptionConversionDelegate() { + return (sqlException, message, sql) -> { + switch ( sqlException.getErrorCode() ) { + case 110: + return new LockTimeoutException( message, sqlException, sql ); + case 114: + return new LockAcquisitionException( message, sqlException, sql ); + case 30: // Table or view not found + return new SQLGrammarException( message, sqlException, sql ); + case 119, 120, 125: + // Unique constraint violation + return new ConstraintViolationException( message, sqlException, sql, ConstraintViolationException.ConstraintKind.UNIQUE, + getViolatedConstraintNameExtractor().extractConstraintName( sqlException ) ); + case 108: + // Null constraint violation + return new ConstraintViolationException( message, sqlException, sql, ConstraintViolationException.ConstraintKind.NOT_NULL, + getViolatedConstraintNameExtractor().extractConstraintName( sqlException ) ); + case 121, 122, 123, 124, 126,127: + // Foreign key constraint violation + return new ConstraintViolationException( message, sqlException, sql, ConstraintViolationException.ConstraintKind.FOREIGN_KEY, + getViolatedConstraintNameExtractor().extractConstraintName( sqlException ) ); + case 3819: + // Check constraint violation + return new ConstraintViolationException( message, sqlException, sql, ConstraintViolationException.ConstraintKind.CHECK, + getViolatedConstraintNameExtractor().extractConstraintName( sqlException ) ); + case 02, 21, 22: + return new DataException( message, sqlException, sql ); + } + return null; + }; + } + + @Override + public ViolatedConstraintNameExtractor getViolatedConstraintNameExtractor() { + return EXTRACTOR; + } + + /** + * The InterSystemsIRIS ViolatedConstraintNameExtracter. + */ + + private static final ViolatedConstraintNameExtractor EXTRACTOR = + new TemplatedViolatedConstraintNameExtractor( sqle -> extractUsingTemplate( "(", ")", sqle.getMessage() ) ); + + @Override + public boolean supportsColumnCheck() { + return false; + } + + @Override + public boolean supportsTupleDistinctCounts() { + return false; + } + + @Override + public ScrollMode defaultScrollMode() { + return super.defaultScrollMode(); + } + + @Override + public boolean supportsExistsInSelect() { + return false; + } + + @Override + public IdentifierHelper buildIdentifierHelper( + IdentifierHelperBuilder builder, DatabaseMetaData dbMetaData) throws SQLException { + if ( dbMetaData == null ) { + builder.setUnquotedCaseStrategy( IdentifierCaseStrategy.MIXED ); + builder.setQuotedCaseStrategy( IdentifierCaseStrategy.MIXED ); + } + else { + builder.applyIdentifierCasing( dbMetaData ); + } + + builder.applyReservedWords( getKeywords() ); + builder.setNameQualifierSupport( getNameQualifierSupport() ); + builder.setAutoQuoteKeywords( true ); + return super.buildIdentifierHelper( builder, dbMetaData ); + } + + + @Override + public boolean supportsNullPrecedence() { + return false; + } + + @Override + public boolean supportsLockTimeouts() { + return false; + } + + + @Override + public boolean supportsFetchClause(FetchClauseType type) { + return type == FetchClauseType.ROWS_ONLY; + } + + @Override + public boolean supportsLobValueChangePropagation() { + return false; + } + + @Override + public boolean supportsCurrentTimestampSelection() { + return true; + } + + @Override + public String getCurrentTimestampSelectString() { + return "SELECT CURRENT_TIMESTAMP"; + } + + @Override + public boolean isCurrentTimestampSelectStringCallable() { + return false; + } + + @Override + public boolean supportsValuesListForInsert() { + return false; + } + + @Override + public boolean supportsRowValueConstructorSyntax() { + return false; + } + + + @Override + public boolean supportsRowValueConstructorSyntaxInInList() { + return false; + } + + @Override + public boolean supportsRowValueConstructorSyntaxInQuantifiedPredicates() { + return false; + } + + @Override + public boolean supportsRowValueConstructorGtLtSyntax() { + return false; + } + + @Override + public int getMaxVarcharLength() { + return 32767; + } + + @Override + public boolean supportsOrderByInSubquery() { + return false; + } + + @Override + public boolean supportsLateral() { + return true; + } + + @Override + public String getForUpdateString() { + return ""; + } + + @SuppressWarnings("deprecation") + @Override + public String timestampaddPattern(TemporalUnit unit, TemporalType temporalType, IntervalType intervalType) { + switch (unit) { + case YEAR: return "{fn TIMESTAMPADD(SQL_TSI_YEAR, ?2, ?3)}"; + case QUARTER: return "{fn TIMESTAMPADD(SQL_TSI_QUARTER, ?2, ?3)}"; + case MONTH: return "{fn TIMESTAMPADD(SQL_TSI_MONTH, ?2, ?3)}"; + case WEEK: return "{fn TIMESTAMPADD(SQL_TSI_WEEK, ?2, ?3)}"; + case DAY: + case DAY_OF_MONTH: + return "{fn TIMESTAMPADD(SQL_TSI_DAY, ?2, ?3)}"; + case HOUR: return "{fn TIMESTAMPADD(SQL_TSI_HOUR, ?2, ?3)}"; + case MINUTE: return "{fn TIMESTAMPADD(SQL_TSI_MINUTE, ?2, ?3)}"; + case SECOND: return "dateadd(second, ?2, ?3)"; + case NANOSECOND: + return "{fn TIMESTAMPADD(SQL_TSI_FRAC_SECOND, (?2)/1000000, ?3)}"; + case NATIVE: + return "dateadd(microsecond, ?2, ?3)"; + default: + throw new UnsupportedOperationException( "Unsupported unit for TIMESTAMPADD: " + unit ); + } + } + + @SuppressWarnings("deprecation") + @Override + public String timestampdiffPattern(TemporalUnit unit, + TemporalType fromTemporalType, + TemporalType toTemporalType) { + if ( unit == null ) { + return "{fn TIMESTAMPDIFF(SQL_TSI_SECOND, ?2, ?3)}"; + } + switch (unit) { + case YEAR: + return "{fn TIMESTAMPDIFF(SQL_TSI_YEAR, ?2, ?3)}"; + case QUARTER: + return "({fn TIMESTAMPDIFF(SQL_TSI_MONTH, ?2, ?3)}/3)"; + case MONTH: + return "{fn TIMESTAMPDIFF(SQL_TSI_MONTH, ?2, ?3)}"; + case WEEK: + return "{fn TIMESTAMPDIFF(SQL_TSI_WEEK, ?2, ?3)}"; + case DAY: + case DAY_OF_MONTH: + return "{fn TIMESTAMPDIFF(SQL_TSI_DAY, ?2, ?3)}"; + case HOUR: + return "{fn TIMESTAMPDIFF(SQL_TSI_HOUR, ?2, ?3)}"; + case MINUTE: + return "{fn TIMESTAMPDIFF(SQL_TSI_MINUTE, ?2, ?3)}"; + case SECOND: + return "{fn TIMESTAMPDIFF(SQL_TSI_SECOND, ?2, ?3)}"; + case NANOSECOND: + return "({fn TIMESTAMPDIFF(SQL_TSI_FRAC_SECOND, ?2, ?3)}*1000000)"; + case NATIVE: + return "({fn TIMESTAMPDIFF(SQL_TSI_FRAC_SECOND, ?2, ?3)}*1000)"; + default: + throw new UnsupportedOperationException( "Unsupported TemporalUnit for TIMESTAMPDIFF: " + unit ); + } + } + @Override + public long getFractionalSecondPrecisionInNanos() { + return 1_000L; //default to nanoseconds for now + } + + @Override + public boolean supportsTableCheck() { + return false; + } + + @Override + public LockingClauseStrategy getLockingClauseStrategy(QuerySpec querySpec, LockOptions lockOptions) { + return NON_CLAUSE_STRATEGY; + } + + @Override + public LockingSupport getLockingSupport() { + return NoLockingSupport.NO_LOCKING_SUPPORT; + } + + + @Override + public void appendDateTimeLiteral( + SqlAppender appender, + TemporalAccessor temporalAccessor, + TemporalType precision, + TimeZone jdbcTimeZone) { + switch ( precision ) { + case DATE: + appender.appendSql( "'" ); + appendAsDate( appender, temporalAccessor ); + appender.appendSql( "'" ); + break; + case TIME: + appender.appendSql( "'" ); + appendAsTime( appender, temporalAccessor, supportsTemporalLiteralOffset(), jdbcTimeZone ); + appender.appendSql( "'" ); + break; + case TIMESTAMP: + appender.appendSql( "'" ); + appendAsTimestampWithNanos( appender, temporalAccessor, supportsTemporalLiteralOffset(), jdbcTimeZone ); + appender.appendSql( "'" ); + break; + default: + throw new IllegalArgumentException(); + } + } + + @Override + public void appendDatetimeFormat(SqlAppender appender, String format) { + appender.appendSql( datetimeFormat( format ).result() ); + } + + public static Replacer datetimeFormat(String format) { + return new Replacer( format, "'", "" ) + //era + .replace("GG", "AD") + .replace("G", "AD") + + //year + .replace("yyyy", "YYYY") + .replace("yyy", "YYYY") + .replace("yy", "YY") + .replace("y", "YYYY") + + //month of year + .replace("MMMM", "Month") + .replace("MMM", "Mon") + .replace("MM", "MM") + .replace("M", "MM") + + //week of year + .replace("ww", "IW") + .replace("w", "IW") + //year for week + .replace("YYYY", "IYYY") + .replace("YYY", "IYYY") + .replace("YY", "IY") + .replace("Y", "IYYY") + + //week of month + .replace("W", "W") + + //day of week + .replace("EEEE", "Day") + .replace("EEE", "Dy") + .replace("ee", "D") + .replace("e", "D") + + //day of month + .replace("dd", "DD") + .replace("d", "DD") + + //day of year + .replace("DDD", "DDD") + .replace("DD", "DDD") + .replace("D", "DDD") + + //am pm + .replace("a", "AM") + + //hour + .replace("hh", "HH12") + .replace("HH", "HH24") + .replace("h", "HH12") + .replace("H", "HH24") + + //minute + .replace("mm", "MI") + .replace("m", "MI") + + //second + .replace("ss", "SS") + .replace("s", "SS") + + //fractional seconds + .replace("SSSSSS", "FF6") + .replace("SSSSS", "FF5") + .replace("SSSS", "FF4") + .replace("SSS", "FF3") + .replace("SS", "FF2") + .replace("S", "FF1") + + //timezones + .replace("zzz", "TZR") + .replace("zz", "TZR") + .replace("z", "TZR") + .replace("ZZZ", "TZHTZM") + .replace("ZZ", "TZHTZM") + .replace("Z", "TZHTZM") + .replace("xxx", "TZH:TZM") + .replace("xx", "TZHTZM") + .replace("x", "TZH"); + } + + @Override + public String extractPattern(TemporalUnit unit) { + switch (unit) { + case DAY_OF_YEAR: + return "dayofyear(?2)"; + case DAY_OF_MONTH: + return "dayofmonth(?2)"; + case DAY_OF_WEEK: + return "dayofweek(?2)"; + case WEEK: + case WEEK_OF_YEAR: + return "week(?2)"; + case DAY: + return "day(?2)"; + case MONTH: + return "month(?2)"; + case YEAR: + return "year(?2)"; + case QUARTER: + return "quarter(?2)"; + case HOUR: + return "hour(?2)"; + case MINUTE: + return "minute(?2)"; + case SECOND: + return "second(?2)"; + case WEEK_OF_MONTH: + return "ceiling( (dayofmonth(?2) + dayofweek(dateadd('day', 1 - dayofmonth(?2), ?2)) - 1) / 7 )"; + case OFFSET: + case TIMEZONE_HOUR: + case TIMEZONE_MINUTE: + return null; + default: + return super.extractPattern( unit ); + } + } + + + @Override + public boolean requiresFloatCastingOfIntegerDivision() { + return true; + } + + @Override + public boolean supportsWithClauseInSubquery() { + return false; + } + + @Override + public boolean supportsWindowFunctions() { + return true; + } + + @Override + public boolean useInputStreamToInsertBlob() { + return false; + } + + @Override + public String translateExtractField(TemporalUnit unit) { + return switch (unit) { + case DAY_OF_MONTH -> "DAYOFMONTH"; + case DAY_OF_YEAR -> "DAYOFYEAR "; + case DAY_OF_WEEK -> "DAYOFWEEK "; + case EPOCH -> "TO_POSIXTIME"; + case DATE -> "DATE"; + default -> super.translateExtractField( unit ); + }; + } + + @Override + public int getPreferredSqlTypeCodeForBoolean() { + return Types.BIT; + } + + + @Override + public String getDual() { + return "(select 1)"; + } + + @Override + public int getInExpressionCountLimit() { + return 900; + } + + @Override + public boolean supportsFromClauseInUpdate() { + return true; + } + + @Override + public DmlTargetColumnQualifierSupport getDmlTargetColumnQualifierSupport() { + return DmlTargetColumnQualifierSupport.TABLE_ALIAS; + } +} diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/InterSystemsIRISSqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/InterSystemsIRISSqlAstTranslator.java new file mode 100644 index 000000000000..b0b1065100fe --- /dev/null +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/InterSystemsIRISSqlAstTranslator.java @@ -0,0 +1,204 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.community.dialect; + +import org.hibernate.dialect.DmlTargetColumnQualifierSupport; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.internal.util.collections.Stack; +import org.hibernate.query.sqm.ComparisonOperator; +import org.hibernate.sql.ast.Clause; +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.delete.DeleteStatement; +import org.hibernate.sql.ast.tree.expression.BinaryArithmeticExpression; +import org.hibernate.sql.ast.tree.expression.ColumnReference; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.SqlTuple; +import org.hibernate.sql.ast.tree.from.DerivedTableReference; +import org.hibernate.sql.ast.tree.from.QueryPartTableReference; +import org.hibernate.sql.ast.tree.from.ValuesTableReference; +import org.hibernate.sql.ast.tree.insert.InsertSelectStatement; +import org.hibernate.sql.ast.tree.select.QueryGroup; +import org.hibernate.sql.ast.tree.select.QueryPart; +import org.hibernate.sql.ast.tree.select.QuerySpec; +import org.hibernate.sql.ast.tree.update.UpdateStatement; +import org.hibernate.sql.exec.spi.JdbcOperation; + +import java.util.List; + +public class InterSystemsIRISSqlAstTranslator extends AbstractSqlAstTranslator { + + protected InterSystemsIRISSqlAstTranslator(SessionFactoryImplementor sessionFactory, Statement statement) { + super( sessionFactory, statement ); + } + + + @Override + protected void renderDeleteClause(DeleteStatement statement) { + appendSql( "delete" ); + final Stack clauseStack = getClauseStack(); + try { + clauseStack.push( Clause.DELETE ); + appendSql( " from " ); + renderDmlTargetTableExpression( statement.getTargetTable() ); + renderTableReferenceIdentificationVariable( statement.getTargetTable() ); + } + finally { + clauseStack.pop(); + } + } + + + @Override + protected void renderTupleComparisonStandard( + List lhsSelections, + SqlTuple rhsTuple, + ComparisonOperator operator + ) { + + if ( operator == ComparisonOperator.EQUAL || operator == ComparisonOperator.NOT_EQUAL ) { + emulateTupleComparisonSelections( lhsSelections, rhsTuple, operator ); + } + else { + + super.renderTupleComparisonStandard( lhsSelections, rhsTuple, operator ); + } + } + + @SuppressWarnings("unchecked") + protected void emulateTupleComparisonSelections( + List lhsSelections, + SqlTuple rhsTuple, + ComparisonOperator operator + ) { + final List rhsExpressions = (List) rhsTuple.getExpressions(); + + if ( lhsSelections.size() != rhsExpressions.size() ) { + throw new IllegalArgumentException( "Tuple size mismatch" ); + } + + final String joiner = ( operator == ComparisonOperator.EQUAL ) ? " and " : " or "; + + appendSql( OPEN_PARENTHESIS ); + for ( int i = 0; i < lhsSelections.size(); i++ ) { + if ( i > 0 ) { + appendSql( joiner ); + } + + lhsSelections.get( i ).getExpression().accept( this ); + appendSql( operator.sqlText() ); + rhsExpressions.get( i ).accept( this ); + } + appendSql( CLOSE_PARENTHESIS ); + } + + @Override + public void visitValuesTableReference(ValuesTableReference tableReference) { + emulateValuesTableReferenceColumnAliasing( tableReference ); + } + + @Override + protected void renderUpdateClause(UpdateStatement updateStatement) { + appendSql( "update" ); + final Stack clauseStack = getClauseStack(); + try { + clauseStack.push( Clause.UPDATE ); + append( WHITESPACE ); + renderDmlTargetTableExpression( updateStatement.getTargetTable() ); + renderTableReferenceIdentificationVariable( updateStatement.getTargetTable() ); + } + finally { + clauseStack.pop(); + } + } + + + @Override + protected String determineColumnReferenceQualifier(ColumnReference columnReference) { + final DmlTargetColumnQualifierSupport qualifierSupport = getDialect().getDmlTargetColumnQualifierSupport(); + final MutationStatement currentDmlStatement; + final String dmlAlias; + + if ( getClauseStack().getCurrent() != Clause.SET + || !( ( currentDmlStatement = getCurrentDmlStatement() ) instanceof InsertSelectStatement ) + || ( dmlAlias = currentDmlStatement.getTargetTable().getIdentificationVariable() ) == null + || !dmlAlias.equals( columnReference.getQualifier() ) ) { + return columnReference.getQualifier(); + } + // Qualify the column reference with the table expression also when in subqueries + else if ( qualifierSupport != DmlTargetColumnQualifierSupport.NONE || !getQueryPartStack().isEmpty() ) { + return getCurrentDmlStatement().getTargetTable().getTableExpression(); + } + else { + return null; + } + } + + @Override + public void visitBinaryArithmeticExpression(BinaryArithmeticExpression arithmeticExpression) { + if ( isIntegerDivisionEmulationRequired( arithmeticExpression ) ) { + visitArithmeticOperand( arithmeticExpression.getLeftHandOperand() ); + appendSql( " \\ " ); + visitArithmeticOperand( arithmeticExpression.getRightHandOperand() ); + } + else { + super.visitBinaryArithmeticExpression( arithmeticExpression ); + } + } + + @Override + public void visitQueryGroup(QueryGroup queryGroup) { + if ( shouldEmulateFetchClause( queryGroup ) ) { + emulateFetchOffsetWithWindowFunctions( queryGroup, true ); + } + else { + super.visitQueryGroup( queryGroup ); + } + } + + @Override + public void visitQuerySpec(QuerySpec querySpec) { + if ( shouldEmulateFetchClause( querySpec ) ) { + emulateFetchOffsetWithWindowFunctions( querySpec, true ); + } + else { + super.visitQuerySpec( querySpec ); + } + } + + protected boolean shouldEmulateFetchClause(QueryPart queryPart) { + // Check if current query part is already row numbering to avoid infinite recursion + if ( getQueryPartForRowNumbering() == queryPart || isRowsOnlyFetchClauseType( queryPart ) ) { + return false; + } + return !getDialect().supportsFetchClause( queryPart.getFetchClauseType() ); + } + + + @Override + protected void renderDerivedTableReferenceIdentificationVariable(DerivedTableReference tableReference) { + renderTableReferenceIdentificationVariable( tableReference ); + } + + @Override + public void visitQueryPartTableReference(QueryPartTableReference tableReference) { + emulateQueryPartTableReferenceColumnAliasing( tableReference ); + } + + @Override + protected void renderFromClauseAfterUpdateSet(UpdateStatement statement) { + if ( statement.getFromClause().getRoots().isEmpty() ) { + appendSql( " from " ); + renderDmlTargetTableExpression( statement.getTargetTable() ); + renderTableReferenceIdentificationVariable( statement.getTargetTable() ); + } + else { + visitFromClause( statement.getFromClause() ); + } + } + +} 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 96e57dc4ef63..bcdad37a6c9b 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 @@ -8,17 +8,17 @@ import java.util.List; import org.hibernate.dialect.DmlTargetColumnQualifierSupport; -import org.hibernate.dialect.sql.ast.MySQLSqlAstTranslator; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.internal.util.collections.Stack; import org.hibernate.metamodel.mapping.JdbcMappingContainer; import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.sql.ast.Clause; import org.hibernate.sql.ast.spi.AbstractSqlAstTranslator; +import org.hibernate.sql.ast.spi.NestedOrTargetTableCorrelationVisitor; +import org.hibernate.sql.ast.tree.AbstractUpdateOrDeleteStatement; import org.hibernate.sql.ast.tree.MutationStatement; import org.hibernate.sql.ast.tree.Statement; import org.hibernate.sql.ast.tree.delete.DeleteStatement; -import org.hibernate.sql.ast.tree.expression.CastTarget; import org.hibernate.sql.ast.tree.expression.ColumnReference; import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.Literal; @@ -43,6 +43,7 @@ * A SQL AST translator for MariaDB. * * @author Christian Beikov + * @author Yoobin Yoon */ public class MariaDBLegacySqlAstTranslator extends AbstractSqlAstTranslator { @@ -100,17 +101,24 @@ public void visitColumnReference(ColumnReference columnReference) { @Override protected void renderDeleteClause(DeleteStatement statement) { - appendSql( "delete" ); final Stack clauseStack = getClauseStack(); try { clauseStack.push( Clause.DELETE ); - renderTableReferenceIdentificationVariable( statement.getTargetTable() ); - if ( statement.getFromClause().getRoots().isEmpty() ) { - appendSql( " from " ); - renderDmlTargetTableExpression( statement.getTargetTable() ); + if ( usesSingleTableDml( statement ) ) { + appendSql( "delete from " ); + appendSql( statement.getTargetTable().getTableExpression() ); + registerAffectedTable( statement.getTargetTable() ); } else { - visitFromClause( statement.getFromClause() ); + appendSql( "delete" ); + renderTableReferenceIdentificationVariable( statement.getTargetTable() ); + if ( statement.getFromClause().getRoots().isEmpty() ) { + appendSql( " from " ); + renderDmlTargetTableExpression( statement.getTargetTable() ); + } + else { + visitFromClause( statement.getFromClause() ); + } } } finally { @@ -120,7 +128,12 @@ protected void renderDeleteClause(DeleteStatement statement) { @Override protected void renderUpdateClause(UpdateStatement updateStatement) { - if ( updateStatement.getFromClause().getRoots().isEmpty() ) { + if ( usesSingleTableDml( updateStatement ) ) { + appendSql( "update " ); + appendSql( updateStatement.getTargetTable().getTableExpression() ); + registerAffectedTable( updateStatement.getTargetTable() ); + } + else if ( updateStatement.getFromClause().getRoots().isEmpty() ) { super.renderUpdateClause( updateStatement ); } else { @@ -132,7 +145,7 @@ protected void renderUpdateClause(UpdateStatement updateStatement) { @Override protected void renderDmlTargetTableExpression(NamedTableReference tableReference) { super.renderDmlTargetTableExpression( tableReference ); - if ( getClauseStack().getCurrent() != Clause.INSERT ) { + if ( getClauseStack().getCurrent() != Clause.INSERT && !usesSingleTableDml( getCurrentDmlStatement() ) ) { renderTableReferenceIdentificationVariable( tableReference ); } } @@ -157,15 +170,24 @@ protected void visitConflictClause(ConflictClause conflictClause) { @Override protected String determineColumnReferenceQualifier(ColumnReference columnReference) { final DmlTargetColumnQualifierSupport qualifierSupport = getDialect().getDmlTargetColumnQualifierSupport(); - final MutationStatement currentDmlStatement; final String dmlAlias; // Since MariaDB does not support aliasing the insert target table, // we must detect column reference that are used in the conflict clause // and use the table expression as qualifier instead if ( getClauseStack().getCurrent() != Clause.SET - || !( ( currentDmlStatement = getCurrentDmlStatement() ) instanceof InsertSelectStatement ) - || ( dmlAlias = currentDmlStatement.getTargetTable().getIdentificationVariable() ) == null + || !( getCurrentDmlStatement() instanceof InsertSelectStatement insertSelectStatement ) + || ( dmlAlias = insertSelectStatement.getTargetTable().getIdentificationVariable() ) == null || !dmlAlias.equals( columnReference.getQualifier() ) ) { + final MutationStatement currentStatement = getCurrentDmlStatement(); + if ( currentStatement != null && usesSingleTableDml( currentStatement ) && columnReference.getQualifier() != null ) { + final NamedTableReference targetTable = currentStatement.getTargetTable(); + final String targetTableName = targetTable.getTableExpression(); + final String qualifier = columnReference.getQualifier(); + final String targetAlias = targetTable.getIdentificationVariable(); + if ( ( targetAlias != null && qualifier.equals( targetAlias ) ) || qualifier.equals( targetTableName ) ) { + return !getQueryPartStack().isEmpty() ? targetTableName : null; + } + } return columnReference.getQualifier(); } // Qualify the column reference with the table expression also when in subqueries @@ -375,17 +397,6 @@ private boolean supportsWindowFunctions() { return dialect.getVersion().isSameOrAfter( 10, 2 ); } - @Override - public void visitCastTarget(CastTarget castTarget) { - String sqlType = MySQLSqlAstTranslator.getSqlType( castTarget, getSessionFactory() ); - if ( sqlType != null ) { - appendSql( sqlType ); - } - else { - super.visitCastTarget( castTarget ); - } - } - @Override protected void renderStringContainsExactlyPredicate(Expression haystack, Expression needle) { // MariaDB can't cope with NUL characters in the position function, so we use a like predicate instead @@ -403,4 +414,72 @@ protected void appendAssignmentColumn(ColumnReference column) { ? determineColumnReferenceQualifier( column ) : null ); } + + private boolean usesSingleTableDml(MutationStatement statement) { + // As of MariaDB 11.1, the self-join rewrite optimization can handle this, so no need force single table DML + return getDialect().getVersion().isBefore( 11, 1 ) && hasTargetTableCorrelation( statement ); + } + + private boolean needsDmlSubqueryWrapper() { + final Statement statement = getStatement(); + // As of MariaDB 11.1, the self-join rewrite optimization can handle this, so no need for the wrapper + return getDialect().getVersion().isBefore( 11, 1 ) + && statement instanceof AbstractUpdateOrDeleteStatement updateOrDeleteStatement + && !NestedOrTargetTableCorrelationVisitor.hasCorrelation( updateOrDeleteStatement ); + } + + @Override + public void visitSelectStatement(SelectStatement statement) { + final boolean needsParenthesis = !statement.getQueryPart().isRoot(); + if ( needsParenthesis && needsDmlSubqueryWrapper() ) { + appendSql( OPEN_PARENTHESIS ); + appendSql( "select * from " ); + super.visitSelectStatement( statement ); + appendSql( " _sub_" ); + appendSql( CLOSE_PARENTHESIS ); + } + else { + super.visitSelectStatement( statement ); + } + } + + @Override + protected void renderRelationalEmulationSubQuery( + QuerySpec subQuery, + X lhsTuple, + SubQueryRelationalRestrictionEmulationRenderer renderer, + ComparisonOperator tupleComparisonOperator) { + if ( needsDmlSubqueryWrapper() ) { + appendSql( OPEN_PARENTHESIS ); + appendSql( "select * from " ); + super.renderRelationalEmulationSubQuery( subQuery, lhsTuple, renderer, tupleComparisonOperator ); + appendSql( " _sub_" ); + appendSql( CLOSE_PARENTHESIS ); + } + else { + super.renderRelationalEmulationSubQuery( subQuery, lhsTuple, renderer, tupleComparisonOperator ); + } + } + + @Override + protected void renderQuantifiedEmulationSubQuery( + QuerySpec subQuery, + ComparisonOperator tupleComparisonOperator) { + if ( needsDmlSubqueryWrapper() ) { + appendSql( OPEN_PARENTHESIS ); + appendSql( "select * from " ); + super.renderQuantifiedEmulationSubQuery( subQuery, tupleComparisonOperator ); + appendSql( " _sub_" ); + appendSql( CLOSE_PARENTHESIS ); + } + else { + super.renderQuantifiedEmulationSubQuery( subQuery, tupleComparisonOperator ); + } + } + + @Override + protected void renderFetchFirstRow() { + appendSql( " limit 1" ); + } + } 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 0e9fadbb6f8c..66e67c1ac372 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 @@ -756,7 +756,7 @@ public void contributeTypes(TypeContributions typeContributions, ServiceRegistry NullJdbcType.INSTANCE, typeContributions.getTypeConfiguration() .getJavaTypeRegistry() - .getDescriptor( Object.class ) + .resolveDescriptor( Object.class ) ) ); } 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 7326c6868df3..df385e80dab6 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 @@ -8,7 +8,6 @@ import java.util.List; import org.hibernate.dialect.DmlTargetColumnQualifierSupport; -import org.hibernate.dialect.sql.ast.MySQLSqlAstTranslator; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.internal.util.collections.Stack; import org.hibernate.query.sqm.ComparisonOperator; @@ -17,7 +16,6 @@ import org.hibernate.sql.ast.tree.MutationStatement; import org.hibernate.sql.ast.tree.Statement; import org.hibernate.sql.ast.tree.delete.DeleteStatement; -import org.hibernate.sql.ast.tree.expression.CastTarget; import org.hibernate.sql.ast.tree.expression.ColumnReference; import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.Literal; @@ -44,6 +42,7 @@ * A SQL AST translator for MySQL. * * @author Christian Beikov + * @author Yoobin Yoon */ public class MySQLLegacySqlAstTranslator extends AbstractSqlAstTranslator { @@ -366,17 +365,6 @@ public MySQLLegacyDialect getDialect() { return dialect; } - @Override - public void visitCastTarget(CastTarget castTarget) { - String sqlType = MySQLSqlAstTranslator.getSqlType( castTarget, getSessionFactory() ); - if ( sqlType != null ) { - appendSql( sqlType ); - } - else { - super.visitCastTarget( castTarget ); - } - } - @Override protected void renderStringContainsExactlyPredicate(Expression haystack, Expression needle) { // MySQL can't cope with NUL characters in the position function, so we use a like predicate instead @@ -394,4 +382,63 @@ protected void appendAssignmentColumn(ColumnReference column) { ? determineColumnReferenceQualifier( column ) : null ); } + + private boolean needsDmlSubqueryWrapper() { + final Statement statement = getStatement(); + return statement instanceof DeleteStatement || statement instanceof UpdateStatement; + } + + @Override + public void visitSelectStatement(SelectStatement statement) { + final boolean needsParenthesis = !statement.getQueryPart().isRoot(); + if ( needsParenthesis && needsDmlSubqueryWrapper() ) { + appendSql( OPEN_PARENTHESIS ); + appendSql( "select * from " ); + super.visitSelectStatement( statement ); + appendSql( " _sub_" ); + appendSql( CLOSE_PARENTHESIS ); + } + else { + super.visitSelectStatement( statement ); + } + } + + @Override + protected void renderRelationalEmulationSubQuery( + QuerySpec subQuery, + X lhsTuple, + SubQueryRelationalRestrictionEmulationRenderer renderer, + ComparisonOperator tupleComparisonOperator) { + if ( needsDmlSubqueryWrapper() ) { + appendSql( OPEN_PARENTHESIS ); + appendSql( "select * from " ); + super.renderRelationalEmulationSubQuery( subQuery, lhsTuple, renderer, tupleComparisonOperator ); + appendSql( " _sub_" ); + appendSql( CLOSE_PARENTHESIS ); + } + else { + super.renderRelationalEmulationSubQuery( subQuery, lhsTuple, renderer, tupleComparisonOperator ); + } + } + + @Override + protected void renderQuantifiedEmulationSubQuery( + QuerySpec subQuery, + ComparisonOperator tupleComparisonOperator) { + if ( needsDmlSubqueryWrapper() ) { + appendSql( OPEN_PARENTHESIS ); + appendSql( "select * from " ); + super.renderQuantifiedEmulationSubQuery( subQuery, tupleComparisonOperator ); + appendSql( " _sub_" ); + appendSql( CLOSE_PARENTHESIS ); + } + else { + super.renderQuantifiedEmulationSubQuery( subQuery, tupleComparisonOperator ); + } + } + + @Override + protected void renderFetchFirstRow() { + appendSql( " limit 1" ); + } } 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 c7a0a2d09167..8e3b9472d232 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 @@ -170,6 +170,7 @@ * @author Steve Ebersole * @author Gavin King * @author Loïc Lefèvre + * @author Yoobin Yoon */ public class OracleLegacyDialect extends Dialect { @@ -384,6 +385,8 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.arraySlice_oracle(); functionFactory.arrayReplace_oracle(); functionFactory.arrayTrim_oracle(); + functionFactory.arrayReverse_oracle(); + functionFactory.arraySort_oracle(); functionFactory.arrayFill_oracle(); functionFactory.arrayToString_oracle(); @@ -415,7 +418,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.xmlagg(); functionFactory.xmltable_oracle(); - functionFactory.unnest_oracle(); + functionFactory.unnest_oracle( getVersion().isSameOrAfter( 21 ) ); functionFactory.generateSeries_recursive( getMaximumSeriesSize(), true, false ); functionFactory.regexpLike_predicateFunction(); } @@ -1029,7 +1032,7 @@ public void contributeTypes(TypeContributions typeContributions, ServiceRegistry NullJdbcType.INSTANCE, typeContributions.getTypeConfiguration() .getJavaTypeRegistry() - .getDescriptor( Object.class ) + .resolveDescriptor( Object.class ) ) ); typeContributions.contributeType( @@ -1037,7 +1040,7 @@ public void contributeTypes(TypeContributions typeContributions, ServiceRegistry ObjectNullAsNullTypeJdbcType.INSTANCE, typeContributions.getTypeConfiguration() .getJavaTypeRegistry() - .getDescriptor( Object.class ) + .resolveDescriptor( Object.class ) ) ); } 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 9d29ba01e9c3..9060257fc620 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 @@ -171,6 +171,7 @@ * A {@linkplain Dialect SQL dialect} for PostgreSQL 8 and above. * * @author Gavin King + * @author Yoobin Yoon */ public class PostgreSQLLegacyDialect extends Dialect { @@ -668,6 +669,14 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio else { functionFactory.arrayTrim_unnest(); } + if ( getVersion().isSameOrAfter( 18 ) ) { + functionFactory.arrayReverse(); + functionFactory.arraySort(); + } + else { + functionFactory.arrayReverse_unnest(); + functionFactory.arraySort_unnest(); + } functionFactory.arrayFill_postgresql(); functionFactory.arrayToString_postgresql(); @@ -979,6 +988,11 @@ public String getCurrentTimestampSelectString() { return "select now()"; } + @Override + public boolean isCurrentTimestampStable() { + return true; + } + @Override public boolean supportsTupleCounts() { return true; @@ -1558,7 +1572,7 @@ protected void contributePostgreSQLTypes(TypeContributions typeContributions, Se ObjectNullAsBinaryTypeJdbcType.INSTANCE, typeContributions.getTypeConfiguration() .getJavaTypeRegistry() - .getDescriptor( Object.class ) + .resolveDescriptor( Object.class ) ) ); diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgresPlusLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgresPlusLegacyDialect.java index e1c3870663b6..d3d6a8507f95 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgresPlusLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgresPlusLegacyDialect.java @@ -53,6 +53,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio super.initializeFunctionRegistry(functionContributions); CommonFunctionFactory functionFactory = new CommonFunctionFactory(functionContributions); + functionFactory.arrayGet_bracket( false ); functionFactory.soundex(); functionFactory.rownumRowid(); functionFactory.sysdate(); 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 76d2f97ecce8..f33e9d4cefa4 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 @@ -665,7 +665,7 @@ public void contributeTypes(TypeContributions typeContributions, ServiceRegistry NullJdbcType.INSTANCE, typeContributions.getTypeConfiguration() .getJavaTypeRegistry() - .getDescriptor( Object.class ) + .resolveDescriptor( Object.class ) ) ); jdbcTypeRegistry.addDescriptor( EnumJdbcType.INSTANCE ); 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 188840b1491c..9cc4e5c30b8c 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 @@ -10,6 +10,7 @@ import org.hibernate.dialect.Dialect; import org.hibernate.dialect.DmlTargetColumnQualifierSupport; +import org.hibernate.dialect.function.array.DdlTypeHelper; import org.hibernate.engine.jdbc.Size; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.internal.util.collections.Stack; @@ -404,7 +405,7 @@ private boolean supportsWindowFunctions() { } public static String getSqlType(CastTarget castTarget, SessionFactoryImplementor factory) { - final String sqlType = getCastTypeName( castTarget, factory.getTypeConfiguration() ); + final String sqlType = DdlTypeHelper.getCastTypeName( castTarget, factory.getTypeConfiguration() ); return getSqlType( castTarget, sqlType, factory.getJdbcServices().getDialect() ); } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SpannerPostgreSQLDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SpannerPostgreSQLDialect.java new file mode 100644 index 000000000000..c283f1575411 --- /dev/null +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SpannerPostgreSQLDialect.java @@ -0,0 +1,1116 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.community.dialect; + +import jakarta.persistence.TemporalType; +import jakarta.persistence.Timeout; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.hibernate.LockOptions; +import org.hibernate.JDBCException; +import org.hibernate.ScrollMode; +import org.hibernate.Timeouts; +import org.hibernate.boot.model.FunctionContributions; +import org.hibernate.boot.model.TypeContributions; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.community.dialect.aggregate.SpannerPostgreSQLAggregateSupport; +import org.hibernate.community.dialect.sequence.SpannerPostgreSQLSequenceSupport; +import org.hibernate.community.dialect.sql.ast.SpannerPostgreSQLSqlAstTranslator; +import org.hibernate.dialect.DatabaseVersion; +import org.hibernate.dialect.function.CountFunction; +import org.hibernate.dialect.function.InsertSubstringOverlayEmulation; +import org.hibernate.dialect.function.array.ArrayContainsOperatorFunction; +import org.hibernate.dialect.function.array.ArrayIncludesOperatorFunction; +import org.hibernate.dialect.function.json.SpannerPostgreSQLJsonArrayFunction; +import org.hibernate.dialect.function.json.SpannerPostgreSQLJsonObjectFunction; +import org.hibernate.persister.entity.mutation.EntityMutationTarget; +import org.hibernate.query.sqm.CastType; +import org.hibernate.dialect.FunctionalDependencyAnalysisSupport; +import org.hibernate.dialect.FunctionalDependencyAnalysisSupportImpl; +import org.hibernate.dialect.PostgreSQLDialect; +import org.hibernate.dialect.aggregate.AggregateSupport; +import org.hibernate.dialect.function.CommonFunctionFactory; +import org.hibernate.dialect.function.SpannerConcatFunction; +import org.hibernate.dialect.function.array.SpannerPostgreSQLArrayConcatElementFunction; +import org.hibernate.dialect.function.array.SpannerPostgreSQLArrayTrimEmulation; +import org.hibernate.dialect.function.array.SpannerPostgreSQLArrayReplaceFunction; +import org.hibernate.dialect.function.array.SpannerPostgreSQLArrayRemoveFunction; +import org.hibernate.dialect.function.array.SpannerPostgreSQLArrayRemoveIndexFunction; +import org.hibernate.dialect.Replacer; +import org.hibernate.dialect.function.SpannerPostgreSQLRegexpLikeFunction; +import org.hibernate.dialect.function.SpannerPostgreSQLTruncFunction; +import org.hibernate.dialect.RowLockStrategy; +import org.hibernate.dialect.lock.PessimisticLockStyle; +import org.hibernate.dialect.lock.internal.NoLockingSupport; +import org.hibernate.dialect.lock.internal.LockingSupportSimple; +import org.hibernate.dialect.lock.spi.ConnectionLockTimeoutStrategy; +import org.hibernate.dialect.lock.spi.LockTimeoutType; +import org.hibernate.dialect.lock.spi.LockingSupport; +import org.hibernate.dialect.lock.spi.OuterJoinLockingType; +import org.hibernate.dialect.pagination.LimitHandler; +import org.hibernate.dialect.pagination.LimitOffsetLimitHandler; +import org.hibernate.dialect.sequence.SequenceSupport; +import org.hibernate.dialect.temptable.PersistentTemporaryTableStrategy; +import org.hibernate.dialect.temptable.TemporaryTableStrategy; +import org.hibernate.dialect.type.PostgreSQLArrayJdbcTypeConstructor; +import org.hibernate.dialect.type.PostgreSQLCastingJsonArrayJdbcTypeConstructor; +import org.hibernate.dialect.type.PostgreSQLCastingJsonJdbcType; +import org.hibernate.dialect.type.PostgreSQLUUIDJdbcType; +import org.hibernate.dialect.unique.AlterTableUniqueIndexDelegate; +import org.hibernate.dialect.unique.UniqueDelegate; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.model.MutationOperation; +import org.hibernate.sql.model.internal.OptionalTableUpdate; +import org.hibernate.sql.model.jdbc.OptionalTableUpdateWithOptionalRowCount; +import org.hibernate.sql.model.jdbc.OptionalTableUpdateWithUpsertOperation; + +import org.hibernate.engine.config.spi.ConfigurationService; +import org.hibernate.engine.config.spi.StandardConverters; +import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.exception.SQLGrammarException; +import org.hibernate.metamodel.mapping.EntityMappingType; +import org.hibernate.metamodel.spi.RuntimeModelCreationContext; +import org.hibernate.exception.ConstraintViolationException; +import org.hibernate.exception.spi.SQLExceptionConversionDelegate; +import org.hibernate.procedure.internal.StandardCallableStatementSupport; +import org.hibernate.procedure.spi.CallableStatementSupport; +import org.hibernate.query.common.FetchClauseType; +import org.hibernate.query.common.TemporalUnit; +import org.hibernate.query.sqm.IntervalType; +import org.hibernate.query.sqm.mutation.internal.temptable.PersistentTableInsertStrategy; +import org.hibernate.query.sqm.mutation.internal.temptable.PersistentTableMutationStrategy; +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.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.LockingClauseStrategy; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.spi.StandardSqlAstTranslatorFactory; +import org.hibernate.sql.ast.tree.Statement; +import org.hibernate.sql.ast.tree.select.QuerySpec; +import org.hibernate.sql.exec.spi.JdbcOperation; +import org.hibernate.tool.schema.extract.internal.InformationExtractorJdbcDatabaseMetaDataImpl; +import org.hibernate.tool.schema.extract.spi.ExtractionContext; +import org.hibernate.tool.schema.extract.spi.InformationExtractor; +import org.hibernate.tool.schema.internal.StandardTableExporter; +import org.hibernate.type.StandardBasicTypes; +import org.hibernate.type.descriptor.jdbc.BlobJdbcType; +import org.hibernate.type.descriptor.jdbc.ClobJdbcType; +import org.hibernate.type.descriptor.jdbc.SpannerLocalDateTimeJdbcType; +import org.hibernate.type.descriptor.jdbc.SpannerLocalTimeJdbcType; +import org.hibernate.type.descriptor.jdbc.SpannerTimeJdbcType; +import org.hibernate.type.descriptor.sql.internal.ArrayDdlTypeImpl; +import org.hibernate.type.descriptor.sql.internal.CapacityDependentDdlType; +import org.hibernate.type.descriptor.sql.internal.DdlTypeImpl; +import org.hibernate.type.spi.TypeConfiguration; + +import java.sql.SQLException; +import java.sql.Types; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetTime; +import java.time.ZoneOffset; +import java.time.temporal.ChronoField; +import java.time.temporal.TemporalAccessor; +import java.util.Calendar; +import java.util.Date; +import java.util.TimeZone; +import java.util.regex.Pattern; + +import static org.hibernate.sql.ast.internal.NonLockingClauseStrategy.NON_CLAUSE_STRATEGY; +import static org.hibernate.type.SqlTypes.BIGINT; +import static org.hibernate.type.SqlTypes.BLOB; +import static org.hibernate.type.SqlTypes.CHAR; +import static org.hibernate.type.SqlTypes.CLOB; +import static org.hibernate.type.SqlTypes.DECIMAL; +import static org.hibernate.type.SqlTypes.FLOAT; +import static org.hibernate.type.SqlTypes.INTEGER; +import static org.hibernate.type.SqlTypes.JSON; +import static org.hibernate.type.SqlTypes.NCLOB; +import static org.hibernate.type.SqlTypes.NUMERIC; +import static org.hibernate.type.SqlTypes.SMALLINT; +import static org.hibernate.type.SqlTypes.TIME; +import static org.hibernate.type.SqlTypes.TIMESTAMP; +import static org.hibernate.type.SqlTypes.TIMESTAMP_UTC; +import static org.hibernate.type.SqlTypes.TIMESTAMP_WITH_TIMEZONE; +import static org.hibernate.type.SqlTypes.TIME_UTC; +import static org.hibernate.type.SqlTypes.TINYINT; +import static org.hibernate.type.SqlTypes.UUID; +import static org.hibernate.type.SqlTypes.VARCHAR; + +public class SpannerPostgreSQLDialect extends PostgreSQLDialect { + + private final UniqueDelegate SPANNER_UNIQUE_DELEGATE = new AlterTableUniqueIndexDelegate( this ); + private final StandardTableExporter SPANNER_TABLE_EXPORTER = new SpannerPostgreSQLTableExporter( this ); + private final SequenceSupport SPANNER_SEQUENCE_SUPPORT = new SpannerPostgreSQLSequenceSupport(this); + + // This will use a monotonically increasing value that is within the range of a 32-bit integer + // as the primary key value. Since Spanner only supports bit-reversed sequences, this option + // range of a 32-bit integer. + // This workaround that is only intended for testing, and should not be used for primary key + // values in production. + private static final String USE_INTEGER_FOR_PRIMARY_KEY = "hibernate.dialect.spanner.use_integer_for_primary_key"; + private static final String USE_EMULATOR = "hibernate.dialect.spanner.use_emulator"; + + private boolean useIntegerForPrimaryKey; + private boolean useEmulator; + + private final LockingSupport SPANNER_LOCKING_SUPPORT = new LockingSupportSimple( + PessimisticLockStyle.CLAUSE, + RowLockStrategy.NONE, + LockTimeoutType.NONE, + OuterJoinLockingType.FULL, + ConnectionLockTimeoutStrategy.NONE + ); + + protected final static DatabaseVersion MINIMUM_POSTGRES_VERSION = DatabaseVersion.make( 15 ); + + private static final Pattern NOT_NULL_CONSTRAINT_PATTERN = Pattern.compile( ".*(must not be NULL in table|does not specify a non-null value for NOT NULL column|Cannot specify a null value for column).*" ); + private static final Pattern FOREIGN_KEY_CONSTRAINT_PATTERN = Pattern.compile( ".*Foreign key.*(constraint violation on table|constraint violation when deleting or updating referenced key|violated on table).*" ); + private static final Pattern CHECK_CONSTRAINT_PATTERN = Pattern.compile( ".*Check constraint.*" ); + private static final Pattern TABLE_DOES_NOT_EXIST_PATTERN = Pattern.compile( ".*relation.*does not exist.*" ); + + public SpannerPostgreSQLDialect() { + super(); + } + + public SpannerPostgreSQLDialect(DialectResolutionInfo info) { + super( info ); + } + + public SpannerPostgreSQLDialect(DatabaseVersion version) { + super( MINIMUM_POSTGRES_VERSION ); + } + + @Override + public void initializeFunctionRegistry(FunctionContributions functionContributions) { + super.initializeFunctionRegistry( functionContributions ); + + final var functionFactory = new CommonFunctionFactory( functionContributions ); + final var functionRegistry = functionContributions.getFunctionRegistry(); + + functionFactory.leftRight_substr(); + functionFactory.pi_acos(); + functionFactory.log_ln(); + functionFactory.degrees_acos(); + functionFactory.radians_acos(); + functionFactory.bitandorxornot_operator(); + functionFactory.characterLength_length( SqlAstNodeRenderingMode.DEFAULT); + functionFactory.dateTrunc(); + functionRegistry.registerAlternateKey("log10", "log"); + functionFactory.power_spanner(); + functionFactory.sqrt_spanner(); + functionFactory.substr(); + functionFactory.position_locate_spanner(); + functionFactory.round_spanner(); + functionFactory.log_spanner(); + functionFactory.sinh_exp(); + functionFactory.cosh_exp(); + functionFactory.tanh_exp(); + functionRegistry.register( + "count", + new CountFunction( + this, + functionContributions.getTypeConfiguration(), + SqlAstNodeRenderingMode.DEFAULT, + "||", + "varchar", + true + ) + ); + functionRegistry.registerPattern( + "chr", + "'~'", + functionContributions.getTypeConfiguration().getBasicTypeRegistry().resolve( StandardBasicTypes.STRING ) + ); + functionRegistry.registerPattern( + "var_pop", + "(avg(?1 * ?1)-power(cast(avg(?1) as float8),cast(2 as float8)))" ); + functionRegistry.registerPattern( + "stddev_pop", + "sqrt(avg(?1 * ?1)-power(cast(avg(?1) as float8),cast(2 as float8)))" ); + + functionFactory.varSamp_sumCount_spanner(); + functionFactory.stddevSamp_sumCount_spanner(); + + functionFactory.octetLength_pattern("length(?1)"); + functionFactory.bitLength_pattern("length(?1)*8"); + functionFactory.sha("sha256(?1)"); + + functionRegistry.register( "concat", + new SpannerConcatFunction( functionContributions.getTypeConfiguration()) ); + functionRegistry.register( "regexp_like", + new SpannerPostgreSQLRegexpLikeFunction(functionContributions.getTypeConfiguration())); + functionRegistry.register( "trunc", + new SpannerPostgreSQLTruncFunction(functionContributions.getTypeConfiguration())); + functionRegistry.registerAlternateKey("truncate", "trunc"); + functionRegistry.register( "overlay", + new InsertSubstringOverlayEmulation(functionContributions.getTypeConfiguration(), false)); + + // Postgres uses # instead of ^ for XOR + functionRegistry.patternDescriptorBuilder( "bitxor", "(?1#?2)" ) + .setExactArgumentCount( 2 ) + .setArgumentTypeResolver( StandardFunctionArgumentTypeResolvers.ARGUMENT_OR_IMPLIED_RESULT_TYPE ) + .register(); + + functionRegistry.register( "json_array", + new SpannerPostgreSQLJsonArrayFunction( functionContributions.getTypeConfiguration() ) ); + functionRegistry.register( "json_object", + new SpannerPostgreSQLJsonObjectFunction( functionContributions.getTypeConfiguration() ) ); + + functionFactory.unnest_postgresql( false ); + functionFactory.arrayLength_spannerpg(); + + functionRegistry.register( "array_prepend", new SpannerPostgreSQLArrayConcatElementFunction( true ) ); + functionRegistry.register( "array_append", new SpannerPostgreSQLArrayConcatElementFunction( false ) ); + functionRegistry.register( "array_trim", new SpannerPostgreSQLArrayTrimEmulation() ); + functionRegistry.register( "array_replace", new SpannerPostgreSQLArrayReplaceFunction() ); + functionRegistry.register( "array_remove", new SpannerPostgreSQLArrayRemoveFunction() ); + functionRegistry.register( "array_remove_index", new SpannerPostgreSQLArrayRemoveIndexFunction( true ) ); + functionRegistry.register( "array_contains", new ArrayContainsOperatorFunction( false, functionContributions.getTypeConfiguration() ) ); + functionRegistry.register( "array_includes", new ArrayIncludesOperatorFunction( false, functionContributions.getTypeConfiguration() ) ); + functionRegistry.register( "array_includes_nullable", new ArrayIncludesOperatorFunction( true, functionContributions.getTypeConfiguration() ) ); + } + + @Override + protected void registerJsonFunction(CommonFunctionFactory functionFactory) { + functionFactory.jsonObject_postgresql(); + functionFactory.jsonArray_postgresql(); + functionFactory.jsonSet_postgresql(); + functionFactory.jsonRemove_postgresql(); + functionFactory.jsonReplace_postgresql(); + functionFactory.jsonArrayInsert_postgresql(); + } + + @Override + protected void registerArrayFunctions(CommonFunctionFactory functionFactory) { + functionFactory.array_postgresql(); + functionFactory.arrayAggregate(); + functionFactory.arrayConcat_postgresql(); + functionFactory.arrayPrepend_postgresql(); + functionFactory.arrayAppend_postgresql(); + functionFactory.arrayIntersects_postgresql(); + functionFactory.arrayGet_bracket(); + functionFactory.arraySlice_operator(); + functionFactory.arrayReplace(); + functionFactory.arrayReverse_unnest(); + functionFactory.arraySort_unnest(); + functionFactory.arrayToString_postgresql(); + } + + @Override + protected void registerXmlFunctions(CommonFunctionFactory functionFactory) { + } + + @Override + protected void registerUtilityFunctions(FunctionContributions functionContributions) { + } + + @Override + protected void initDefaultProperties() { + super.initDefaultProperties(); + getDefaultProperties().setProperty( AvailableSettings.PREFERRED_POOLED_OPTIMIZER, "none" ); + } + + @Override + public String getArrayTypeName(String javaElementTypeName, String elementTypeName, Integer maxLength) { + if ( elementTypeName != null && elementTypeName.equals( "varchar" ) ) { + elementTypeName = "text"; + } + return super.getArrayTypeName( javaElementTypeName, elementTypeName, maxLength ); + } + + @Override + public StandardTableExporter getTableExporter() { + return SPANNER_TABLE_EXPORTER; + } + + @Override + public UniqueDelegate getUniqueDelegate() { + return SPANNER_UNIQUE_DELEGATE; + } + + @Override + public SequenceSupport getSequenceSupport() { + return SPANNER_SEQUENCE_SUPPORT; + } + + @Override + public LimitHandler getLimitHandler() { + return LimitOffsetLimitHandler.INSTANCE; + } + + @Override + public String getCheckCondition(String columnName, String[] values) { + final StringBuilder check = new StringBuilder(); + check.append("("); + String separator = ""; + boolean nullIsValid = false; + for (String value : values) { + if (value == null) { + nullIsValid = true; + continue; + } + check.append(separator).append(columnName).append("='").append(value).append("'"); + separator = " or "; + } + check.append(")"); + if (nullIsValid) { + check.append(" or ").append(columnName).append(" is null"); + } + return check.toString(); + } + + @Override + public String getCheckCondition(String columnName, Long[] values) { + final StringBuilder check = new StringBuilder(); + check.append("("); + String separator = ""; + boolean nullIsValid = false; + for (Long value : values) { + if (value == null) { + nullIsValid = true; + continue; + } + check.append(separator).append(columnName).append("=").append(value); + separator = " or "; + } + check.append(")"); + if (nullIsValid) { + check.append(" or ").append(columnName).append(" is null"); + } + return check.toString(); + } + + @Override + public String getCheckCondition(String columnName, java.util.Collection valueSet, + org.hibernate.type.descriptor.jdbc.JdbcType jdbcType) { + final boolean isCharacterJdbcType = org.hibernate.type.SqlTypes.isCharacterType(jdbcType.getJdbcTypeCode()); + + final StringBuilder check = new StringBuilder(); + check.append("("); + String separator = ""; + boolean nullIsValid = false; + for (Object value : valueSet) { + if (value == null) { + nullIsValid = true; + continue; + } + check.append(separator).append(columnName).append("="); + if (isCharacterJdbcType) { + check.append("'").append(String.valueOf(value).replace("'", "''")).append("'"); + } + else { + check.append(value); + } + separator = " or "; + } + check.append(")"); + if (nullIsValid) { + check.append(" or ").append(columnName).append(" is null"); + } + return check.toString(); + } + + // @Override + // public String getCheckCondition(String columnName, long[] values) { + // final Long[] boxedValues = new Long[values.length]; + // for (int i = 0; i < values.length; i++) { + // boxedValues[i] = values[i]; + // } + // return getCheckCondition(columnName, boxedValues); + // } + + @Override + public AggregateSupport getAggregateSupport() { + return SpannerPostgreSQLAggregateSupport.INSTANCE; + } + + @Override + public LockingSupport getLockingSupport() { + return useEmulator ? NoLockingSupport.NO_LOCKING_SUPPORT : SPANNER_LOCKING_SUPPORT; + } + + @Override + protected Integer resolveSqlTypeCode(String columnTypeName, TypeConfiguration typeConfiguration) { + return switch (columnTypeName) { + case "character varying" -> Types.VARCHAR; + case "timestamp with time zone" -> Types.TIMESTAMP_WITH_TIMEZONE; + case "bigint" -> Types.BIGINT; + case "real" -> Types.REAL; // Use REAL instead of FLOAT to get Float as recommended Java type + default -> super.resolveSqlTypeCode( columnTypeName, typeConfiguration ); + }; + } + + @Override + public boolean supportsFetchClause(FetchClauseType type) { + return false; + } + + @Override + public String getForUpdateString() { + return " for update"; + } + + @Override + public String getForUpdateString(String aliases) { + return getForUpdateString(); + } + + @Override + public String getWriteLockString(int timeout) { + validateSpannerLockTimeout( timeout ); + return getForUpdateString(); + } + + @Override + public String getWriteLockString(String aliases, int timeout) { + return getWriteLockString( timeout ); + } + + @Override + public String getWriteLockString(Timeout timeout) { + return getWriteLockString( timeout.milliseconds() ); + } + + @Override + public String getWriteLockString(String aliases, Timeout timeout) { + return getWriteLockString( aliases, timeout.milliseconds() ); + } + + @Override + public String getReadLockString(int timeout) { + validateSpannerLockTimeout( timeout ); + return getForUpdateString(); + } + + @Override + public String getReadLockString(Timeout timeout) { + return getWriteLockString( timeout.milliseconds() ); + } + + @Override + public String getReadLockString(String aliases, Timeout timeout) { + return getWriteLockString( timeout.milliseconds() ); + } + + @Override + public String getReadLockString(String aliases, int timeout) { + return getWriteLockString( timeout ); + } + + @Override + public String getForUpdateNowaitString() { + throw new UnsupportedOperationException( + "Spanner doesn't support for-update with no-wait timeout" ); + } + + @Override + public String getForUpdateNowaitString(String aliases) { + return getForUpdateNowaitString(); + } + + @Override + public String getForUpdateSkipLockedString() { + throw new UnsupportedOperationException( + "Spanner doesn't support for-update with skip locked timeout" ); + } + + @Override + public String getForUpdateSkipLockedString(String aliases) { + return getForUpdateSkipLockedString(); + } + + @Override + public LockingClauseStrategy getLockingClauseStrategy( + QuerySpec querySpec, LockOptions lockOptions) { + if ( lockOptions == null ) { + return NON_CLAUSE_STRATEGY; + } + validateSpannerLockTimeout( lockOptions.getTimeOut() ); + return super.getLockingClauseStrategy( querySpec, lockOptions ); + } + + private static void validateSpannerLockTimeout(int millis) { + if ( Timeouts.isRealTimeout( millis ) ) { + throw new UnsupportedOperationException( "Spanner does not support lock timeout." ); + } + if ( millis == Timeouts.SKIP_LOCKED_MILLI ) { + throw new UnsupportedOperationException( "Spanner does not support skip locked." ); + } + if ( millis == Timeouts.NO_WAIT_MILLI ) { + throw new UnsupportedOperationException( "Spanner does not support no wait." ); + } + } + + @Override + public void contributeTypes(TypeContributions typeContributions, ServiceRegistry serviceRegistry) { + super.contributeTypes( typeContributions, serviceRegistry ); + + final var configurationService = serviceRegistry.requireService( ConfigurationService.class ); + + this.useIntegerForPrimaryKey = configurationService.getSetting( + USE_INTEGER_FOR_PRIMARY_KEY, + StandardConverters.BOOLEAN, + false + ); + + this.useEmulator = configurationService.getSetting( + USE_EMULATOR, + StandardConverters.BOOLEAN, + false + ); + } + + @Override + public FunctionalDependencyAnalysisSupport getFunctionalDependencyAnalysisSupport() { + return FunctionalDependencyAnalysisSupportImpl.NONE; + } + + @Override + protected void contributePostgreSQLTypes(TypeContributions typeContributions, ServiceRegistry serviceRegistry) { + final var jdbcTypeRegistry = typeContributions.getTypeConfiguration() + .getJdbcTypeRegistry(); + + jdbcTypeRegistry.addDescriptor( SpannerLocalDateTimeJdbcType.INSTANCE ); + jdbcTypeRegistry.addDescriptor( SpannerLocalTimeJdbcType.INSTANCE ); + jdbcTypeRegistry.addDescriptor( SpannerTimeJdbcType.INSTANCE ); + jdbcTypeRegistry.addDescriptor( Types.BLOB, BlobJdbcType.BLOB_BINDING ); + jdbcTypeRegistry.addDescriptor( Types.CLOB, ClobJdbcType.CLOB_BINDING ); + jdbcTypeRegistry.addDescriptor( PostgreSQLUUIDJdbcType.INSTANCE ); + + // Replace the standard array constructor + jdbcTypeRegistry.addTypeConstructor( PostgreSQLArrayJdbcTypeConstructor.INSTANCE ); + + jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingJsonJdbcType.JSONB_INSTANCE ); + jdbcTypeRegistry.addTypeConstructorIfAbsent( PostgreSQLCastingJsonArrayJdbcTypeConstructor.JSONB_INSTANCE ); + } + + @Override + public SqlAstTranslatorFactory getSqlAstTranslatorFactory() { + return new StandardSqlAstTranslatorFactory() { + @Override + protected SqlAstTranslator buildTranslator(SessionFactoryImplementor sessionFactory, Statement statement) { + return new SpannerPostgreSQLSqlAstTranslator( sessionFactory, statement ); + } + }; + } + + @Override + protected void registerPostgreSQLColumnTypes(TypeContributions typeContributions, ServiceRegistry serviceRegistry) { + final var ddlTypeRegistry = typeContributions.getTypeConfiguration().getDdlTypeRegistry(); + + // We need to configure that the array type uses the raw element type for casts + ddlTypeRegistry.addDescriptor( new ArrayDdlTypeImpl( this, true ) ); + ddlTypeRegistry.addDescriptor( new DdlTypeImpl( UUID, "uuid", this ) ); + ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON, "jsonb", this ) ); + + ddlTypeRegistry.addDescriptor( + CapacityDependentDdlType.builder( FLOAT, columnType( FLOAT ), castType( FLOAT ), this ) + .withTypeCapacity( 24, "real" ) + .withTypeCapacity( 53, "double precision" ) + .build() + ); + } + + @Override + public void appendDateTimeLiteral( + SqlAppender appender, + TemporalAccessor temporalAccessor, + @SuppressWarnings("deprecation") + TemporalType precision, + TimeZone jdbcTimeZone) { + if ( precision == TemporalType.TIME || (precision == TemporalType.TIMESTAMP && !temporalAccessor.isSupported( ChronoField.OFFSET_SECONDS ))) { + precision = TemporalType.TIMESTAMP; + if ( temporalAccessor instanceof LocalTime localTime) { + temporalAccessor = localTime.atDate( LocalDate.of( 1970, 1, 1 ) ) + .atOffset( ZoneOffset.UTC ); + } + else if ( temporalAccessor instanceof OffsetTime offsetTime ) { + temporalAccessor = offsetTime.atDate( LocalDate.of( 1970, 1, 1 ) ); + } + else if ( temporalAccessor instanceof LocalDateTime localDateTime) { + temporalAccessor = localDateTime.atOffset( ZoneOffset.UTC ); + } + else if ( temporalAccessor instanceof Instant instant) { + temporalAccessor = instant.atOffset( ZoneOffset.UTC ); + } + else { + throw new UnsupportedOperationException( "Unsupported temporal type: " + temporalAccessor.getClass().getName() ); + } + } + + super.appendDateTimeLiteral( appender, temporalAccessor, precision, jdbcTimeZone ); + } + + @Override + public void appendDateTimeLiteral( + SqlAppender appender, + Date date, + @SuppressWarnings("deprecation") + TemporalType precision, + TimeZone jdbcTimeZone) { + if ( precision == TemporalType.TIME ) { + precision = TemporalType.TIMESTAMP; + } + super.appendDateTimeLiteral( appender, date, precision, jdbcTimeZone ); + } + + public void appendDateTimeLiteral( + SqlAppender appender, + Calendar calendar, + @SuppressWarnings("deprecation") + TemporalType precision, + TimeZone jdbcTimeZone) { + if ( precision == TemporalType.TIME ) { + precision = TemporalType.TIMESTAMP; + } + + super.appendDateTimeLiteral( appender, calendar, precision, jdbcTimeZone ); + } + + @Override + protected String castType(int sqlTypeCode) { + return switch (sqlTypeCode) { + case TIME, TIME_UTC, TIMESTAMP, TIMESTAMP_UTC -> columnType(TIMESTAMP_WITH_TIMEZONE); + default -> super.castType(sqlTypeCode); + }; + } + + @Override + public String timestampaddPattern(TemporalUnit unit, TemporalType temporalType, IntervalType intervalType) { + final String temporal = temporalType == TemporalType.DATE ? "cast(?3 as " + castType(TIMESTAMP) + ")" : "?3"; + return intervalType != null + ? "(?2+" + temporal + ")" + : "cast(" + temporal + "+" + intervalPattern(unit) + " as " + castTemporalType(temporalType) + ")"; + } + + @Override + public String timestampdiffPattern(TemporalUnit unit, TemporalType fromTemporalType, TemporalType toTemporalType) { + final String pattern = switch (unit) { + case YEAR -> "extract(year from ?3)-extract(year from ?2)"; + // For month, we also need to account for years + case MONTH -> "(extract(year from ?3)-extract(year from ?2))*12+(extract(month from ?3)-extract(month from ?2))"; + // Quarter is month diff / 3 + case QUARTER -> + "((extract(year from ?3)-extract(year from ?2))*12+(extract(month from ?3)-extract(month from ?2)))/3"; + case WEEK -> "(extract(epoch from ?3)-extract(epoch from ?2))/604800"; + case DAY -> "(extract(epoch from ?3)-extract(epoch from ?2))/86400"; + case HOUR -> "(extract(epoch from ?3)-extract(epoch from ?2))/3600"; + case MINUTE -> "(extract(epoch from ?3)-extract(epoch from ?2))/60"; + case SECOND -> "extract(epoch from ?3)-extract(epoch from ?2)"; + case NANOSECOND -> "(extract(epoch from ?3)-extract(epoch from ?2))*1e9"; + case NATIVE -> "extract(epoch from ?3)-extract(epoch from ?2)"; + default -> "extract(epoch from ?3)-extract(epoch from ?2)"; + }; + + return "cast(" + pattern + " as bigint)"; + } + + @Override + public String castPattern(CastType from, CastType to) { + if (from == CastType.STRING && to == CastType.TIME) { + return "cast('1970-01-01 ' || ?1 as timestamp with time zone)"; + } + if (from == CastType.TIME && to == CastType.STRING) { + return "to_char(?1, 'HH24:MI:SS.MS')"; + } + return super.castPattern(from, to); + } + + private static String intervalPattern(TemporalUnit unit) { + return switch (unit) { + case NANOSECOND -> "cast(concat(cast((?2)/1e3 as text), ' microsecond') as interval)"; + case NATIVE -> "cast(concat(cast((?2) as text), ' second') as interval)"; + case QUARTER -> "cast(concat(cast((?2)*3 as text), ' month') as interval)"; + case WEEK -> "cast(concat(cast((?2) as text), ' week') as interval)"; + default -> "cast(concat(cast((?2) as text), ' " + unit + "') as interval)"; + }; + } + + private String castTemporalType(TemporalType temporalType) { + return switch (temporalType) { + case TIME, TIMESTAMP -> castType( TIMESTAMP ); + default -> temporalType.name().toLowerCase(); + }; + } + + @Override + protected String columnType(int sqlTypeCode) { + return switch (sqlTypeCode) { + // Spanner doesn't support precision with the timestamp + case TIME, TIME_UTC, TIMESTAMP, TIMESTAMP_UTC, TIMESTAMP_WITH_TIMEZONE -> "timestamp with time zone"; + case BLOB -> "bytea"; + case CLOB, NCLOB -> "character varying"; + // Spanner doesn't support NUMERIC with precision and scale + case NUMERIC -> "numeric"; + case DECIMAL -> "decimal"; + // Spanner doesn't support CHAR so we should use VARCHAR + case CHAR -> columnType( VARCHAR ); + case SMALLINT, INTEGER, TINYINT -> columnType( BIGINT ); + default -> super.columnType(sqlTypeCode); + }; + } + + @Override + public ScrollMode defaultScrollMode() { + return ScrollMode.FORWARD_ONLY; + } + + @Override + public boolean supportsTupleCounts() { + return false; + } + + @Override + public boolean supportsUserDefinedTypes() { + return false; + } + + @Override + public boolean supportsFilterClause() { + return false; + } + + @Override + public boolean supportsRecursiveCycleUsingClause() { + return false; + } + + @Override + public boolean supportsRecursiveSearchClause() { + return false; + } + + @Override + public boolean supportsUniqueConstraints() { + return false; + } + + @Override + public boolean supportsRowValueConstructorGtLtSyntax() { + return false; + } + + @Override + public boolean supportsRowValueConstructorSyntax() { + return false; + } + + // ALL subqueries with operators other than <>/!= are not supported + @Override + public boolean supportsRowValueConstructorSyntaxInQuantifiedPredicates() { + return false; + } + + @Override + public boolean supportsValuesList() { + return false; + } + + @Override + public boolean supportsRowValueConstructorSyntaxInInSubQuery() { + return false; + } + + @Override + public boolean supportsCaseInsensitiveLike() { + return false; + } + + @Override + public String currentTimestamp() { + return currentTimestampWithTimeZone(); + } + + @Override + public String currentTime() { + return currentTimestampWithTimeZone(); + } + + @Override + public String currentLocalTimestamp() { + return currentTimestampWithTimeZone(); + } + + @Override + public String currentLocalTime() { + return currentTimestampWithTimeZone(); + } + + @Override + public boolean supportsLateral() { + return false; + } + + @Override + public boolean supportsFromClauseInUpdate() { + return false; + } + + @Override + public int getMaxVarcharLength() { + return 2_621_440; + } + + @Override + public int getMaxVarbinaryLength() { + //max is equivalent 10 MiB + return 10_485_760; + } + + @Override + public String getCurrentSchemaCommand() { + return ""; + } + + @Override + public boolean supportsCommentOn() { + return false; + } + + @Override + public boolean supportsWindowFunctions() { + return false; + } + + @Override + public int getInExpressionCountLimit() { + return 100; + } + + @Override + public String getAddForeignKeyConstraintString( + String constraintName, + String[] foreignKey, + String referencedTable, + String[] primaryKey, + boolean referencesPrimaryKey) { + // Cloud Spanner requires the referenced columns to specified in all cases, including + // if the foreign key is referencing the primary key of the referenced table. Setting referencesPrimaryKey to + // false will add all the referenced columns. + return super.getAddForeignKeyConstraintString( constraintName, foreignKey, referencedTable, primaryKey, false ); + } + + @Override + public boolean canBatchTruncate() { + return false; + } + + @Override + public String rowId(String rowId) { + return null; + } + + @Override + public boolean supportsRowConstructor() { + return false; + } + + @Override + public String getTruncateTableStatement(String tableName) { + return "delete from " + tableName; + } + + @Override + public String getBeforeDropStatement() { + return null; + } + + @Override + public String getCascadeConstraintsString() { + return ""; + } + + @Override + public boolean supportsIfExistsBeforeConstraintName() { + return false; + } + + @Override + public boolean supportsIfExistsAfterAlterTable() { + return false; + } + + @Override + public boolean supportsDistinctFromPredicate() { + return false; + } + + @Override + public boolean supportsPartitionBy() { + return false; + } + + @Override + public boolean addPartitionKeyToPrimaryKey() { + return false; + } + + @Override + public String getDual() { + return "unnest(ARRAY[1])"; + } + + @Override + public String getFromDualForSelectOnly() { + return " from " + getDual() + " dual"; + } + + public Replacer datetimeFormat(String format) { + return org.hibernate.dialect.OracleDialect.datetimeFormat(format, true, false) + .replace("SSSSSS", "US") + .replace("SSSSS", "US") + .replace("SSSS", "US") + .replace("SSS", "MS") + .replace("SS", "MS") + .replace("S", "MS") + // use ISO day in week, as per DateTimeFormatter + .replace("ee", "ID") + .replace("e", "fmID") + // TZR is TZ in Postgres + .replace("zzz", "TZ") + .replace("zz", "TZ") + .replace("z", "TZ") + .replace("ZZZ", "OF") + .replace("ZZ", "OF") + .replace("Z", "OF") + .replace("xxx", "OF") + .replace("xx", "OF") + .replace("x", "OF") + .replace("a", "AM") + // Spanner-specific overrides + .replace("hh", "HH12") + .replace("h", "HH12"); + } + + @Override + public boolean supportsRecursiveCTE() { + return false; + } + + public boolean supportsWithClauseInSubquery() { + return false; + } + + public boolean supportsNestedWithClause() { + return false; + } + + public boolean supportsCteHeaderColumnList() { + return false; + } + + @Override + public boolean supportsTupleDistinctCounts() { + return false; + } + + @Override + public InformationExtractor getInformationExtractor(ExtractionContext extractionContext) { + return new InformationExtractorJdbcDatabaseMetaDataImpl( extractionContext ); + } + + @Override + public SqmMultiTableMutationStrategy getFallbackSqmMutationStrategy( + EntityMappingType rootEntityDescriptor, + RuntimeModelCreationContext runtimeModelCreationContext) { + return new PersistentTableMutationStrategy( rootEntityDescriptor, runtimeModelCreationContext ); + } + + @Override + public SqmMultiTableInsertStrategy getFallbackSqmInsertStrategy( + EntityMappingType rootEntityDescriptor, + RuntimeModelCreationContext runtimeModelCreationContext) { + return new PersistentTableInsertStrategy( rootEntityDescriptor, runtimeModelCreationContext ); + } + + @Override + public TemporaryTableStrategy getLocalTemporaryTableStrategy() { + return null; + } + + @Override + public @Nullable TemporaryTableStrategy getGlobalTemporaryTableStrategy() { + return null; + } + + @Override + public TemporaryTableStrategy getPersistentTemporaryTableStrategy() { + return new PersistentTemporaryTableStrategy( this ); + } + + @Override + public SQLExceptionConversionDelegate buildSQLExceptionConversionDelegate() { + return this::handleConstraintViolatedException; + } + + private @Nullable JDBCException handleConstraintViolatedException(SQLException sqlException, String message, String sql) { + if (sqlException.getErrorCode() == 6) { + return new ConstraintViolationException( message, sqlException, ConstraintViolationException.ConstraintKind.UNIQUE, null ); + } + else if (sqlException.getErrorCode() == 5 || matches( TABLE_DOES_NOT_EXIST_PATTERN, message )) { + return new SQLGrammarException( message, sqlException ); + } + else if (matches( NOT_NULL_CONSTRAINT_PATTERN, message )) { + return new ConstraintViolationException( message, sqlException, ConstraintViolationException.ConstraintKind.NOT_NULL, null ); + } + else if (matches( CHECK_CONSTRAINT_PATTERN, message )) { + return new ConstraintViolationException( message, sqlException, ConstraintViolationException.ConstraintKind.CHECK, null ); + } + else if(matches( FOREIGN_KEY_CONSTRAINT_PATTERN, message )) { + return new ConstraintViolationException( message, sqlException, ConstraintViolationException.ConstraintKind.FOREIGN_KEY, null ); + } + else { + return null; + } + } + + private boolean matches(Pattern pattern, String message) { + return pattern.matcher( message ).matches(); + } + + @Override + public CallableStatementSupport getCallableStatementSupport() { + return StandardCallableStatementSupport.NO_REF_CURSOR_INSTANCE; + } + + public boolean useIntegerForPrimaryKey() { + return useIntegerForPrimaryKey; + } + + @Override + public MutationOperation createOptionalTableUpdateOperation( + EntityMutationTarget mutationTarget, + OptionalTableUpdate optionalTableUpdate, + SessionFactoryImplementor factory) { + // Spanner returns 0 affected rows for ON CONFLICT DO NOTHING upsert operations + // when the row already exists, while Hibernate's default expectation expects 1 + // affected row. + // However, we only want to apply this for truly optional tables. + // For non-optional tables, we want to keep the default expectation + // (expecting 1 row) so it correctly throws StaleStateException on conflict + // after a failed update (e.g. due to version mismatch). + // However, for unversioned entities, a no-op on conflict is fine even for + // non-optional tables, so we allow 0 rows affected for them as well. + boolean isOptional = optionalTableUpdate.getMutatingTable().getTableMapping().isOptional(); + boolean isVersioned = mutationTarget.getTargetPart().getVersionMapping() != null; + if (isOptional || !isVersioned) { + return new OptionalTableUpdateWithUpsertOperation( + mutationTarget, + new OptionalTableUpdateWithOptionalRowCount(optionalTableUpdate), + factory); + } + else { + return super.createOptionalTableUpdateOperation( mutationTarget, optionalTableUpdate, factory ); + } + } +} diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SpannerPostgreSQLTableExporter.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SpannerPostgreSQLTableExporter.java new file mode 100644 index 000000000000..b009f82832cf --- /dev/null +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SpannerPostgreSQLTableExporter.java @@ -0,0 +1,101 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.community.dialect; + +import org.hibernate.boot.Metadata; +import org.hibernate.boot.model.relational.SqlStringGenerationContext; +import org.hibernate.dialect.Dialect; +import org.hibernate.mapping.Column; +import org.hibernate.mapping.RootClass; +import org.hibernate.tool.schema.internal.ColumnValue; +import org.hibernate.mapping.Index; +import org.hibernate.mapping.PrimaryKey; +import org.hibernate.mapping.Table; +import org.hibernate.mapping.UniqueKey; +import org.hibernate.tool.schema.internal.StandardTableExporter; + +import java.sql.Types; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +public class SpannerPostgreSQLTableExporter extends StandardTableExporter { + + public SpannerPostgreSQLTableExporter(Dialect dialect) { + super( dialect ); + } + + @Override + public String[] getSqlCreateStrings(Table table, Metadata metadata, SqlStringGenerationContext context) { + // Spanner mandates that primary key should be present in all the tables. For element collection tables or + // sequence tables, there will be no primary key. In order to fix the problem, we randomly generate + // the ID column with BIT_REVERSED_POSITIVE sequence + if ( !table.hasPrimaryKey() && (isElementCollectionTable(table, metadata) || isHistoryOrAuditedTable(table, metadata) || isSequenceTable(table, context))) { + Column column = getAutoGeneratedPrimaryKeyColumn( table, metadata ); + table.addColumn( column ); + + PrimaryKey primaryKey = new PrimaryKey( table ); + primaryKey.addColumn( column ); + + table.setPrimaryKey( primaryKey ); + } + + return super.getSqlCreateStrings( table, metadata, context ); + } + + private boolean isSequenceTable(Table table, SqlStringGenerationContext context) { + return table.getColumnSpan() == 1 && table.getInitCommands( context ).size() == 1; + } + + private boolean isHistoryOrAuditedTable(Table targetTable, Metadata metadata) { + return metadata.getCollectionBindings().stream() + .anyMatch( collection -> collection.getAuxiliaryTable() == targetTable ) + || metadata.getEntityBindings().stream() + .filter( entity -> entity instanceof RootClass ) + .anyMatch( entity -> ((RootClass) entity).getAuxiliaryTable() == targetTable ); + } + + public boolean isElementCollectionTable(Table targetTable, Metadata metadata) { + return metadata.getCollectionBindings().stream() + .filter( collection -> collection.getCollectionTable() == targetTable ) + .anyMatch( collection -> !collection.isOneToMany() ); + } + + @Override + public String[] getSqlDropStrings(Table table, Metadata metadata, SqlStringGenerationContext context) { + // Spanner requires the indexes to be dropped before dropping the table + List sqlDropIndexStrings = new ArrayList<>(); + for ( Index index : table.getIndexes().values() ) { + sqlDropIndexStrings.add( sqlDropIndexString(index.getName()) ); + } + // Spanner requires all the unique indexes to be dropped before dropping the tables + for ( UniqueKey uniqueKey : table.getUniqueKeys().values() ) { + sqlDropIndexStrings.add( sqlDropIndexString(uniqueKey.getName()) ); + } + for ( Column column : table.getColumns() ) { + if ( column.isUnique() ) { + sqlDropIndexStrings.add( sqlDropIndexString(column.getUniqueKeyName()) ); + } + } + String[] sqlDropStrings = super.getSqlDropStrings( table, metadata, context ); + return Stream.concat( sqlDropIndexStrings.stream(), Stream.of( sqlDropStrings ) ) + .toArray( String[]::new ); + } + + private String sqlDropIndexString(String indexName) { + return "drop index if exists " + indexName; + } + + private Column getAutoGeneratedPrimaryKeyColumn(Table table, Metadata metadata) { + Column column = new Column( "rowid" ); + column.setSqlTypeCode( Types.BIGINT ); + column.setNullable( false ); + column.setSqlType( "bigint" ); + column.setOptions( "hidden" ); + column.setIdentity( true ); + column.setValue( new ColumnValue( metadata.getDatabase(), table, column, metadata.getDatabase().getTypeConfiguration().getBasicTypeForJavaType( Long.class ) ) ); + return column; + } +} diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseLegacyDialect.java index d025598c4c15..b47549c67373 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseLegacyDialect.java @@ -12,6 +12,14 @@ import org.hibernate.dialect.DmlTargetColumnQualifierSupport; import org.hibernate.dialect.NationalizationSupport; import org.hibernate.dialect.SybaseDriverKind; +import org.hibernate.dialect.SybaseJtdsJsonAsStringArrayJdbcTypeConstructor; +import org.hibernate.dialect.SybaseJtdsJsonAsStringJdbcType; +import org.hibernate.dialect.SybaseJtdsLongNVarcharJdbcType; +import org.hibernate.dialect.SybaseJtdsNCharJdbcType; +import org.hibernate.dialect.SybaseJtdsNClobJdbcType; +import org.hibernate.dialect.SybaseJtdsNVarcharJdbcType; +import org.hibernate.dialect.SybaseJtdsXmlAsStringArrayJdbcTypeConstructor; +import org.hibernate.dialect.SybaseJtdsXmlAsStringJdbcType; import org.hibernate.dialect.function.CommonFunctionFactory; import org.hibernate.dialect.function.CountFunction; import org.hibernate.dialect.function.IntegralTimestampaddFunction; @@ -57,6 +65,7 @@ import org.hibernate.type.descriptor.jdbc.BlobJdbcType; import org.hibernate.type.descriptor.jdbc.ClobJdbcType; import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.descriptor.jdbc.NClobJdbcType; import org.hibernate.type.descriptor.jdbc.ObjectNullAsBinaryTypeJdbcType; import org.hibernate.type.descriptor.jdbc.TinyIntAsSmallIntJdbcType; import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; @@ -237,21 +246,32 @@ public void contributeTypes(TypeContributions typeContributions, ServiceRegistry final JdbcTypeRegistry jdbcTypeRegistry = typeContributions.getTypeConfiguration() .getJdbcTypeRegistry(); if ( driverKind == SybaseDriverKind.JTDS ) { - jdbcTypeRegistry.addDescriptor( Types.TINYINT, TinyIntAsSmallIntJdbcType.INSTANCE ); + jdbcTypeRegistry.addDescriptor( TinyIntAsSmallIntJdbcType.INSTANCE ); // The jTDS driver doesn't support the JDBC4 signatures using 'long length' for stream bindings - jdbcTypeRegistry.addDescriptor( Types.CLOB, ClobJdbcType.CLOB_BINDING ); - - // The jTDS driver doesn't support nationalized types - jdbcTypeRegistry.addDescriptor( Types.NCLOB, ClobJdbcType.CLOB_BINDING ); - jdbcTypeRegistry.addDescriptor( Types.NVARCHAR, ClobJdbcType.CLOB_BINDING ); + jdbcTypeRegistry.addDescriptor( ClobJdbcType.CLOB_BINDING ); + + // Need to register specialized JdbcType instances for jTDS because it throws an AbstractMethodError + // when invoking nationalized methods and requires binding through UTF-16LE bytes + jdbcTypeRegistry.addDescriptor( SybaseJtdsNClobJdbcType.JTDS_INSTANCE ); + jdbcTypeRegistry.addDescriptor( SybaseJtdsNCharJdbcType.JTDS_INSTANCE ); + jdbcTypeRegistry.addDescriptor( SybaseJtdsNVarcharJdbcType.JTDS_INSTANCE ); + jdbcTypeRegistry.addDescriptor( SybaseJtdsLongNVarcharJdbcType.JTDS_INSTANCE ); + jdbcTypeRegistry.addDescriptor( SybaseJtdsJsonAsStringJdbcType.JTDS_INSTANCE ); + jdbcTypeRegistry.addDescriptor( SybaseJtdsXmlAsStringJdbcType.JTDS_INSTANCE ); + jdbcTypeRegistry.addTypeConstructor( SybaseJtdsJsonAsStringArrayJdbcTypeConstructor.INSTANCE ); + jdbcTypeRegistry.addTypeConstructor( SybaseJtdsXmlAsStringArrayJdbcTypeConstructor.INSTANCE ); } else { - // Some Sybase drivers cannot support getClob. See HHH-7889 - jdbcTypeRegistry.addDescriptor( Types.CLOB, ClobJdbcType.STREAM_BINDING_EXTRACTING ); + // jConnect driver only conditionally supports getClob/getNClob depending on a server setting. See + // - https://help.sap.com/doc/e3cb6844decf441e85e4670e1cf48c9b/16.0.3.6/en-US/SAP_jConnect_Programmers_Reference_en.pdf + // - https://infocenter.sybase.com/help/index.jsp?topic=/com.sybase.infocenter.dc20155.1570/html/OS_SDK_nf/CIHJFDDH.htm + // - HHH-7889 + jdbcTypeRegistry.addDescriptor( ClobJdbcType.STREAM_BINDING_EXTRACTING ); + jdbcTypeRegistry.addDescriptor( NClobJdbcType.STREAM_BINDING_EXTRACTING ); } - jdbcTypeRegistry.addDescriptor( Types.BLOB, BlobJdbcType.PRIMITIVE_ARRAY_BINDING ); + jdbcTypeRegistry.addDescriptor( BlobJdbcType.PRIMITIVE_ARRAY_BINDING ); // Sybase requires a custom binder for binding untyped nulls with the NULL type typeContributions.contributeJdbcType( ObjectNullAsBinaryTypeJdbcType.INSTANCE ); @@ -262,7 +282,7 @@ public void contributeTypes(TypeContributions typeContributions, ServiceRegistry ObjectNullAsBinaryTypeJdbcType.INSTANCE, typeContributions.getTypeConfiguration() .getJavaTypeRegistry() - .getDescriptor( Object.class ) + .resolveDescriptor( Object.class ) ) ); typeContributions.contributeType( @@ -270,7 +290,7 @@ public void contributeTypes(TypeContributions typeContributions, ServiceRegistry ObjectNullAsBinaryTypeJdbcType.INSTANCE, typeContributions.getTypeConfiguration() .getJavaTypeRegistry() - .getDescriptor( Object.class ) + .resolveDescriptor( Object.class ) ) ); } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/TeradataDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/TeradataDialect.java index 2e7dfda78dff..72a4f9018851 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/TeradataDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/TeradataDialect.java @@ -644,7 +644,7 @@ public boolean supportsRowValueConstructorSyntax() { } /** - * Teradata uses the syntax DELETE FROM ALL instead of TRUNCATE + * Teradata uses the syntax {@code DELETE FROM ALL instead of TRUNCATE } * @param tableName the name of the table */ public String getTruncateTableStatement(String tableName) { diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/TiDBDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/TiDBDialect.java index c176699b7f11..a1fc17e6d338 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/TiDBDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/TiDBDialect.java @@ -21,6 +21,7 @@ import org.hibernate.dialect.sequence.SequenceSupport; import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo; import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.persister.entity.mutation.EntityMutationTarget; import org.hibernate.query.common.TemporalUnit; import org.hibernate.query.sqm.IntervalType; import org.hibernate.sql.ast.SqlAstTranslator; @@ -28,6 +29,8 @@ import org.hibernate.sql.ast.spi.StandardSqlAstTranslatorFactory; import org.hibernate.sql.ast.tree.Statement; import org.hibernate.sql.exec.spi.JdbcOperation; +import org.hibernate.sql.model.internal.OptionalTableUpdate; +import org.hibernate.sql.model.MutationOperation; import org.hibernate.tool.schema.extract.spi.SequenceInformationExtractor; import static org.hibernate.community.dialect.lock.internal.TiDBLockingSupport.TIDB_LOCKING_SUPPORT; @@ -39,8 +42,12 @@ */ public class TiDBDialect extends MySQLDialect { - private static final DatabaseVersion VERSION57 = DatabaseVersion.make( 5, 7 ); + // 8.0.11 is the first MySQL 8.0 GA release. + // See also: https://docs.pingcap.com/tidb/stable/mysql-compatibility/ + private static final DatabaseVersion VERSION80 = DatabaseVersion.make( 8, 0, 11 ); + // See also: https://www.pingcap.com/tidb-release-support-policy/ + // v5.4 EOL date: 15 Feb 2026 private static final DatabaseVersion MINIMUM_VERSION = DatabaseVersion.make( 5, 4 ); public TiDBDialect() { @@ -58,8 +65,8 @@ public TiDBDialect(DialectResolutionInfo info) { @Override public DatabaseVersion getMySQLVersion() { - // For simplicity’s sake, configure MySQL 5.7 compatibility - return VERSION57; + // For simplicity’s sake, configure MySQL 8.0 compatibility + return VERSION80; } @Override @@ -132,11 +139,6 @@ public LockingSupport getLockingSupport() { return TIDB_LOCKING_SUPPORT; } - @Override - protected boolean supportsForShare() { - return false; - } - @Override protected boolean supportsAliasLocks() { return false; @@ -233,4 +235,19 @@ public String timestampaddPattern(TemporalUnit unit, TemporalType temporalType, public String getDual() { return "dual"; } + + @Override + public MutationOperation createOptionalTableUpdateOperation(EntityMutationTarget mutationTarget, OptionalTableUpdate optionalTableUpdate, SessionFactoryImplementor factory) { + if ( optionalTableUpdate.getNumberOfOptimisticLockBindings() == 0 ) { + final TiDBSqlAstTranslator translator = new TiDBSqlAstTranslator<>( factory, optionalTableUpdate, TiDBDialect.this ); + return translator.createMergeOperation( optionalTableUpdate ); + } + return super.createOptionalTableUpdateOperation( mutationTarget, optionalTableUpdate, factory ); + } + + // https://github.com/pingcap/tidb/issues/66392 + @Override + public boolean supportsExceptAll() { + return false; + } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/TiDBSqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/TiDBSqlAstTranslator.java index 77789b6ae34b..f007613e99fc 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/TiDBSqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/TiDBSqlAstTranslator.java @@ -5,17 +5,16 @@ package org.hibernate.community.dialect; import org.hibernate.dialect.DmlTargetColumnQualifierSupport; -import org.hibernate.dialect.sql.ast.MySQLSqlAstTranslator; +import org.hibernate.dialect.sql.ast.SqlAstTranslatorWithOnDuplicateKeyUpdate; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.internal.util.collections.Stack; import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.sql.ast.Clause; -import org.hibernate.sql.ast.spi.AbstractSqlAstTranslator; +import org.hibernate.sql.ast.spi.FullJoinEmulation; import org.hibernate.sql.ast.tree.MutationStatement; import org.hibernate.sql.ast.tree.Statement; import org.hibernate.sql.ast.tree.delete.DeleteStatement; import org.hibernate.sql.ast.tree.expression.BinaryArithmeticExpression; -import org.hibernate.sql.ast.tree.expression.CastTarget; import org.hibernate.sql.ast.tree.expression.ColumnReference; import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.Literal; @@ -36,7 +35,9 @@ import org.hibernate.sql.exec.internal.JdbcOperationQueryInsertImpl; import org.hibernate.sql.exec.spi.JdbcOperation; import org.hibernate.sql.exec.spi.JdbcOperationQueryInsert; +import org.hibernate.sql.model.ast.ColumnValueBinding; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.List; @@ -46,13 +47,19 @@ * @author Christian Beikov * @author Cong Wang */ -public class TiDBSqlAstTranslator extends AbstractSqlAstTranslator { +public class TiDBSqlAstTranslator extends SqlAstTranslatorWithOnDuplicateKeyUpdate { private final TiDBDialect dialect; + private final ArrayDeque fullJoinEmulations = new ArrayDeque<>(); public TiDBSqlAstTranslator(SessionFactoryImplementor sessionFactory, Statement statement, TiDBDialect dialect) { super( sessionFactory, statement ); this.dialect = dialect; + this.fullJoinEmulations.push( new FullJoinEmulation( this ) ); + } + + private FullJoinEmulation currentFullJoinEmulationHelper() { + return fullJoinEmulations.getFirst(); } @Override @@ -228,11 +235,29 @@ public void visitQueryGroup(QueryGroup queryGroup) { @Override public void visitQuerySpec(QuerySpec querySpec) { - if ( shouldEmulateFetchClause( querySpec ) ) { - emulateFetchOffsetWithWindowFunctions( querySpec, true ); + final var helper = currentFullJoinEmulationHelper(); + final boolean needsNestedHelper = + helper.hasActiveFullJoinEmulation() + && !helper.isFullJoinEmulationQueryPart( querySpec ); + if ( needsNestedHelper ) { + fullJoinEmulations.push( new FullJoinEmulation( this ) ); } - else { - super.visitQuerySpec( querySpec ); + try { + final var currentHelper = currentFullJoinEmulationHelper(); + if ( !currentHelper.renderFullJoinEmulationBranchIfNeeded( querySpec, super::visitQuerySpec ) + && !currentHelper.emulateFullJoinWithUnionIfNeeded( querySpec ) ) { + if ( shouldEmulateFetchClause( querySpec ) ) { + emulateFetchOffsetWithWindowFunctions( querySpec, true ); + } + else { + super.visitQuerySpec( querySpec ); + } + } + } + finally { + if ( needsNestedHelper ) { + fullJoinEmulations.pop(); + } } } @@ -321,17 +346,6 @@ public TiDBDialect getDialect() { return dialect; } - @Override - public void visitCastTarget(CastTarget castTarget) { - String sqlType = MySQLSqlAstTranslator.getSqlType( castTarget, getSessionFactory() ); - if ( sqlType != null ) { - appendSql( sqlType ); - } - else { - super.visitCastTarget( castTarget ); - } - } - @Override protected void renderStringContainsExactlyPredicate(Expression haystack, Expression needle) { // TiDB can't cope with NUL characters in the position function, so we use a like predicate instead @@ -340,4 +354,15 @@ protected void renderStringContainsExactlyPredicate(Expression haystack, Express needle.accept( this ); appendSql( ",'~','~~'),'?','~?'),'%','~%'),'%') escape '~'" ); } + + @Override + protected void renderNewRowAlias() { + } + + @Override + protected void renderUpdateValue(ColumnValueBinding columnValueBinding) { + appendSql( "values(" ); + appendSql( columnValueBinding.getColumnReference().getColumnExpression() ); + appendSql( ")" ); + } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/aggregate/SpannerPostgreSQLAggregateSupport.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/aggregate/SpannerPostgreSQLAggregateSupport.java new file mode 100644 index 000000000000..0e7332e6d214 --- /dev/null +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/aggregate/SpannerPostgreSQLAggregateSupport.java @@ -0,0 +1,63 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.community.dialect.aggregate; + +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +import org.hibernate.dialect.aggregate.AggregateSupport; +import org.hibernate.dialect.aggregate.PostgreSQLAggregateSupport; +import org.hibernate.mapping.Column; +import org.hibernate.metamodel.mapping.SelectableMapping; +import org.hibernate.metamodel.mapping.SqlTypedMapping; +import org.hibernate.type.spi.TypeConfiguration; + +import static org.hibernate.type.SqlTypes.JSON; +import static org.hibernate.type.SqlTypes.JSON_ARRAY; + +/*** + * Spanner supports only JSON aggregation. It doesn't support XML or STRUCT aggregation + */ +public class SpannerPostgreSQLAggregateSupport extends PostgreSQLAggregateSupport { + + public static final AggregateSupport INSTANCE = new SpannerPostgreSQLAggregateSupport(); + + @Override + public String aggregateComponentCustomReadExpression(String template, String placeholder, String aggregateParentReadExpression, String columnExpression, int aggregateColumnTypeCode, SqlTypedMapping column, TypeConfiguration typeConfiguration) { + return switch ( aggregateColumnTypeCode ) { + case JSON_ARRAY, JSON -> + super.aggregateComponentCustomReadExpression( template, placeholder, aggregateParentReadExpression, + columnExpression, aggregateColumnTypeCode, column, typeConfiguration ); + default -> + throw new IllegalArgumentException( "Unsupported aggregate SQL type: " + aggregateColumnTypeCode ); + }; + } + + @Override + public String aggregateComponentAssignmentExpression(String aggregateParentAssignmentExpression, String columnExpression, int aggregateColumnTypeCode, Column column) { + return switch ( aggregateColumnTypeCode ) { + case JSON, JSON_ARRAY -> + super.aggregateComponentAssignmentExpression( aggregateParentAssignmentExpression, columnExpression, + aggregateColumnTypeCode, column ); + default -> + throw new IllegalArgumentException( "Unsupported aggregate SQL type: " + aggregateColumnTypeCode ); + }; + } + + @Override + public boolean requiresAggregateCustomWriteExpressionRenderer(int aggregateSqlTypeCode) { + return aggregateSqlTypeCode == JSON; + } + + @Override + public WriteExpressionRenderer aggregateCustomWriteExpressionRenderer(SelectableMapping aggregateColumn, SelectableMapping[] columnsToUpdate, TypeConfiguration typeConfiguration) { + final int aggregateSqlTypeCode = aggregateColumn.getJdbcMapping().getJdbcType().getDefaultSqlTypeCode(); + if ( aggregateSqlTypeCode == JSON ) { + return super.aggregateCustomWriteExpressionRenderer( aggregateColumn, columnsToUpdate, typeConfiguration ); + } + throw new IllegalArgumentException( "Unsupported aggregate SQL type: " + aggregateSqlTypeCode ); + } +} diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/function/GaussDBTruncRoundFunction.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/function/GaussDBTruncRoundFunction.java index 458480ccec2b..1838455737b1 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/function/GaussDBTruncRoundFunction.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/function/GaussDBTruncRoundFunction.java @@ -36,8 +36,7 @@ * This custom function falls back to using {@code floor} as a workaround only when necessary, * e.g. when there are 2 arguments to the function and either: *
    - *
  • The first argument is not of type {@code numeric}
  • - * or + *
  • The first argument is not of type {@code numeric}, or
  • *
  • The dialect doesn't support the two-argument {@code trunc} function
  • *
* @@ -80,23 +79,26 @@ public void render( } else { // workaround using floor + final SqlAstNode secondArg = arguments.get( 1 ); if ( getName().equals( "trunc" ) ) { sqlAppender.appendSql( "sign(" ); firstArg.accept( walker ); sqlAppender.appendSql( ")*floor(abs(" ); firstArg.accept( walker ); - sqlAppender.appendSql( ")*1e" ); - arguments.get( 1 ).accept( walker ); + sqlAppender.appendSql( ")*power(10," ); + secondArg.accept( walker ); + sqlAppender.appendSql( "))" ); } else { sqlAppender.appendSql( "floor(" ); firstArg.accept( walker ); - sqlAppender.appendSql( "*1e" ); - arguments.get( 1 ).accept( walker ); - sqlAppender.appendSql( "+0.5" ); + sqlAppender.appendSql( "*power(10," ); + secondArg.accept( walker ); + sqlAppender.appendSql( ")+0.5)" ); } - sqlAppender.appendSql( ")/1e" ); - arguments.get( 1 ).accept( walker ); + sqlAppender.appendSql( "/power(10," ); + secondArg.accept( walker ); + sqlAppender.appendSql( ")" ); } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/function/InterSystemsIRISLogFunction.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/function/InterSystemsIRISLogFunction.java new file mode 100644 index 000000000000..092c61083a97 --- /dev/null +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/function/InterSystemsIRISLogFunction.java @@ -0,0 +1,53 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.community.dialect.function; + +import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor; +import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators; +import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.type.StandardBasicTypes; +import org.hibernate.type.spi.TypeConfiguration; + +import java.util.List; + +public class InterSystemsIRISLogFunction extends AbstractSqmSelfRenderingFunctionDescriptor { + + public InterSystemsIRISLogFunction(TypeConfiguration typeConfiguration) { + super( + "log", + StandardArgumentsValidators.between( 1, 2 ), + StandardFunctionReturnTypeResolvers.invariant( + typeConfiguration.getBasicTypeRegistry() + .resolve( StandardBasicTypes.DOUBLE ) + ), + null + ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List arguments, + SqlAstTranslator walker) { + + if ( arguments.size() == 1 ) { + // LOG(x) → log(x) + sqlAppender.appendSql( "log(" ); + arguments.get( 0 ).accept( walker ); + sqlAppender.appendSql( ")" ); + } + else if ( arguments.size() == 2 ) { + // LOG(base, value) → (log(value) / log(base)) + sqlAppender.appendSql( "(log(" ); + arguments.get( 1 ).accept( walker ); + sqlAppender.appendSql( ")/log(" ); + arguments.get( 0 ).accept( walker ); + sqlAppender.appendSql( "))" ); + } + } +} diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/identity/InterSystemsIRISIdentityColumnSupport.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/identity/InterSystemsIRISIdentityColumnSupport.java new file mode 100644 index 000000000000..b1264cb0855d --- /dev/null +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/identity/InterSystemsIRISIdentityColumnSupport.java @@ -0,0 +1,42 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.community.dialect.identity; + +import org.hibernate.MappingException; +import org.hibernate.dialect.identity.IdentityColumnSupportImpl; + +public class InterSystemsIRISIdentityColumnSupport extends IdentityColumnSupportImpl { + + @Override + public boolean supportsIdentityColumns() { + return true; + } + + @Override + public boolean hasDataTypeInIdentityColumn() { + return true; + } + + @Override + public String getIdentityColumnString(int type) throws MappingException { + return "IDENTITY"; + } + + @Override + public String getIdentitySelectString(String table, String column, int type) { + return "SELECT LAST_IDENTITY() FROM %TSQL_sys.snf"; + } + + @Override + public String getIdentityInsertString() { + return null; + } + + @Override + public boolean supportsInsertSelectIdentity() { + return false; + } + +} diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/lock/internal/TiDBLockingSupport.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/lock/internal/TiDBLockingSupport.java index 97c93ae7b17e..afcdfd2b7e6e 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/lock/internal/TiDBLockingSupport.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/lock/internal/TiDBLockingSupport.java @@ -10,6 +10,7 @@ import org.hibernate.dialect.lock.spi.LockTimeoutType; import org.hibernate.dialect.lock.spi.LockingSupport; import org.hibernate.dialect.lock.spi.OuterJoinLockingType; +import org.hibernate.dialect.lock.internal.MySQLLockingSupport.ConnectionLockTimeoutStrategyImpl; import static org.hibernate.dialect.lock.internal.MySQLLockingSupport.MYSQL_CONN_LOCK_TIMEOUT_STRATEGY; @@ -17,6 +18,8 @@ * @author Steve Ebersole */ public class TiDBLockingSupport implements LockingSupport, LockingSupport.Metadata { + // Max innodb_lock_wait_timeout in TiDB v8.5.5 is 3600 + public static final ConnectionLockTimeoutStrategy MYSQL_CONN_LOCK_TIMEOUT_STRATEGY = new ConnectionLockTimeoutStrategyImpl(3600); public static final TiDBLockingSupport TIDB_LOCKING_SUPPORT = new TiDBLockingSupport(); @Override diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/pagination/InterSystemsIRISLimitHandler.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/pagination/InterSystemsIRISLimitHandler.java new file mode 100644 index 000000000000..f4817f944a85 --- /dev/null +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/pagination/InterSystemsIRISLimitHandler.java @@ -0,0 +1,88 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.community.dialect.pagination; + +import org.hibernate.dialect.pagination.AbstractLimitHandler; +import org.hibernate.query.spi.Limit; + +import java.util.Locale; + +public class InterSystemsIRISLimitHandler extends AbstractLimitHandler { + public static final InterSystemsIRISLimitHandler INSTANCE = new InterSystemsIRISLimitHandler(true); + + private final boolean variableLimit; + + public InterSystemsIRISLimitHandler(boolean variableLimit) { + this.variableLimit = variableLimit; + } + + @Override + public String processSql(String sql, Limit limit) { + + boolean hasFirstRow = hasFirstRow( limit ); + boolean hasMaxRows = hasMaxRows( limit ); + + if ( !hasFirstRow && !hasMaxRows ) { + return sql; + } + + String lowersql = sql.toLowerCase( Locale.ROOT ); + int selectIndex = lowersql.indexOf( "select" ); + if ( hasFirstRow && hasMaxRows ) { + return new StringBuilder( sql.length() + 27 ) + .append( sql ) + .insert( selectIndex + 6, " %ROWOFFSET ? %ROWLIMIT ? " ) + .toString(); + + } + else if ( hasFirstRow ) { + return new StringBuilder( sql.length() + 15 ) + .append( sql ) + .insert( selectIndex + 6, " %ROWOFFSET ? " ) + .toString(); + } + else { + final int selectDistinctIndex = lowersql.indexOf( "select distinct" ); + final int insertionPoint = selectIndex + ( selectDistinctIndex == selectIndex ? 15 : 6 ); + + return new StringBuilder( sql.length() + 8 ) + .append( sql ) + .insert( insertionPoint, " TOP ? " ) + .toString(); + } + } + + + @Override + public final boolean supportsLimit() { + return true; + } + + @Override + public final boolean supportsOffset() { + return true; + } + + @Override + public boolean supportsLimitOffset() { + return true; + } + + @Override + public final boolean supportsVariableLimit() { + return true; + } + + @Override + public boolean useMaxForLimit() { + return false; + } + + @Override + public boolean bindLimitParametersFirst() { + return true; + } + +} diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/sequence/SpannerPostgreSQLSequenceSupport.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/sequence/SpannerPostgreSQLSequenceSupport.java new file mode 100644 index 000000000000..121665a9b566 --- /dev/null +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/sequence/SpannerPostgreSQLSequenceSupport.java @@ -0,0 +1,53 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.community.dialect.sequence; + +import org.hibernate.MappingException; +import org.hibernate.community.dialect.SpannerPostgreSQLDialect; +import org.hibernate.dialect.sequence.PostgreSQLSequenceSupport; + +public class SpannerPostgreSQLSequenceSupport extends PostgreSQLSequenceSupport { + + private final SpannerPostgreSQLDialect dialect; + + public SpannerPostgreSQLSequenceSupport(SpannerPostgreSQLDialect dialect) { + super(); + this.dialect = dialect; + } + + @Override + public boolean supportsPooledSequences() { + return false; + } + + @Override + public String getCreateSequenceString(String sequenceName, int initialValue, int incrementSize) throws MappingException { + if ( incrementSize == 0 ) { + throw new MappingException( "Unable to create the sequence [" + sequenceName + "]: the increment size must not be 0" ); + } + return getCreateSequenceString( sequenceName ) + + startingValue( initialValue, incrementSize ) + + " start counter with " + initialValue; + } + + @Override + public String getRestartSequenceString(String sequenceName, long startWith) { + return "alter sequence " + sequenceName + " restart counter with " + startWith; + } + + @Override + public String getSelectSequenceNextValString(String sequenceName) { + var nextValString = super.getSelectSequenceNextValString( sequenceName ); + if (dialect.useIntegerForPrimaryKey()) { + nextValString = "spanner.bit_reverse(" + nextValString + ", true)"; + } + return nextValString; + } + + @Override + public String getSelectSequencePreviousValString(String sequenceName) throws MappingException { + throw new UnsupportedOperationException( "No support for retrieving previous value" ); + } +} diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/sql/ast/SpannerPostgreSQLSqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/sql/ast/SpannerPostgreSQLSqlAstTranslator.java new file mode 100644 index 000000000000..adb3146775a2 --- /dev/null +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/sql/ast/SpannerPostgreSQLSqlAstTranslator.java @@ -0,0 +1,178 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.community.dialect.sql.ast; + +import org.hibernate.dialect.sql.ast.PostgreSQLSqlAstTranslator; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.sql.ast.Clause; +import org.hibernate.sql.ast.tree.Statement; +import org.hibernate.sql.ast.tree.cte.CteMaterialization; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.Literal; +import org.hibernate.sql.ast.tree.expression.Summarization; +import org.hibernate.sql.ast.tree.from.NamedTableReference; +import org.hibernate.sql.ast.tree.insert.InsertSelectStatement; +import org.hibernate.sql.ast.tree.predicate.LikePredicate; +import org.hibernate.query.sqm.UnaryArithmeticOperator; +import org.hibernate.sql.ast.tree.expression.UnaryOperation; +import org.hibernate.sql.ast.tree.expression.Every; +import org.hibernate.sql.ast.tree.expression.Any; +import org.hibernate.sql.ast.tree.expression.ModifiedSubQueryExpression; +import org.hibernate.sql.ast.tree.select.SelectStatement; +import org.hibernate.sql.ast.tree.select.QuerySpec; +import org.hibernate.query.sqm.ComparisonOperator; +import org.hibernate.sql.exec.spi.JdbcOperation; + +public class SpannerPostgreSQLSqlAstTranslator extends PostgreSQLSqlAstTranslator { + + public SpannerPostgreSQLSqlAstTranslator( + SessionFactoryImplementor sessionFactory, Statement statement) { + super(sessionFactory, statement); + } + + @Override + protected void renderMaterializationHint(CteMaterialization materialization) { + // NO-OP + } + + @Override + protected void renderDmlTargetTableExpression(NamedTableReference tableReference) { + appendSql(tableReference.getTableExpression()); + registerAffectedTable(tableReference); + // ALWAYS render the alias for the target table since Spanner doesn't support + // FROM in UPDATE + final Clause currentClause = getClauseStack().getCurrent(); + if ( currentClause == Clause.UPDATE || currentClause == Clause.DELETE) { + renderTableReferenceIdentificationVariable(tableReference); + } + } + + @Override + protected void renderPartitionItem(Expression expression) { + if ( expression instanceof Literal ) { + appendSql( "'0' || '0'" ); + } + else if ( expression instanceof Summarization ) { + // This could theoretically be emulated by rendering all grouping variations of the query and + // connect them via union all but that's probably pretty inefficient and would have to happen + // on the query spec level + throw new UnsupportedOperationException( "Summarization is not supported by DBMS" ); + } + else { + expression.accept( this ); + } + } + + @Override + protected void renderLikePredicate(LikePredicate likePredicate) { + // We need a custom implementation here because Spanner + // uses the backslash character as default escape character + if (likePredicate.getEscapeCharacter() == null) { + renderBackslashEscapedLikePattern( likePredicate.getPattern(), likePredicate.getEscapeCharacter(), true ); + } + else { + renderLikePattern( likePredicate.getPattern(), likePredicate.getEscapeCharacter() ); + } + } + + @Override + protected void renderLikePattern(Expression pattern, Expression escapeCharacter) { + if (escapeCharacter == null) { + super.renderLikePattern( pattern, escapeCharacter ); + } + else { + appendSql( "replace(replace(replace(" ); + pattern.accept( this ); + appendSql( ", " ); + escapeCharacter.accept( this ); + appendSql( "||" ); + escapeCharacter.accept( this ); + appendSql( ", '\\\\'), " ); + escapeCharacter.accept( this ); + appendSql( "||'%', '\\%'), " ); + escapeCharacter.accept( this ); + appendSql( "||'_', '\\_')" ); + } + } + + @Override + protected void renderEscapeCharacter(Expression escapeCharacter) { + // Spanner doesn't support passing escape character other than "\" + } + + @Override + protected void renderSelectExpression(Expression expression) { + if (getStatement() instanceof InsertSelectStatement + && expression instanceof Literal) { + renderCasted(expression); + } + else { + super.renderSelectExpression(expression); + } + } + + @Override + protected void renderComparison(Expression lhs, ComparisonOperator operator, Expression rhs) { + if ( operator == ComparisonOperator.DISTINCT_FROM || operator == ComparisonOperator.NOT_DISTINCT_FROM ) { + renderComparisonEmulateCase( lhs, operator, rhs ); + return; + } + if ( rhs instanceof Every every ) { + SelectStatement subquery = every.getSubquery(); + if ( subquery.getQueryPart() instanceof QuerySpec querySpec ) { + // Emulate ALL + if ( operator != ComparisonOperator.NOT_EQUAL && operator != ComparisonOperator.NOT_DISTINCT_FROM ) { + lhs.accept( this ); + appendSql( operator.sqlText() ); + renderQuantifiedEmulationSubQuery( querySpec, operator ); + return; + } + } + } + else if ( rhs instanceof ModifiedSubQueryExpression expression ) { + SelectStatement subquery = expression.getSubQuery(); + if ( subquery.getQueryPart() instanceof QuerySpec querySpec ) { + if ( operator != ComparisonOperator.NOT_EQUAL && operator != ComparisonOperator.NOT_DISTINCT_FROM ) { + if ( expression.getModifier() == ModifiedSubQueryExpression.Modifier.ALL ) { + // Emulate ALL + lhs.accept( this ); + appendSql( operator.sqlText() ); + renderQuantifiedEmulationSubQuery( querySpec, operator ); + return; + } + else if ( expression.getModifier() == ModifiedSubQueryExpression.Modifier.ANY || expression.getModifier() == ModifiedSubQueryExpression.Modifier.SOME ) { + // Emulate ANY + lhs.accept( this ); + appendSql( operator.sqlText() ); + renderQuantifiedEmulationSubQuery( querySpec, operator.invert() ); + return; + } + } + } + } + else if ( rhs instanceof Any any ) { + SelectStatement subquery = any.getSubquery(); + if ( subquery.getQueryPart() instanceof QuerySpec querySpec ) { + // Emulate ANY + if ( operator != ComparisonOperator.NOT_EQUAL && operator != ComparisonOperator.NOT_DISTINCT_FROM ) { + lhs.accept( this ); + appendSql( operator.sqlText() ); + renderQuantifiedEmulationSubQuery( querySpec, operator.invert() ); + return; + } + } + } + super.renderComparison(lhs, operator, rhs); + } + + @Override + public void visitUnaryOperationExpression(UnaryOperation unaryOperationExpression) { + // Spanner PostgreSQL doesn't support unary plus, so we just render the operand + if ( unaryOperationExpression.getOperator() == UnaryArithmeticOperator.UNARY_MINUS ) { + appendSql( UnaryArithmeticOperator.UNARY_MINUS.getOperatorChar() ); + } + unaryOperationExpression.getOperand().accept( this ); + } +} diff --git a/hibernate-community-dialects/src/test/java/org/hibernate/community/dialect/AltibaseDialectTestCase.java b/hibernate-community-dialects/src/test/java/org/hibernate/community/dialect/AltibaseDialectTestCase.java index 3361af7c0c1f..0f15d183cc47 100644 --- a/hibernate-community-dialects/src/test/java/org/hibernate/community/dialect/AltibaseDialectTestCase.java +++ b/hibernate-community-dialects/src/test/java/org/hibernate/community/dialect/AltibaseDialectTestCase.java @@ -45,7 +45,7 @@ public void testSupportLimits() { @Test public void testSelectWithLimitOnly() { assertThat( withLimit( "select c1, c2 from t1 order by c1, c2 desc", - toRowSelection( 0, 15 ) ).toLowerCase( Locale.ROOT ) ) + toRowSelection( null, 15 ) ).toLowerCase( Locale.ROOT ) ) .isEqualTo( "select c1, c2 from t1 order by c1, c2 desc limit ?" ); } @@ -66,7 +66,7 @@ private String withLimit(String sql, Limit limit) { return dialect.getLimitHandler().processSql( sql, -1, null, new LimitQueryOptions( limit ) ); } - private Limit toRowSelection(int firstRow, int maxRows) { + private Limit toRowSelection(Integer firstRow, Integer maxRows) { Limit selection = new Limit(); selection.setFirstRow( firstRow ); selection.setMaxRows( maxRows ); diff --git a/hibernate-community-dialects/src/test/java/org/hibernate/community/dialect/DerbyDialectTestCase.java b/hibernate-community-dialects/src/test/java/org/hibernate/community/dialect/DerbyDialectTestCase.java index aa8e9ac6e35d..4f564d654511 100644 --- a/hibernate-community-dialects/src/test/java/org/hibernate/community/dialect/DerbyDialectTestCase.java +++ b/hibernate-community-dialects/src/test/java/org/hibernate/community/dialect/DerbyDialectTestCase.java @@ -37,7 +37,7 @@ public void testInsertLimitClause(SessionFactoryScope scope) { final String input = "select * from tablename t where t.cat = 5"; final String expected = "select * from tablename t where t.cat = 5 fetch first ? rows only"; - final String actual = withLimit( input, toRowSelection( 0, limit ) ); + final String actual = withLimit( input, toRowSelection( null, limit ) ); assertThat( actual ).isEqualTo( expected ); } @@ -105,7 +105,7 @@ private String withLimit(String sql, Limit limit) { return new DerbyDialect().getLimitHandler().processSql( sql, -1, null, new LimitQueryOptions( limit ) ); } - private Limit toRowSelection(int firstRow, int maxRows) { + private Limit toRowSelection(Integer firstRow, Integer maxRows) { Limit selection = new Limit(); selection.setFirstRow( firstRow ); selection.setMaxRows( maxRows ); diff --git a/hibernate-community-dialects/src/test/java/org/hibernate/community/dialect/DerbyLegacyDialectTestCase.java b/hibernate-community-dialects/src/test/java/org/hibernate/community/dialect/DerbyLegacyDialectTestCase.java index 8bd7f9ef800e..51b870d0bb8a 100644 --- a/hibernate-community-dialects/src/test/java/org/hibernate/community/dialect/DerbyLegacyDialectTestCase.java +++ b/hibernate-community-dialects/src/test/java/org/hibernate/community/dialect/DerbyLegacyDialectTestCase.java @@ -28,7 +28,7 @@ public void testInsertLimitClause() { final String input = "select * from tablename t where t.cat = 5"; final String expected = "select * from tablename t where t.cat = 5 fetch first " + limit + " rows only"; - final String actual = withLimit( input, toRowSelection( 0, limit ) ); + final String actual = withLimit( input, toRowSelection( null, limit ) ); assertThat( actual ).isEqualTo( expected ); } @@ -88,7 +88,7 @@ private String withLimit(String sql, Limit limit) { .processSql( sql, -1, null, new LimitQueryOptions( limit ) ); } - private Limit toRowSelection(int firstRow, int maxRows) { + private Limit toRowSelection(Integer firstRow, Integer maxRows) { Limit selection = new Limit(); selection.setFirstRow( firstRow ); selection.setMaxRows( maxRows ); diff --git a/hibernate-community-dialects/src/test/java/org/hibernate/community/dialect/FirebirdDialectTest.java b/hibernate-community-dialects/src/test/java/org/hibernate/community/dialect/FirebirdDialectTest.java index 36f9e19d93b0..05cae5a22098 100644 --- a/hibernate-community-dialects/src/test/java/org/hibernate/community/dialect/FirebirdDialectTest.java +++ b/hibernate-community-dialects/src/test/java/org/hibernate/community/dialect/FirebirdDialectTest.java @@ -19,15 +19,15 @@ class FirebirdDialectTest { @ParameterizedTest @CsvSource(useHeadersInDisplayName = true, value = { "major, minor, offset, limit, expectedSQL", - "2, 5, 0, 10, select first ? * from tablename t where t.cat = 5", + "2, 5, , 10, select first ? * from tablename t where t.cat = 5", "2, 5, 10, 0, select skip ? * from tablename t where t.cat = 5", "2, 5, 5, 10, select first ? skip ? * from tablename t where t.cat = 5", - "3, 0, 0, 10, select * from tablename t where t.cat = 5 fetch first ? rows only", + "3, 0, , 10, select * from tablename t where t.cat = 5 fetch first ? rows only", "3, 0, 10, 0, select * from tablename t where t.cat = 5 offset ? rows", "3, 0, 5, 10, select * from tablename t where t.cat = 5 offset ? rows fetch next ? rows only" }) @JiraKey( "HHH-18213" ) - void insertOffsetLimitClause(int major, int minor, int offset, int limit, String expectedSql) { + void insertOffsetLimitClause(int major, int minor, Integer offset, Integer limit, String expectedSql) { String input = "select * from tablename t where t.cat = 5"; FirebirdDialect dialect = new FirebirdDialect( DatabaseVersion.make( major, minor ) ); String actual = dialect.getLimitHandler().processSql( input, -1, null, new LimitQueryOptions( new Limit( offset, limit ) ) ); diff --git a/hibernate-community-dialects/src/test/java/org/hibernate/community/dialect/SQLServer2005DialectTestCase.java b/hibernate-community-dialects/src/test/java/org/hibernate/community/dialect/SQLServer2005DialectTestCase.java index 92ddeb7e65a0..52349f2e01a4 100644 --- a/hibernate-community-dialects/src/test/java/org/hibernate/community/dialect/SQLServer2005DialectTestCase.java +++ b/hibernate-community-dialects/src/test/java/org/hibernate/community/dialect/SQLServer2005DialectTestCase.java @@ -153,7 +153,7 @@ public void testGetLimitStringWithSelectDistinctSubselect() { String expected = "select top(?) col0_.CONTENTID as CONTENT1_12_ " + "where col0_.CONTENTTYPE='PAGE' and (col0_.CONTENTID in " + "(select distinct col2_.PREVVER from CONTENT col2_ where (col2_.PREVVER is not null)))"; - assertThat( withLimit( selectDistinctSubselectSQL, toRowSelection( 0, 5 ) ) ).isEqualTo( expected ); + assertThat( withLimit( selectDistinctSubselectSQL, toRowSelection( null, 5 ) ) ).isEqualTo( expected ); } @Test @@ -227,14 +227,14 @@ public void testGetLimitStringWithMaxOnly() { String expected = "select top(?) product2x0_.id as id0_, product2x0_.description as descript2_0_ " + "from Product2 product2x0_ order by product2x0_.id"; - assertThat( withLimit( query, toRowSelection( 0, 1 ) )).isEqualTo( expected ); + assertThat( withLimit( query, toRowSelection( null, 1 ) )).isEqualTo( expected ); final String distinctQuery = "select distinct product2x0_.id as id0_, product2x0_.description as descript2_0_ " + "from Product2 product2x0_ order by product2x0_.id"; expected = "select distinct top(?) product2x0_.id as id0_, product2x0_.description as descript2_0_ " + "from Product2 product2x0_ order by product2x0_.id"; - assertThat( withLimit( distinctQuery, toRowSelection( 0, 5 ) )).isEqualTo( expected ); + assertThat( withLimit( distinctQuery, toRowSelection( null, 5 ) )).isEqualTo( expected ); } @Test @@ -358,13 +358,13 @@ public void testGetLimitStringWithSelectClauseNestedQueryUsingParenthesisOnlyTop final String query = "select t1.c1 as col_0_0, (select case when count(t2.c1)>0 then 'ADDED' else 'UNMODIFIED' end from table2 t2 WHERE (t2.c1 in (?))) as col_1_0 from table1 t1 WHERE 1=1 ORDER BY t1.c1 ASC"; String expected = "select top(?) t1.c1 as col_0_0, (select case when count(t2.c1)>0 then 'ADDED' else 'UNMODIFIED' end from table2 t2 WHERE (t2.c1 in (?))) as col_1_0 from table1 t1 WHERE 1=1 ORDER BY t1.c1 ASC"; - assertThat( withLimit( query, toRowSelection( 0, 5 ) ) ).isEqualTo( expected ); + assertThat( withLimit( query, toRowSelection( null, 5 ) ) ).isEqualTo( expected ); } @Test @JiraKey(value = "HHH-8916") public void testGetLimitStringUsingCTEQueryNoOffset() { - Limit selection = toRowSelection( 0, 5 ); + Limit selection = toRowSelection( null, 5 ); // test top-based CTE with single CTE query_ definition with no odd formatting final String query1 = "WITH a (c1, c2) AS (SELECT c1, c2 FROM t) SELECT c1, c2 FROM a"; @@ -574,7 +574,7 @@ private String withLimit(String sql, Limit limit) { return dialect.getLimitHandler().processSql( sql, -1, null, new LimitQueryOptions( limit ) ); } - private Limit toRowSelection(int firstRow, int maxRows) { + private Limit toRowSelection(Integer firstRow, Integer maxRows) { Limit selection = new Limit(); selection.setFirstRow( firstRow ); selection.setMaxRows( maxRows ); diff --git a/hibernate-community-dialects/src/test/java/org/hibernate/community/dialect/SQLServer2008DialectTestCase.java b/hibernate-community-dialects/src/test/java/org/hibernate/community/dialect/SQLServer2008DialectTestCase.java index 8ca256220a18..a680f25a26a2 100644 --- a/hibernate-community-dialects/src/test/java/org/hibernate/community/dialect/SQLServer2008DialectTestCase.java +++ b/hibernate-community-dialects/src/test/java/org/hibernate/community/dialect/SQLServer2008DialectTestCase.java @@ -153,7 +153,7 @@ public void testGetLimitStringWithSelectDistinctSubselect() { String expected = "select top(?) col0_.CONTENTID as CONTENT1_12_ " + "where col0_.CONTENTTYPE='PAGE' and (col0_.CONTENTID in " + "(select distinct col2_.PREVVER from CONTENT col2_ where (col2_.PREVVER is not null)))"; - assertThat( withLimit( selectDistinctSubselectSQL, toRowSelection( 0, 5 ) ) ).isEqualTo( expected ); + assertThat( withLimit( selectDistinctSubselectSQL, toRowSelection( null, 5 ) ) ).isEqualTo( expected ); } @Test @@ -227,14 +227,14 @@ public void testGetLimitStringWithMaxOnly() { String expected = "select top(?) product2x0_.id as id0_, product2x0_.description as descript2_0_ " + "from Product2 product2x0_ order by product2x0_.id"; - assertThat( withLimit( query, toRowSelection( 0, 1 ) ) ).isEqualTo( expected ); + assertThat( withLimit( query, toRowSelection( null, 1 ) ) ).isEqualTo( expected ); final String distinctQuery = "select distinct product2x0_.id as id0_, product2x0_.description as descript2_0_ " + "from Product2 product2x0_ order by product2x0_.id"; expected = "select distinct top(?) product2x0_.id as id0_, product2x0_.description as descript2_0_ " + "from Product2 product2x0_ order by product2x0_.id"; - assertThat( withLimit( distinctQuery, toRowSelection( 0, 5 ) ) ).isEqualTo( expected ); + assertThat( withLimit( distinctQuery, toRowSelection( null, 5 ) ) ).isEqualTo( expected ); } @Test @@ -358,13 +358,13 @@ public void testGetLimitStringWithSelectClauseNestedQueryUsingParenthesisOnlyTop final String query = "select t1.c1 as col_0_0, (select case when count(t2.c1)>0 then 'ADDED' else 'UNMODIFIED' end from table2 t2 WHERE (t2.c1 in (?))) as col_1_0 from table1 t1 WHERE 1=1 ORDER BY t1.c1 ASC"; String expected = "select top(?) t1.c1 as col_0_0, (select case when count(t2.c1)>0 then 'ADDED' else 'UNMODIFIED' end from table2 t2 WHERE (t2.c1 in (?))) as col_1_0 from table1 t1 WHERE 1=1 ORDER BY t1.c1 ASC"; - assertThat( withLimit( query, toRowSelection( 0, 5 ) ) ).isEqualTo( expected ); + assertThat( withLimit( query, toRowSelection( null, 5 ) ) ).isEqualTo( expected ); } @Test @JiraKey("HHH-8916") public void testGetLimitStringUsingCTEQueryNoOffset() { - Limit selection = toRowSelection( 0, 5 ); + Limit selection = toRowSelection( null, 5 ); // test top-based CTE with single CTE query_ definition with no odd formatting final String query1 = "WITH a (c1, c2) AS (SELECT c1, c2 FROM t) SELECT c1, c2 FROM a"; @@ -574,7 +574,7 @@ private String withLimit(String sql, Limit limit) { return dialect.getLimitHandler().processSql( sql, -1, null, new LimitQueryOptions( limit ) ); } - private Limit toRowSelection(int firstRow, int maxRows) { + private Limit toRowSelection(Integer firstRow, Integer maxRows) { Limit selection = new Limit(); selection.setFirstRow( firstRow ); selection.setMaxRows( maxRows ); diff --git a/hibernate-community-dialects/src/test/java/org/hibernate/community/dialect/SQLServer2012DialectTestCase.java b/hibernate-community-dialects/src/test/java/org/hibernate/community/dialect/SQLServer2012DialectTestCase.java index 4c7b14d1c166..299b661403f8 100644 --- a/hibernate-community-dialects/src/test/java/org/hibernate/community/dialect/SQLServer2012DialectTestCase.java +++ b/hibernate-community-dialects/src/test/java/org/hibernate/community/dialect/SQLServer2012DialectTestCase.java @@ -41,7 +41,7 @@ public void tearDown() { @JiraKey(value = "HHH-8768") public void testGetLimitStringMaxRowsOnly() { final String input = "select distinct f1 as f53245 from table846752 order by f234, f67 desc"; - assertThat( withLimit( input, toRowSelection( 0, 10 ) ).toLowerCase( Locale.ROOT ) ) + assertThat( withLimit( input, toRowSelection( null, 10 ) ).toLowerCase( Locale.ROOT ) ) .isEqualTo( input + " offset 0 rows fetch first ? rows only" ); } @@ -57,7 +57,7 @@ public void testGetLimitStringWithOffsetAndMaxRows() { @JiraKey(value = "HHH-8768") public void testGetLimitStringMaxRowsOnlyNoOrderBy() { final String input = "select f1 from table"; - assertThat( withLimit( input, toRowSelection( 0, 10 ) ).toLowerCase( Locale.ROOT ) ) + assertThat( withLimit( input, toRowSelection( null, 10 ) ).toLowerCase( Locale.ROOT ) ) .isEqualTo( "select f1 from table order by @@version offset 0 rows fetch first ? rows only" ); } @@ -73,7 +73,7 @@ private String withLimit(String sql, Limit limit) { return dialect.getLimitHandler().processSql( sql, -1, null, new LimitQueryOptions( limit ) ); } - private Limit toRowSelection(int firstRow, int maxRows) { + private Limit toRowSelection(Integer firstRow, Integer maxRows) { final Limit selection = new Limit(); selection.setFirstRow( firstRow ); selection.setMaxRows( maxRows ); diff --git a/hibernate-community-dialects/src/test/java/org/hibernate/community/dialect/functional/SpannerPostgreSQLDisableIntPKTest.java b/hibernate-community-dialects/src/test/java/org/hibernate/community/dialect/functional/SpannerPostgreSQLDisableIntPKTest.java new file mode 100644 index 000000000000..7528317ec342 --- /dev/null +++ b/hibernate-community-dialects/src/test/java/org/hibernate/community/dialect/functional/SpannerPostgreSQLDisableIntPKTest.java @@ -0,0 +1,97 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.community.dialect.functional; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import org.hibernate.community.dialect.SpannerPostgreSQLDialect; +import org.hibernate.dialect.SpannerDialect; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.FailureExpected; +import org.hibernate.testing.orm.junit.RequiresDialect; +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 static org.assertj.core.api.Assertions.assertThat; + + +@RequiresDialect(SpannerPostgreSQLDialect.class) +@RequiresDialect(SpannerDialect.class) +@DomainModel(annotatedClasses = { + SpannerPostgreSQLDisableIntPKTest.IntegerIdEntity.class, + SpannerPostgreSQLDisableIntPKTest.LongIdEntity.class +}) +@SessionFactory +@ServiceRegistry(settings = { + @Setting(name = "hibernate.dialect.spanner.use_integer_for_primary_key", value = "false"), +}) +public class SpannerPostgreSQLDisableIntPKTest { + + @Test + @FailureExpected(reason = "Spanner bit-reversed sequences return very large Long values. " + + "When these are cast to Integer values, the produced Integers are non-unique, " + + "causing a NonUniqueObjectException to be thrown.") + public void testIntegerPooledSequences(SessionFactoryScope scope) { + scope.inTransaction(session -> { + for (int i = 0; i < 55; i++) { + var intEntity = new IntegerIdEntity(); + session.persist(intEntity); + } + }); + } + + @Test + public void testLongPooledSequences(SessionFactoryScope scope) { + scope.inTransaction(session -> { + for (int i = 0; i < 55; i++) { + var longEntity = new LongIdEntity(); + session.persist(longEntity); + } + }); + + scope.inTransaction(session -> { + var entities = session.createQuery( "FROM LongIdEntity order by id", LongIdEntity.class ).getResultList(); + for ( var entity : entities ) { + assertThat(entity.getId()).isBetween( Long.MIN_VALUE, Long.MAX_VALUE ); + } + }); + } + + @Entity(name = "IntegerIdEntity") + public static class IntegerIdEntity { + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "int_seq") + private Integer id; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + } + + @Entity(name = "LongIdEntity") + public static class LongIdEntity { + @Id + @GeneratedValue(strategy = jakarta.persistence.GenerationType.SEQUENCE, generator = "long_seq") + private Long id; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + } +} diff --git a/hibernate-core/hibernate-core.gradle b/hibernate-core/hibernate-core.gradle index a9bd5791df96..44c1fb700bf9 100644 --- a/hibernate-core/hibernate-core.gradle +++ b/hibernate-core/hibernate-core.gradle @@ -27,7 +27,6 @@ dependencies { api jakartaLibs.jta implementation libs.hibernateModels - implementation libs.classmate implementation libs.byteBuddy implementation jakartaLibs.jaxbApi @@ -42,6 +41,8 @@ dependencies { compileOnly jakartaLibs.jsonbApi compileOnly libs.jackson compileOnly libs.jacksonXml + compileOnly libs.jackson3 + compileOnly libs.jackson3Xml compileOnly jdbcLibs.postgresql compileOnly jdbcLibs.edb @@ -79,6 +80,8 @@ dependencies { testImplementation libs.jackson testRuntimeOnly libs.jacksonXml testRuntimeOnly libs.jacksonJsr310 + testImplementation libs.jackson3 + testImplementation libs.jackson3Xml testAnnotationProcessor project( ':hibernate-processor' ) @@ -99,7 +102,7 @@ jar { } ext { - jaxbTargetDir = project.file( "${buildDir}/generated/sources/xjc/main" ) + jaxbTargetDir = project.layout.buildDirectory.dir("generated/sources/xjc/main").get().asFile } sourceSets { @@ -141,11 +144,11 @@ versionInjection { into( 'org.hibernate.Version', 'initVersion' ) } -task copyBundleResourcesXml (type: Copy) { +tasks.register('copyBundleResourcesXml', Copy) { inputs.property( "db", db ) inputs.property( "dbHost", dbHost ) ext { - bundlesTargetDir = file( "${buildDir}/bundles" ) + bundlesTargetDir = layout.buildDirectory.dir("bundles").get().asFile // Escape bundleTokens = [ 'db.dialect' : dbBundle[db]['db.dialect'].replace("&", "&"), @@ -156,7 +159,7 @@ task copyBundleResourcesXml (type: Copy) { 'jdbc.datasource' : dbBundle[db]['jdbc.datasource'].replace("&", "&"), 'connection.init_sql' : dbBundle[db]['connection.init_sql'].replace("&", "&") ] - ext.bundleTokens['buildDirName'] = project.relativePath( buildDir ) + ext.bundleTokens['buildDirName'] = project.relativePath( layout.buildDirectory.asFile.get() ) } from('src/test/bundles/templates') { @@ -170,13 +173,13 @@ task copyBundleResourcesXml (type: Copy) { } } -task copyBundleResourcesNonXml (type: Copy) { +tasks.register('copyBundleResourcesNonXml', Copy) { inputs.property( "db", db ) ext { - bundlesTargetDir = file( "${buildDir}/bundles" ) + bundlesTargetDir = layout.buildDirectory.dir("bundles").get().asFile // Escape bundleTokens = dbBundle[db] - ext.bundleTokens['buildDirName'] = project.relativePath( buildDir ) + ext.bundleTokens['buildDirName'] = project.relativePath(layout.buildDirectory.asFile.get()) } from('src/test/bundles/templates') { @@ -190,7 +193,7 @@ task copyBundleResourcesNonXml (type: Copy) { } } -task copyBundleResources (type: Copy) { +tasks.register('copyBundleResources', Copy) { inputs.property( "db", db ) dependsOn tasks.copyBundleResourcesXml dependsOn tasks.copyBundleResourcesNonXml @@ -205,7 +208,8 @@ sourcesJar { duplicatesStrategy = DuplicatesStrategy.EXCLUDE } -task testJar(type: Jar, dependsOn: testClasses) { +tasks.register('testJar', Jar) { + dependsOn testClasses duplicatesStrategy = DuplicatesStrategy.EXCLUDE archiveClassifier.set( 'test' ) from sourceSets.test.output @@ -231,9 +235,9 @@ tasks.register( "generateAnnotationClasses", JavaCompile ) { destinationDirectory.set( project.layout.buildDirectory.dir( "generated/sources/annotations/" ) ) } -task generateEnversStaticMetamodel( - type: JavaCompile, - description: "Generate the Hibernate Envers revision entity static metamodel classes." ) { +tasks.register('generateEnversStaticMetamodel', JavaCompile) { + description = "Generate the Hibernate Envers revision entity static metamodel classes." + source = sourceSets.main.java // we only want to include these specific classes for metamodel generation. // if envers adds any additional revision entity classes, they must be listed here. @@ -250,10 +254,10 @@ task generateEnversStaticMetamodel( ] // put static metamodel classes back out to the source tree since they're version controlled. - destinationDirectory = new File( "${projectDir}/src/main/java" ) + destinationDirectory = layout.projectDirectory.dir("src/main/java").asFile } -tasks.withType( Test.class ).each { test -> +tasks.withType( Test.class ).configureEach { test -> test.systemProperty 'file.encoding', 'utf-8' // Allow creating a function in HSQLDB for this Java method test.systemProperty 'hsqldb.method_class_names', 'org.hibernate.orm.test.jpa.transaction.TransactionTimeoutTest.sleep' @@ -270,11 +274,12 @@ tasks.withType( Test.class ).each { test -> // see GradleParallelTestingResolver for how the test worker id is resolved in JDBC configs if ( project.db == "h2" || project.db == "hsqldb" || project.db == "pgsql_ci" || project.db == "edb_ci" || project.db == "oracle_ci" || project.db == "oracle_xe_ci" || project.db == "mysql_ci" || project.db == "mariadb_ci" - || project.db == "db2_ci" || project.db == "mssql_ci" || project.db == "cockroachdb" ) { + || project.db == "db2_ci" || project.db == "mssql_ci" || project.db == "cockroachdb" || project.db == "hana_ci" + || project.db == "tidb" ) { // Most systems have multi-threading and maxing out a core on both threads will hurt performance // Also, as soon as we hit 16+ threads, the returns are diminishing, so divide by 4 def threadCount = Runtime.runtime.availableProcessors() - def testThreads = project.getProperties().get( "test.threads" ) + def testThreads = project.findProperty( "test.threads" ) if ( testThreads == null ) { testThreads = threadCount >= 16 ? threadCount.intdiv( 4 ) : (threadCount.intdiv( 2 ) ?: 1) } @@ -286,19 +291,42 @@ tasks.withType( Test.class ).each { test -> } else if ( project.db == "oracle_test_pilot_database" ) { // Since Oracle TestPilot databases run on separate machines, use all threads for testing + test.maxParallelForks = Runtime.runtime.availableProcessors() * 2 + test.systemProperty 'maxParallelForks', test.maxParallelForks + } + else if ( project.db == "hana_cloud" ) { + // Since HANA Cloud databases run on separate machines, use all threads for testing test.maxParallelForks = Runtime.runtime.availableProcessors() test.systemProperty 'maxParallelForks', test.maxParallelForks } + else if ( project.db == "spannerpgsql" || project.db == "spanner" ) { + def threadCount = Runtime.runtime.availableProcessors() + def testThreads = project.findProperty( "test.threads" ) + if ( testThreads == null ) { + def dbCount = threadCount.intdiv( 2 ) ?: 1 + if ( dbCount > 8 ) { + testThreads = 4 + } else { + testThreads = dbCount.intdiv( 2 ) ?: 1 + } + } + else { + testThreads = Integer.parseInt( testThreads.toString() ) + } + test.maxParallelForks = Math.min( testThreads, 4 ) + test.systemProperty 'maxParallelForks', test.maxParallelForks + } } tasks.named( "javadoc", Javadoc ) { configure(options) { overview = rootProject.file( "shared/javadoc/overview.html" ) - exclude( "**/internal/**", "org/hibernate/boot/jaxb/**", "org/hibernate/tuple/**" ) + exclude( "**/internal/**", "org/hibernate/boot/jaxb/**", "org/hibernate/tuple/**", "org/hibernate/grammars/hql/**" ) } } tasks.sourcesJar.dependsOn ':hibernate-core:generateGraphParser' +tasks.sourcesJar.dependsOn ':hibernate-core:generateDeprecated-graphParser' tasks.sourcesJar.dependsOn ':hibernate-core:generateHqlParser' tasks.sourcesJar.dependsOn ':hibernate-core:generateSqlScriptParser' tasks.sourcesJar.dependsOn ':hibernate-core:generateOrderingParser' diff --git a/hibernate-core/src/main/antlr/org/hibernate/grammars/graph/GraphLanguageParser.g4 b/hibernate-core/src/main/antlr/org/hibernate/grammars/graph/GraphLanguageParser.g4 index a0e6c33b9974..ad3fdb779aab 100644 --- a/hibernate-core/src/main/antlr/org/hibernate/grammars/graph/GraphLanguageParser.g4 +++ b/hibernate-core/src/main/antlr/org/hibernate/grammars/graph/GraphLanguageParser.g4 @@ -21,15 +21,32 @@ package org.hibernate.grammars.graph; */ } - graph - : typeIndicator? attributeList - ; + : graphElementList + ; + +graphElementList + : graphElement (COMMA graphElement)* + ; + + +graphElement + : subGraph + | attributeNode + ; + +subGraph + : subTypeIndicator? LPAREN attributeList RPAREN + ; typeIndicator : TYPE_NAME COLON ; +subTypeIndicator + : COLON TYPE_NAME + ; + attributeList : attributeNode (COMMA attributeNode)* ; @@ -44,9 +61,4 @@ attributePath attributeQualifier : DOT ATTR_NAME - ; - -subGraph - : LPAREN typeIndicator? attributeList RPAREN - ; - + ; \ No newline at end of file diff --git a/hibernate-core/src/main/antlr/org/hibernate/grammars/graph/LegacyGraphLanguageParser.g4 b/hibernate-core/src/main/antlr/org/hibernate/grammars/graph/LegacyGraphLanguageParser.g4 new file mode 100644 index 000000000000..df697a42d6d6 --- /dev/null +++ b/hibernate-core/src/main/antlr/org/hibernate/grammars/graph/LegacyGraphLanguageParser.g4 @@ -0,0 +1,50 @@ +parser grammar LegacyGraphLanguageParser; + +options { + tokenVocab=GraphLanguageLexer; +} + +@header { +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.grammars.graph; +} + +@members { +/* + * Antlr grammar describing the Hibernate EntityGraph Language - for parsing a structured + * textual representation of an entity graph + * + * `LegacyGraphLanguageParser.g4` + */ +} + +graph + : typeIndicator? attributeList + ; + +typeIndicator + : TYPE_NAME COLON + ; + +attributeList + : attributeNode (COMMA attributeNode)* + ; + +attributeNode + : attributePath subGraph? + ; + +attributePath + : ATTR_NAME attributeQualifier? + ; + +attributeQualifier + : DOT ATTR_NAME + ; + +subGraph + : LPAREN typeIndicator? attributeList RPAREN + ; \ No newline at end of file diff --git a/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 b/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 index 17716d416a3c..f11eea189548 100644 --- a/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 +++ b/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 @@ -1833,7 +1833,7 @@ xmltableDefaultClause * This parser rule helps with that. Here we expect that the caller already understands their * context enough to know that keywords-as-identifiers are allowed. */ - // All except the possible optional following keywords LEFT, RIGHT, INNER, FULL, OUTER + // All except the possible optional following keywords LEFT, RIGHT, INNER, FULL, OUTER, NOT nakedIdentifier : IDENTIFIER | QUOTED_IDENTIFIER @@ -1962,7 +1962,7 @@ xmltableDefaultClause | NEW | NEXT | NO - | NOT +// | NOT | NOTHING | NULLS | OBJECT @@ -2054,4 +2054,5 @@ identifier | LEFT | OUTER | RIGHT + | NOT ; diff --git a/hibernate-core/src/main/java/org/hibernate/BatchSize.java b/hibernate-core/src/main/java/org/hibernate/BatchSize.java index 15b98e7eade7..c47caa5be0e0 100644 --- a/hibernate-core/src/main/java/org/hibernate/BatchSize.java +++ b/hibernate-core/src/main/java/org/hibernate/BatchSize.java @@ -9,7 +9,7 @@ import java.util.List; /** - * Specify a batch size, that is, how many entities should be + * Specifies a batch size, that is, how many entities should be * fetched in each request to the database, for an invocation of * {@link Session#findMultiple(Class, List, FindOption...)}. *
    @@ -33,7 +33,7 @@ *
  • on the other hand, for databases with no SQL array type, a * large batch size results in long SQL statements with many JDBC * parameters. - *

    + *

* A batch size is considered a hint. This option has no effect * on {@link Session#find(Class, Object, FindOption...)}. * diff --git a/hibernate-core/src/main/java/org/hibernate/DetachedObjectException.java b/hibernate-core/src/main/java/org/hibernate/DetachedObjectException.java new file mode 100644 index 000000000000..c177ade998c0 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/DetachedObjectException.java @@ -0,0 +1,20 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate; + +/** + * Thrown if a detached instance of an entity class is passed to + * a {@link Session} method that expects a managed instance. + * + * @author Gavin King + * + * @since 7.0 + */ +@Incubating +public class DetachedObjectException extends HibernateException { + public DetachedObjectException(String message) { + super( message ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/GraphParserMode.java b/hibernate-core/src/main/java/org/hibernate/GraphParserMode.java new file mode 100644 index 000000000000..d6b801b946a4 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/GraphParserMode.java @@ -0,0 +1,62 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate; + +/** + * Enumeration of available graph parser syntax modes. + * + */ +public enum GraphParserMode { + /** + * Legacy syntax: attribute(SubType: attributes) + * This is the legacy syntax. + */ + LEGACY( "legacy" ), + + /** + * Modern syntax: attribute:SubType(attributes) + * This is the preferred new syntax. + */ + MODERN( "modern" ); + + private final String configValue; + + GraphParserMode(String configValue) { + this.configValue = configValue; + } + + public String getConfigValue() { + return configValue; + } + + /** + * Interpret the configured valueHandlingMode value. + * Valid values are either a {@link GraphParserMode} object or its String representation. + * For string values, the matching is case insensitive, so you can use either {@code MODERN} or {@code modern}. + * + * @param graphParserMode configured {@link GraphParserMode} representation + * + * @return associated {@link GraphParserMode} object + */ + public static GraphParserMode interpret(Object graphParserMode) { + if ( graphParserMode == null ) { + return LEGACY; + } + else if ( graphParserMode instanceof GraphParserMode mode ) { + return mode; + } + else if ( graphParserMode instanceof String string ) { + for ( GraphParserMode value : values() ) { + if ( value.name().equalsIgnoreCase( string ) ) { + return value; + } + } + } + throw new HibernateException( + "Unrecognized graph_parser_mode value : " + graphParserMode + + ". Supported values include 'modern' and 'legacy'." + ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/Hibernate.java b/hibernate-core/src/main/java/org/hibernate/Hibernate.java index 183ae3054cdb..5a4cb11b3f78 100644 --- a/hibernate-core/src/main/java/org/hibernate/Hibernate.java +++ b/hibernate-core/src/main/java/org/hibernate/Hibernate.java @@ -272,7 +272,7 @@ public static Class getClass(T proxy) { /** * Get the true, underlying class of a proxied entity. - *

+ *

* Like {@link #getClass}, this operation might initialize a proxy by side effect. * However, here the initialization is avoided if possible. If the entity type is * defined with subclasses, the proxy will need to be initialized to properly diff --git a/hibernate-core/src/main/java/org/hibernate/KeyType.java b/hibernate-core/src/main/java/org/hibernate/KeyType.java new file mode 100644 index 000000000000..72c3270c6650 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/KeyType.java @@ -0,0 +1,34 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate; + +import jakarta.persistence.FindOption; + +/// FindOption allowing to load based on either id (default) or natural-id. +/// +/// @see jakarta.persistence.EntityManager#find +/// @see Session#findMultiple +/// +/// @since 7.3 +/// +/// @author Steve Ebersole +/// @author Gavin King +public enum KeyType implements FindOption { + /// Indicates to find by the entity's identifier. The default. + /// + /// @see jakarta.persistence.Id + /// @see jakarta.persistence.EmbeddedId + /// @see jakarta.persistence.IdClass + IDENTIFIER, + + /// Indicates to find based on the entity's natural-id, if one. + /// + /// @see org.hibernate.annotations.NaturalId + /// @see org.hibernate.annotations.NaturalIdClass + /// + /// @implSpec Will trigger an [IllegalArgumentException] if the entity does + /// not define a natural-id. + NATURAL +} diff --git a/hibernate-core/src/main/java/org/hibernate/Length.java b/hibernate-core/src/main/java/org/hibernate/Length.java index 9de8cb04653b..6fed4b7931c0 100644 --- a/hibernate-core/src/main/java/org/hibernate/Length.java +++ b/hibernate-core/src/main/java/org/hibernate/Length.java @@ -42,6 +42,8 @@ public final class Length { * For example, {@code @Column(length=LONG)} results * in the column type: *

+ * + * * * * @@ -63,6 +65,8 @@ public final class Length { * For example, {@code @Column(length=LONG16)} results * in the column type: *
Column type per database
Column typeDatabase
{@code varchar(32600)}on h2, Db2, and PostgreSQL
{@code text}on MySQL
{@code clob}on Oracle
+ * + * * * * @@ -85,6 +89,8 @@ public final class Length { * For example, {@code @Column(length=LONG32)} results * in the column type: *
Column type per database
Column typeDatabase
{@code varchar(32767)}on h2 and PostgreSQL
{@code text}on MySQL
{@code clob}on Oracle and Db2
+ * + * * * * diff --git a/hibernate-core/src/main/java/org/hibernate/LockOptions.java b/hibernate-core/src/main/java/org/hibernate/LockOptions.java index 572ea9f7c18e..e8748ba3b0f4 100644 --- a/hibernate-core/src/main/java/org/hibernate/LockOptions.java +++ b/hibernate-core/src/main/java/org/hibernate/LockOptions.java @@ -241,7 +241,7 @@ public LockOptions setTimeout(Timeout timeout) { /** * The {@linkplain #getTimeout() timeout}, in milliseconds, associated * with {@code this} options. - *

+ *

* {@link #NO_WAIT}, {@link #WAIT_FOREVER}, or {@link #SKIP_LOCKED} * represent 3 "magic" values. * @@ -255,7 +255,7 @@ public int getTimeOut() { /** * Set the {@linkplain #getTimeout() timeout}, in milliseconds, associated * with {@code this} options. - *

+ *

* {@link #NO_WAIT}, {@link #WAIT_FOREVER}, or {@link #SKIP_LOCKED} * represent 3 "magic" values. * diff --git a/hibernate-core/src/main/java/org/hibernate/Locking.java b/hibernate-core/src/main/java/org/hibernate/Locking.java index 5bf3cb2886b1..acb0ecd8dca8 100644 --- a/hibernate-core/src/main/java/org/hibernate/Locking.java +++ b/hibernate-core/src/main/java/org/hibernate/Locking.java @@ -47,7 +47,7 @@ enum Scope implements FindOption, LockOption, RefreshOption { * rows for collection tables ({@linkplain jakarta.persistence.ElementCollection}, * {@linkplain jakarta.persistence.OneToMany} and {@linkplain jakarta.persistence.ManyToMany}) * will also be locked. - *

+ *

* Hibernate will only lock these collection rows when they are joined. The alternatives * would be to either:

    *
  • diff --git a/hibernate-core/src/main/java/org/hibernate/MultiIdentifierLoadAccess.java b/hibernate-core/src/main/java/org/hibernate/MultiIdentifierLoadAccess.java index 2b5f850ff4b3..cc06f179edf2 100644 --- a/hibernate-core/src/main/java/org/hibernate/MultiIdentifierLoadAccess.java +++ b/hibernate-core/src/main/java/org/hibernate/MultiIdentifierLoadAccess.java @@ -16,7 +16,6 @@ * Loads multiple instances of a given entity type at once, by * specifying a list of identifier values. This allows the entities * to be fetched from the database in batches. - *

    *

      * var graph = session.createEntityGraph(Book.class);
      * graph.addSubgraph(Book_.publisher);
    diff --git a/hibernate-core/src/main/java/org/hibernate/NaturalIdLoadAccess.java b/hibernate-core/src/main/java/org/hibernate/NaturalIdLoadAccess.java
    index d60e404cfc9b..6e8a5cc599a5 100644
    --- a/hibernate-core/src/main/java/org/hibernate/NaturalIdLoadAccess.java
    +++ b/hibernate-core/src/main/java/org/hibernate/NaturalIdLoadAccess.java
    @@ -5,7 +5,6 @@
     package org.hibernate;
     
     import jakarta.persistence.EntityGraph;
    -
     import jakarta.persistence.PessimisticLockScope;
     import jakarta.persistence.Timeout;
     import jakarta.persistence.metamodel.SingularAttribute;
    @@ -20,14 +19,13 @@
      * entity. If the entity has exactly one attribute annotated
      * {@link org.hibernate.annotations.NaturalId @NaturalId},
      * then {@link SimpleNaturalIdLoadAccess} may be used instead.
    - * 

    - *

    + * 
    {@code
      * Book book =
      *         session.byNaturalId(Book.class)
      *             .using(Book_.isbn, isbn)
      *             .using(Book_.printing, printing)
      *             .load();
    - * 
    + * }
    * * @author Eric Dalquist * @author Steve Ebersole @@ -35,7 +33,10 @@ * @see Session#byNaturalId(Class) * @see org.hibernate.annotations.NaturalId * @see SimpleNaturalIdLoadAccess + * + * @deprecated (since 7.3) Use {@linkplain Session#find} with {@link KeyType#NATURAL} instead. */ +@Deprecated public interface NaturalIdLoadAccess { /** diff --git a/hibernate-core/src/main/java/org/hibernate/NaturalIdMultiLoadAccess.java b/hibernate-core/src/main/java/org/hibernate/NaturalIdMultiLoadAccess.java index 29c21475af44..9ee6c7edd7be 100644 --- a/hibernate-core/src/main/java/org/hibernate/NaturalIdMultiLoadAccess.java +++ b/hibernate-core/src/main/java/org/hibernate/NaturalIdMultiLoadAccess.java @@ -6,6 +6,7 @@ import jakarta.persistence.EntityGraph; +import jakarta.persistence.FindOption; import jakarta.persistence.PessimisticLockScope; import jakarta.persistence.Timeout; import org.hibernate.graph.GraphSemantic; @@ -16,13 +17,13 @@ * Loads multiple instances of a given entity type at once, by * specifying a list of natural id values. This allows the entities * to be fetched from the database in batches. - *

    - *

    + *
    + * 
    {@code
      * List<Book> books =
      *         session.byMultipleNaturalId(Book.class)
      *             .withBatchSize(10)
      *             .multiLoad(isbnList);
    - * 
    + * }
    *

    * Composite natural ids may be accommodated by passing a list of * maps of type {@code Map} to {@link #multiLoad}. @@ -36,7 +37,10 @@ * * @see Session#byMultipleNaturalId(Class) * @see org.hibernate.annotations.NaturalId + * + * @deprecated (since 7.3) Use {@linkplain Session#findMultiple(Class, List, FindOption...)} with {@link KeyType#NATURAL} instead. */ +@Deprecated public interface NaturalIdMultiLoadAccess { /** diff --git a/hibernate-core/src/main/java/org/hibernate/NaturalIdSynchronization.java b/hibernate-core/src/main/java/org/hibernate/NaturalIdSynchronization.java new file mode 100644 index 000000000000..4aee7d7deb36 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/NaturalIdSynchronization.java @@ -0,0 +1,19 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate; + +import jakarta.persistence.FindOption; + +/// Indicates whether to perform synchronization (a sort of flush) +/// before a [find by natural-id][KeyType#NATURAL]. +/// +/// @author Steve Ebersole +public enum NaturalIdSynchronization implements FindOption { + /// Perform the synchronization. + ENABLED, + + /// Do not perform the synchronization. + DISABLED; +} diff --git a/hibernate-core/src/main/java/org/hibernate/OrderingMode.java b/hibernate-core/src/main/java/org/hibernate/OrderingMode.java index 4e9e21d44759..0f119c7b9be8 100644 --- a/hibernate-core/src/main/java/org/hibernate/OrderingMode.java +++ b/hibernate-core/src/main/java/org/hibernate/OrderingMode.java @@ -28,7 +28,7 @@ * The default is {@link #ORDERED}. * * @see org.hibernate.Session#findMultiple(Class, List, FindOption...) - * @see org.hibernate.Session#findMultiple(EntityGraph, List , FindOption...) + * @see org.hibernate.Session#findMultiple(EntityGraph, List, FindOption...) * * @since 7.2 */ diff --git a/hibernate-core/src/main/java/org/hibernate/RemovalsMode.java b/hibernate-core/src/main/java/org/hibernate/RemovalsMode.java index 21a62d511ef0..018419386de5 100644 --- a/hibernate-core/src/main/java/org/hibernate/RemovalsMode.java +++ b/hibernate-core/src/main/java/org/hibernate/RemovalsMode.java @@ -18,7 +18,7 @@ * The default is {@link #REPLACE}. * * @see org.hibernate.Session#findMultiple(Class, List, FindOption...) - * @see org.hibernate.Session#findMultiple(EntityGraph, List , FindOption...) + * @see org.hibernate.Session#findMultiple(EntityGraph, List, FindOption...) * * @since 7.2 */ @@ -31,5 +31,15 @@ public enum RemovalsMode implements FindMultipleOption { /** * The default. Removed entities are replaced with {@code null} in the load result. */ - REPLACE + REPLACE, + /** + * Removed entities are excluded from the load result. + *

    + * This option is incompatible with {@link OrderingMode#ORDERED}. + * It must be used in conjunction with {@link OrderingMode#UNORDERED} + * and {@link SessionCheckMode#ENABLED}. + * + * @since 7.3 + */ + EXCLUDE } diff --git a/hibernate-core/src/main/java/org/hibernate/Session.java b/hibernate-core/src/main/java/org/hibernate/Session.java index cd4109bc9a24..79996816cb1c 100644 --- a/hibernate-core/src/main/java/org/hibernate/Session.java +++ b/hibernate-core/src/main/java/org/hibernate/Session.java @@ -4,1518 +4,1365 @@ */ package org.hibernate; -import java.util.Collection; -import java.util.List; -import java.util.function.Consumer; - -import jakarta.persistence.FindOption; -import jakarta.persistence.LockOption; -import jakarta.persistence.RefreshOption; -import jakarta.persistence.metamodel.EntityType; -import org.hibernate.graph.RootGraph; -import org.hibernate.jdbc.Work; -import org.hibernate.query.Query; -import org.hibernate.stat.SessionStatistics; - import jakarta.persistence.CacheRetrieveMode; import jakarta.persistence.CacheStoreMode; import jakarta.persistence.EntityGraph; import jakarta.persistence.EntityManager; +import jakarta.persistence.FindOption; import jakarta.persistence.FlushModeType; import jakarta.persistence.LockModeType; +import jakarta.persistence.LockOption; +import jakarta.persistence.RefreshOption; import jakarta.persistence.TypedQueryReference; import jakarta.persistence.criteria.CriteriaDelete; import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.CriteriaUpdate; +import jakarta.persistence.metamodel.EntityType; +import org.hibernate.graph.RootGraph; +import org.hibernate.query.NativeQuery; +import org.hibernate.query.Query; +import org.hibernate.stat.SessionStatistics; -/** - * The main runtime interface between a Java application and Hibernate. Represents the - * notion of a persistence context, a set of managed entity instances associated - * with a logical transaction. - *

    - * The lifecycle of a {@code Session} is bounded by the beginning and end of the logical - * transaction. But a long logical transaction might span several database transactions. - *

    - * The primary purpose of the {@code Session} is to offer create, read, and delete - * operations for instances of mapped entity classes. An instance may be in one of three - * states with respect to a given open session: - *

      - *
    • transient: never persistent, and not associated with the {@code Session}, - *
    • persistent: currently associated with the {@code Session}, or - *
    • detached: previously persistent, but not currently associated with the - * {@code Session}. - *
    - *

    - * Each persistent instance has a persistent identity determined by its type - * and identifier value. There may be at most one persistent instance with a given - * persistent identity associated with a given session. A persistent identity is - * assigned when an {@linkplain #persist(Object) instance is made persistent}. - *

    - * An instance of an entity class may be associated with at most one open session. - * Distinct sessions represent state with the same persistent identity using distinct - * persistent instances of the mapped entity class. - *

    - * Any instance returned by {@link #get(Class, Object)}, {@link #find(Class, Object)}, - * or by a query is persistent. A persistent instance might hold references to other - * entity instances, and sometimes these references are proxied by an - * intermediate object. When an associated entity has not yet been fetched from the - * database, references to the unfetched entity are represented by uninitialized - * proxies. The state of an unfetched entity is automatically fetched from the - * database when a method of its proxy is invoked, if and only if the proxy is - * associated with an open session. Otherwise, {@link #getReference(Object)} may be - * used to trade a proxy belonging to a closed session for a new proxy associated - * with the current session. - *

    - * A transient instance may be made persistent by calling {@link #persist(Object)}. - * A persistent instance may be made detached by calling {@link #detach(Object)}. - * A persistent instance may be marked for removal, and eventually made transient, - * by calling {@link #remove(Object)}. - *

    - * Persistent instances are held in a managed state by the persistence context. Any - * change to the state of a persistent instance is automatically detected and eventually - * flushed to the database. This process of automatic change detection is called - * dirty checking and can be expensive in some circumstances. Dirty checking - * may be disabled by marking an entity as read-only using - * {@link #setReadOnly(Object, boolean)} or simply by {@linkplain #detach(Object) evicting} - * it from the persistence context. A session may be set to load entities as read-only - * {@linkplain #setDefaultReadOnly(boolean) by default}, or this may be controlled at the - * {@linkplain Query#setReadOnly(boolean) query level}. - *

    - * The state of a transient or detached instance may be made persistent by copying it to - * a persistent instance using {@link #merge(Object)}. All older operations which moved a - * detached instance to the persistent state are now deprecated, and clients should now - * migrate to the use of {@code merge()}. - *

    - * The persistent state of a managed entity may be refreshed from the database, discarding - * all modifications to the object held in memory, by calling {@link #refresh(Object)}. - *

    - * From {@linkplain FlushMode time to time}, a {@linkplain #flush() flush operation} is - * triggered, and the session synchronizes state held in memory with persistent state - * held in the database by executing SQL {@code insert}, {@code update}, and {@code delete} - * statements. Note that SQL statements are often not executed synchronously by the methods - * of the {@code Session} interface. If synchronous execution of SQL is desired, the - * {@link StatelessSession} allows this. - *

    - * Each managed instance has an associated {@link LockMode}. By default, the session - * obtains only {@link LockMode#READ} on an entity instance it reads from the database - * and {@link LockMode#WRITE} on an entity instance it writes to the database. This - * behavior is appropriate for programs which use optimistic locking. - *

      - *
    • A different lock level may be obtained by explicitly specifying the mode using - * {@link #find(Class,Object,LockModeType)}, {@link #find(Class,Object,FindOption...)}, - * {@link #refresh(Object,LockModeType)}, {@link #refresh(Object,RefreshOption...)}, - * or {@link org.hibernate.query.SelectionQuery#setLockMode(LockModeType)}. - *
    • The lock level of a managed instance already held by the session may be upgraded - * to a more restrictive lock level by calling {@link #lock(Object, LockMode)} or - * {@link #lock(Object, LockModeType)}. - *
    - *

    - * A persistence context holds hard references to all its entities and prevents them - * from being garbage collected. Therefore, a {@code Session} is a short-lived object, - * and must be discarded as soon as a logical transaction ends. In extreme cases, - * {@link #clear()} and {@link #detach(Object)} may be used to control memory usage. - * However, for processes which read many entities, a {@link StatelessSession} should - * be used. - *

    - * A session might be associated with a container-managed JTA transaction, or it might be - * in control of its own resource-local database transaction. In the case of a - * resource-local transaction, the client must demarcate the beginning and end of the - * transaction using a {@link Transaction}. A typical resource-local transaction should - * use the following idiom: - *

    - * Session session = factory.openSession();
    - * Transaction tx = null;
    - * try {
    - *     tx = session.beginTransaction();
    - *     //do some work
    - *     ...
    - *     tx.commit();
    - * }
    - * catch (Exception e) {
    - *     if (tx!=null) tx.rollback();
    - *     throw e;
    - * }
    - * finally {
    - *     session.close();
    - * }
    - * 
    - *

    - * It's crucially important to appreciate the following restrictions and why they exist: - *

      - *
    • If the {@code Session} throws an exception, the current transaction must be rolled - * back and the session must be discarded. The internal state of the {@code Session} - * cannot be expected to be consistent with the database after the exception occurs. - *
    • At the end of a logical transaction, the session must be explicitly {@linkplain - * #close() destroyed}, so that all JDBC resources may be released. - *
    • If a transaction is rolled back, the state of the persistence context and of its - * associated entities must be assumed inconsistent with the database, and the - * session must be discarded. - *
    • A {@code Session} is never thread-safe. It contains various different sorts of - * fragile mutable state. Each thread or transaction must obtain its own dedicated - * instance from the {@link SessionFactory}. - *
    - *

    - * An easy way to be sure that session and transaction management is being done correctly - * is to {@linkplain SessionFactory#inTransaction(Consumer) let the factory do it}: - *

    - * sessionFactory.inTransaction(session -> {
    - *     //do the work
    - *     ...
    - * });
    - * 
    - *

    - * A session may be used to {@linkplain #doWork(Work) execute JDBC work} using its JDBC - * connection and transaction: - *

    - * session.doWork(connection -> {
    - *     try ( PreparedStatement ps = connection.prepareStatement( " ... " ) ) {
    - *         ps.execute();
    - *     }
    - * });
    - * 
    - *

    - * A {@code Session} instance is serializable if its entities are serializable. - *

    - * Every {@code Session} is a JPA {@link EntityManager}. Furthermore, when Hibernate is - * acting as the JPA persistence provider, the method {@link EntityManager#unwrap(Class)} - * may be used to obtain the underlying {@code Session}. - *

    - * Hibernate, unlike JPA, allows a persistence unit where an entity class is mapped multiple - * times, with different entity names, usually to different tables. In this case, the session - * needs a way to identify the entity name of a given instance of the entity class. Therefore, - * some operations of this interface, including operations inherited from {@code EntityManager}, - * are overloaded with a form that accepts an explicit entity name along with the instance. An - * alternative solution to this problem is to provide an {@link EntityNameResolver}. - * - * @see SessionFactory - * - * @author Gavin King - * @author Steve Ebersole - */ +import java.util.Collection; +import java.util.List; + +/// The main runtime interface between a Java application and Hibernate. Represents the +/// notion of a _persistence context_, a set of managed entity instances associated +/// with a logical transaction. +/// +/// The lifecycle of a `Session` is bounded by the beginning and end of the logical +/// transaction. But a long logical transaction might span several database transactions. +/// +/// The primary purpose of the `Session` is to offer create, read, and delete +/// operations for instances of mapped entity classes. An instance may be in one of three +/// states with respect to a given open session: +/// +/// - _transient:_ never persistent, and not associated with the `Session`, +/// - _persistent:_ currently associated with the `Session`, or +/// - _detached:_ previously persistent, but not currently associated with the +/// `Session`. +/// +/// Each persistent instance has a _persistent identity_ determined by its type +/// and identifier value. There may be at most one persistent instance with a given +/// persistent identity associated with a given session. A persistent identity is +/// assigned when an {@linkplain #persist(Object) instance is made persistent}. +/// +/// An instance of an entity class may be associated with at most one open session. +/// Distinct sessions represent state with the same persistent identity using distinct +/// persistent instances of the mapped entity class. +/// +/// Any instance returned by [#get(Class,Object)], [#find(Class,Object)], +/// or by a query is persistent. A persistent instance might hold references to other +/// entity instances, and sometimes these references are _proxied_ by an +/// intermediate object. When an associated entity has not yet been fetched from the +/// database, references to the unfetched entity are represented by uninitialized +/// proxies. The state of an unfetched entity is automatically fetched from the +/// database when a method of its proxy is invoked, if and only if the proxy is +/// associated with an open session. Otherwise, [#getReference(Object)] may be +/// used to trade a proxy belonging to a closed session for a new proxy associated +/// with the current session. +/// +/// A transient instance may be made persistent by calling [#persist(Object)]. +/// A persistent instance may be made detached by calling [#detach(Object)]. +/// A persistent instance may be marked for removal, and eventually made transient, +/// by calling [#remove(Object)]. +/// +/// Persistent instances are held in a managed state by the persistence context. Any +/// change to the state of a persistent instance is automatically detected and eventually +/// flushed to the database. This process of automatic change detection is called +/// _dirty checking_ and can be expensive in some circumstances. Dirty checking +/// may be disabled by marking an entity as read-only using +/// [#setReadOnly(Object,boolean)] or simply by [evicting][#detach(Object)] +/// it from the persistence context. A session may be set to load entities as read-only +/// [by default][#setDefaultReadOnly(boolean)], or this may be controlled at the +/// [query level][Query#setReadOnly(boolean)]. +/// +/// The state of a transient or detached instance may be made persistent by copying it to +/// a persistent instance using [#merge(Object)]. Since version 7, all older operations +/// which moved a detached instance to the persistent state have been completely removed, +/// and clients must now migrate to the use of `merge()`. +/// +/// The persistent state of a managed entity may be refreshed from the database, discarding +/// all modifications to the object held in memory, by calling [#refresh(Object)]. +/// +/// From {@linkplain FlushMode time to time}, a {@linkplain #flush() flush operation} is +/// triggered, and the session synchronizes state held in memory with persistent state +/// held in the database by executing SQL `insert`, `update`, and `delete` +/// statements. Note that SQL statements are often not executed synchronously by the methods +/// of the `Session` interface. If synchronous execution of SQL is desired, the +/// [StatelessSession] allows this. +/// +/// Each managed instance has an associated [LockMode]. By default, the session +/// obtains only [LockMode#READ] on an entity instance it reads from the database +/// and [LockMode#WRITE] on an entity instance it writes to the database. This +/// behavior is appropriate for programs which use optimistic locking. +/// +/// - A different lock level may be obtained by explicitly specifying the mode using +/// [#find(Class,Object,LockModeType)], [#find(Class,Object,FindOption...)], +/// [#refresh(Object,LockModeType)], [#refresh(Object,RefreshOption...)], +/// or [org.hibernate.query.SelectionQuery#setLockMode(LockModeType)]. +/// - The lock level of a managed instance already held by the session may be upgraded +/// to a more restrictive lock level by calling [#lock(Object,LockMode)] or +/// [#lock(Object,LockModeType)]. +/// +/// A persistence context holds hard references to all its entities and prevents them +/// from being garbage collected. Therefore, a `Session` is a short-lived object, +/// and must be discarded as soon as a logical transaction ends. In extreme cases, +/// [#clear()] and [#detach(Object)] may be used to control memory usage. +/// However, for processes which read many entities, a [StatelessSession] should +/// be used. +/// +/// A session might be associated with a container-managed JTA transaction, or it might be +/// in control of its own _resource-local_ database transaction. In the case of a +/// resource-local transaction, the client must demarcate the beginning and end of the +/// transaction using a [Transaction]. A typical resource-local transaction should +/// use the following idiom: +/// +/// ```java +/// try (var session = factory.openSession()) { +/// Transaction tx = null; +/// try { +/// tx = session.beginTransaction(); +/// //do some work +/// ... +/// tx.commit(); +/// } +/// catch (Exception e) { +/// if (tx!=null) tx.rollback(); +/// throw e; +/// } +/// } +/// ``` +/// +/// It's crucially important to appreciate the following restrictions and why they exist: +/// +/// - If the `Session` throws an exception, the current transaction must be rolled +/// back and the session must be discarded. The internal state of the `Session` +/// cannot be expected to be consistent with the database after the exception occurs. +/// - At the end of a logical transaction, the session must be explicitly +/// [destroyed][#close()], so that all JDBC resources may be released. +/// - If a transaction is rolled back, the state of the persistence context and of its +/// associated entities must be assumed inconsistent with the database, and the +/// session must be discarded. +/// - A `Session` is never thread-safe. It contains various different sorts of +/// fragile mutable state. Each thread or transaction must obtain its own dedicated +/// instance from the [SessionFactory]. +/// +/// An easy way to be sure that session and transaction management is being done correctly +/// is to [let the factory do it][SessionFactory#inTransaction(java.util.function.Consumer)]: +/// +/// ```java +/// sessionFactory.inTransaction(session -> { +/// //do the work +/// ... +/// }); +/// ``` +/// +/// A session may be used to [execute JDBC work][#doWork] using its JDBC +/// connection and transaction: +/// +/// ```java +/// session.doWork(connection -> { +/// try (var statement = connection.prepareStatement(...)) { +/// statement.execute(); +/// } +/// }); +/// ``` +/// +/// A `Session` instance is serializable if its entities are serializable. +/// +/// Every `Session` is a JPA [EntityManager]. Furthermore, when Hibernate is +/// acting as the JPA persistence provider, the method [#unwrap(Class)] +/// may be used to obtain the underlying `Session`. +/// +/// Hibernate, unlike JPA, allows a persistence unit where an entity class is mapped multiple +/// times, with different entity names, usually to different tables. In this case, the session +/// needs a way to identify the entity name of a given instance of the entity class. Therefore, +/// some operations of this interface, including operations inherited from `EntityManager`, +/// are overloaded with a form that accepts an explicit entity name along with the instance. An +/// alternative solution to this problem is to provide an [EntityNameResolver]. +/// +/// @see SessionFactory +/// +/// @author Gavin King +/// @author Steve Ebersole public interface Session extends SharedSessionContract, EntityManager { - /** - * Force this session to flush. Must be called at the end of a unit of work, - * before the transaction is committed. Depending on the current - * {@linkplain #setHibernateFlushMode(FlushMode) flush mode}, the session might - * automatically flush when {@link Transaction#commit()} is called, and it is not - * necessary to call this method directly. - *

    - * Flushing is the process of synchronizing the underlying persistent - * store with persistable state held in memory. - * - * @throws HibernateException if changes could not be synchronized with the database - */ + /// Force this session to flush. Must be called at the end of a unit of work, + /// before the transaction is committed. Depending on the current + /// [flush mode][#getHibernateFlushMode()], the session might + /// automatically flush when [Transaction#commit()] is called, and it is not + /// necessary to call this method directly. + /// + /// _Flushing_ is the process of synchronizing the underlying persistent + /// store with persistable state held in memory. + /// + /// @throws HibernateException if changes could not be synchronized with the database @Override void flush(); - /** - * Set the current {@linkplain FlushModeType JPA flush mode} for this session. - *

    - * Flushing is the process of synchronizing the underlying persistent - * store with persistable state held in memory. The current flush mode determines - * when the session is automatically flushed. - * - * @param flushMode the new {@link FlushModeType} - * - * @see #setHibernateFlushMode(FlushMode) - */ + /// Set the current [JPA flush mode][FlushModeType] for this session. + /// + /// _Flushing_ is the process of synchronizing the underlying persistent + /// store with persistable state held in memory. The current flush mode determines + /// when the session is automatically flushed. + /// + /// @param flushMode the new [FlushModeType] + /// + /// @see #setHibernateFlushMode(FlushMode) @Override void setFlushMode(FlushModeType flushMode); - /** - * Set the current {@linkplain FlushMode flush mode} for this session. - *

    - * Flushing is the process of synchronizing the underlying persistent - * store with persistable state held in memory. The current flush mode determines - * when the session is automatically flushed. - *

    - * The {@linkplain FlushMode#AUTO default flush mode} is sometimes unnecessarily - * aggressive. For a logically "read only" session, it's reasonable to set the - * session's flush mode to {@link FlushMode#MANUAL} at the start of the session - * in order to avoid some unnecessary work. - *

    - * Note that {@link FlushMode} defines more options than {@link FlushModeType}. - * - * @param flushMode the new {@link FlushMode} - */ + /// Set the current [flush mode][FlushMode] for this session. + /// + /// _Flushing_ is the process of synchronizing the underlying persistent + /// store with persistable state held in memory. The current flush mode determines + /// when the session is automatically flushed. + /// + /// The [default flush mode][FlushMode#AUTO] is sometimes unnecessarily + /// aggressive. For a logically "read only" session, it's reasonable to set the + /// session's flush mode to [FlushMode#MANUAL] at the start of the session + /// in order to avoid some unnecessary work. + /// + /// Note that [FlushMode] defines more options than [FlushModeType]. + /// + /// @param flushMode the new [FlushMode] void setHibernateFlushMode(FlushMode flushMode); - /** - * Get the current {@linkplain FlushModeType JPA flush mode} for this session. - * - * @return the {@link FlushModeType} currently in effect - * - * @see #getHibernateFlushMode() - */ + /// Get the current [JPA flush mode][FlushModeType] for this session. + /// + /// @return the [FlushModeType] currently in effect + /// + /// @see #getHibernateFlushMode() @Override FlushModeType getFlushMode(); - /** - * Get the current {@linkplain FlushMode flush mode} for this session. - * - * @return the {@link FlushMode} currently in effect - */ + /// Get the current [flush mode][FlushMode] for this session. + /// + /// @return the [FlushMode] currently in effect FlushMode getHibernateFlushMode(); - /** - * Set the current {@linkplain CacheMode cache mode} for this session. - *

    - * The cache mode determines the manner in which this session can interact with - * the second level cache. - * - * @param cacheMode the new cache mode - */ + /// Set the current [cache mode][CacheMode] for this session. + /// + /// The cache mode determines the manner in which this session can interact with + /// the second level cache. + /// + /// @param cacheMode the new cache mode void setCacheMode(CacheMode cacheMode); - /** - * Get the current {@linkplain CacheMode cache mode} for this session. - * - * @return the current cache mode - */ + /// Get the current [cache mode][CacheMode] for this session. + /// + /// @return the current cache mode CacheMode getCacheMode(); - /** - * The JPA-defined {@link CacheStoreMode}. - * - * @see #getCacheMode() - * - * @since 6.2 - */ + /// The JPA-defined [CacheStoreMode]. + /// + /// @see #getCacheMode() + /// + /// @since 6.2 @Override CacheStoreMode getCacheStoreMode(); - /** - * The JPA-defined {@link CacheRetrieveMode}. - * - * @see #getCacheMode() - * - * @since 6.2 - */ + /// The JPA-defined [CacheRetrieveMode]. + /// + /// @see #getCacheMode() + /// + /// @since 6.2 @Override CacheRetrieveMode getCacheRetrieveMode(); - /** - * Enable or disable writes to the second-level cache. - * - * @param cacheStoreMode a JPA-defined {@link CacheStoreMode} - * - * @see #setCacheMode(CacheMode) - * - * @since 6.2 - */ + /// Enable or disable writes to the second-level cache. + /// + /// @param cacheStoreMode a JPA-defined [CacheStoreMode] + /// + /// @see #setCacheMode(CacheMode) + /// + /// @since 6.2 @Override void setCacheStoreMode(CacheStoreMode cacheStoreMode); - /** - * Enable or disable reads from the second-level cache. - * - * @param cacheRetrieveMode a JPA-defined {@link CacheRetrieveMode} - * - * @see #setCacheMode(CacheMode) - * - * @since 6.2 - */ + /// Enable or disable reads from the second-level cache. + /// + /// @param cacheRetrieveMode a JPA-defined [CacheRetrieveMode] + /// + /// @see #setCacheMode(CacheMode) + /// + /// @since 6.2 @Override void setCacheRetrieveMode(CacheRetrieveMode cacheRetrieveMode); - /** - * Get the maximum batch size for batch fetching associations by - * id in this session. - * - * @since 6.3 - */ + /// Get the maximum batch size for batch fetching associations by + /// id in this session. + /// + /// @since 6.3 int getFetchBatchSize(); - /** - * Set the maximum batch size for batch fetching associations by - * id in this session. Override the - * {@linkplain org.hibernate.boot.spi.SessionFactoryOptions#getDefaultBatchFetchSize() - * factory-level} default controlled by the configuration property - * {@value org.hibernate.cfg.AvailableSettings#DEFAULT_BATCH_FETCH_SIZE}. - *

    - *

      - *
    • If {@code batchSize>1}, then batch fetching is enabled. - *
    • If {@code batchSize<0}, the batch size is inherited from - * the factory-level setting. - *
    • Otherwise, batch fetching is disabled. - *
    - * - * @param batchSize the maximum batch size for batch fetching - * - * @since 6.3 - * - * @see org.hibernate.cfg.AvailableSettings#DEFAULT_BATCH_FETCH_SIZE - */ + /// Set the maximum batch size for batch fetching associations by + /// id in this session. Override the + /// [factory-level][org.hibernate.boot.spi.SessionFactoryOptions#getDefaultBatchFetchSize()] + /// default controlled by the configuration property + /// {@value org.hibernate.cfg.AvailableSettings#DEFAULT_BATCH_FETCH_SIZE}. + /// + /// - If `batchSize>1`, then batch fetching is enabled. + /// - If `batchSize<0`, the batch size is inherited from + /// the factory-level setting. + /// - Otherwise, batch fetching is disabled. + /// + /// @param batchSize the maximum batch size for batch fetching + /// + /// @since 6.3 + /// + /// @see org.hibernate.cfg.AvailableSettings#DEFAULT_BATCH_FETCH_SIZE void setFetchBatchSize(int batchSize); - /** - * Determine if subselect fetching is enabled in this session. - * - * @return {@code true} is subselect fetching is enabled - * - * @since 6.3 - */ + /// Determine if subselect fetching is enabled in this session. + /// + /// @return `true` is subselect fetching is enabled + /// + /// @since 6.3 boolean isSubselectFetchingEnabled(); - /** - * Enable or disable subselect fetching in this session. Override the - * {@linkplain org.hibernate.boot.spi.SessionFactoryOptions#isSubselectFetchEnabled() - * factory-level} default controlled by the configuration property - * {@value org.hibernate.cfg.AvailableSettings#USE_SUBSELECT_FETCH}. - * - * @param enabled {@code true} to enable subselect fetching - * - * @since 6.3 - * - * @see org.hibernate.cfg.AvailableSettings#USE_SUBSELECT_FETCH - */ + /// Enable or disable subselect fetching in this session. Override the + /// [factory-level][org.hibernate.boot.spi.SessionFactoryOptions#isSubselectFetchEnabled()] + /// default controlled by the configuration property + /// {@value org.hibernate.cfg.AvailableSettings#USE_SUBSELECT_FETCH}. + /// + /// @param enabled `true` to enable subselect fetching + /// + /// @since 6.3 + /// + /// @see org.hibernate.cfg.AvailableSettings#USE_SUBSELECT_FETCH void setSubselectFetchingEnabled(boolean enabled); - /** - * Get the session factory which created this session. - * - * @return the session factory - * - * @see SessionFactory - */ + /// Get the session factory which created this session. + /// + /// @return the session factory + /// + /// @see SessionFactory SessionFactory getSessionFactory(); - /** - * Cancel the execution of the current query. - *

    - * This is the sole method on session which may be safely called from - * another thread. - * - * @throws HibernateException if there was a problem cancelling the query - */ + /// Cancel the execution of the current query. + /// + /// This is the sole method on session which may be safely called from + /// another thread. + /// + /// @throws HibernateException if there was a problem cancelling the query void cancelQuery(); - /** - * Does this session contain any changes which must be synchronized with - * the database? In other words, would any DML operations be executed if - * we flushed this session? - * - * @return {@code true} if the session contains pending changes; - * {@code false} otherwise. - */ + /// Whether this session contains any changes which must be synchronized with + /// the database. In other words, would any DML operations be executed if + /// we flushed this session? + /// + /// @return `true` if the session contains pending changes; `false` otherwise. boolean isDirty(); - /** - * Will entities and proxies that are loaded into this session be made - * read-only by default? - *

    - * To determine the read-only/modifiable setting for a particular entity - * or proxy use {@link #isReadOnly(Object)}. - * - * @see #isReadOnly(Object) - * - * @return {@code true}, loaded entities/proxies will be made read-only by default; - * {@code false}, loaded entities/proxies will be made modifiable by default. - */ + /// Will entities and proxies that are loaded into this session be made + /// read-only by default? + /// + /// To determine the read-only/modifiable setting for a particular entity + /// or proxy use [#isReadOnly(Object)]. + /// + /// @see #isReadOnly(Object) + /// + /// @return `true`, loaded entities/proxies will be made read-only by default; + /// `false`, loaded entities/proxies will be made modifiable by default. boolean isDefaultReadOnly(); - /** - * Change the default for entities and proxies loaded into this session - * from modifiable to read-only mode, or from read-only to modifiable mode. - *

    - * Read-only entities are not dirty-checked, and snapshots of persistent - * state are not maintained. Read-only entities can be modified, but a - * modification to a field of a read-only entity is not made persistent. - *

    - * When a proxy is initialized, the loaded entity will have the same - * read-only/modifiable setting as the uninitialized proxy, regardless of - * the {@linkplain #isDefaultReadOnly current default read-only mode} - * of the session. - *

    - * To change the read-only/modifiable setting for a particular entity - * or proxy that already belongs to this session, use - * {@link #setReadOnly(Object, boolean)}. - *

    - * To override the default read-only mode of the current session for - * all entities and proxies returned by a given {@code Query}, use - * {@link Query#setReadOnly(boolean)}. - *

    - * Every instance of an {@linkplain org.hibernate.annotations.Immutable - * immutable} entity is loaded in read-only mode. - * - * @see #setReadOnly(Object,boolean) - * @see Query#setReadOnly(boolean) - * - * @param readOnly {@code true}, the default for loaded entities/proxies is read-only; - * {@code false}, the default for loaded entities/proxies is modifiable - * @throws SessionException if the session was originally - * {@linkplain SessionBuilder#readOnly created in read-only mode} - */ + /// Change the default for entities and proxies loaded into this session + /// from modifiable to read-only mode, or from read-only to modifiable mode. + /// + /// Read-only entities are not dirty-checked, and snapshots of persistent + /// state are not maintained. Read-only entities can be modified, but a + /// modification to a field of a read-only entity is not made persistent. + /// + /// When a proxy is initialized, the loaded entity will have the same + /// read-only/modifiable setting as the uninitialized proxy, regardless of + /// the [current default read-only mode][#isDefaultReadOnly] + /// of the session. + /// + /// To change the read-only/modifiable setting for a particular entity + /// or proxy that already belongs to this session, use + /// [#setReadOnly(Object,boolean)]. + /// + /// To override the default read-only mode of the current session for + /// all entities and proxies returned by a given `Query`, use + /// [Query#setReadOnly(boolean)]. + /// + /// Every instance of an [immutable][org.hibernate.annotations.Immutable] + /// entity is loaded in read-only mode. + /// + /// @see #setReadOnly(Object,boolean) + /// @see Query#setReadOnly(boolean) + /// + /// @param readOnly `true`, the default for loaded entities/proxies is read-only; + /// `false`, the default for loaded entities/proxies is modifiable + /// @throws SessionException if the session was originally + /// {@linkplain SessionBuilder#readOnly created in read-only mode} void setDefaultReadOnly(boolean readOnly); - /** - * Return the identifier value of the given entity associated with this session. - * An exception is thrown if the given entity instance is transient or detached - * in relation to this session. - * - * @param object a persistent instance associated with this session - * - * @return the identifier - * - * @throws TransientObjectException if the instance is transient or associated with - * a different session - */ + /// Return the identifier value of the given entity associated with this session. + /// An exception is thrown if the given entity instance is transient or detached + /// in relation to this session. + /// + /// @param object a persistent instance associated with this session + /// + /// @return the identifier + /// + /// @throws TransientObjectException if the instance is transient or associated with + /// a different session Object getIdentifier(Object object); - /** - * Determine if the given entity is associated with this session. - * - * @param entityName the entity name - * @param object an instance of a persistent class - * - * @return {@code true} if the given instance is associated with this {@code Session} - * - * @deprecated Use {@link #contains(Object)} instead. - */ + /// Determine if the given entity is associated with this session. + /// + /// @param entityName the entity name + /// @param object an instance of a persistent class + /// + /// @return `true` if the given instance is associated with this `Session` + /// + /// @deprecated Use [#contains(Object)] instead. @Deprecated(since = "7.2", forRemoval = true) boolean contains(String entityName, Object object); - /** - * Remove this instance from the session cache. Changes to the instance will - * not be synchronized with the database. This operation cascades to associated - * instances if the association is mapped with - * {@link jakarta.persistence.CascadeType#DETACH}. - * - * @param object the managed instance to detach - */ + /// Remove this instance from the session cache. Changes to the instance will + /// not be synchronized with the database. This operation cascades to associated + /// instances if the association is mapped with [jakarta.persistence.CascadeType#DETACH]. + /// + /// @param object the managed instance to detach @Override void detach(Object object); - /** - * Remove this instance from the session cache. Changes to the instance will - * not be synchronized with the database. This operation cascades to associated - * instances if the association is mapped with - * {@link jakarta.persistence.CascadeType#DETACH}. - *

    - * This operation is a synonym for {@link #detach(Object)}. - * - * @param object the managed entity to evict - * - * @throws IllegalArgumentException if the given object is not an entity - */ + /// Remove this instance from the session cache. Changes to the instance will + /// not be synchronized with the database. This operation cascades to associated + /// instances if the association is mapped with [jakarta.persistence.CascadeType#DETACH]. + /// + /// This operation is a synonym for [#detach(Object)]. + /// + /// @param object the managed entity to evict + /// + /// @throws IllegalArgumentException if the given object is not an entity void evict(Object object); - /** - * Return the persistent instance of the given entity class with the given identifier, - * or null if there is no such persistent instance. If the instance is already associated - * with the session, return that instance. This method never returns an uninitialized - * instance. - *

    - * The object returned by {@code get()} or {@code find()} is either an unproxied instance - * of the given entity class, or a fully-fetched proxy object. - *

    - * This operation requests {@link LockMode#NONE}, that is, no lock, allowing the object - * to be retrieved from the cache without the cost of database access. However, if it is - * necessary to read the state from the database, the object will be returned with the - * lock mode {@link LockMode#READ}. - *

    - * To bypass the {@linkplain Cache second-level cache}, and ensure that the state of the - * requested instance is read directly from the database, either: - *

      - *
    • call {@link #find(Class, Object, FindOption...)}, passing - * {@link CacheRetrieveMode#BYPASS} as an option, - *
    • call {@link #find(Class, Object, FindOption...)} with the explicit lock mode - * {@link LockMode#READ}, or - *
    • {@linkplain #setCacheRetrieveMode set the cache mode} to - * {@link CacheRetrieveMode#BYPASS} before calling this method. - *
    - * - * @apiNote This operation is very similar to {@link #get(Class, Object)}. - * - * @param entityType the entity type - * @param id an identifier - * - * @return a fully-fetched persistent instance or null - */ + /// Return the persistent instance of the given entity class with the given identifier, + /// or null if there is no such persistent instance. If the instance is already associated + /// with the session, return that instance. This method never returns an uninitialized + /// instance. + /// + /// The object returned by `get()` or `find()` is either an unproxied instance + /// of the given entity class, or a fully-fetched proxy object. + /// + /// This operation requests [LockMode#NONE], that is, no lock, allowing the object + /// to be retrieved from the cache without the cost of database access. However, if it is + /// necessary to read the state from the database, the object will be returned with the + /// lock mode [LockMode#READ]. + /// + /// To bypass the [second-level cache][Cache], and ensure that the state of the + /// requested instance is read directly from the database, either: + /// + /// - call [#find(Class,Object,FindOption...)], passing + /// [CacheRetrieveMode#BYPASS] as an option, + /// - call [#find(Class,Object,FindOption...)] with the explicit lock mode + /// [LockMode#READ], or + /// - {@linkplain #setCacheRetrieveMode set the cache mode} to + /// [CacheRetrieveMode#BYPASS] before calling this method. + /// + /// @apiNote This operation is very similar to [#get(Class,Object)]. + /// + /// @param entityType the entity type + /// @param id an identifier + /// + /// @return a fully-fetched persistent instance or null @Override T find(Class entityType, Object id); - /** - * Return the persistent instance of the named entity type with the given identifier, - * or null if there is no such persistent instance. - *

    - * Differs from {@linkplain #find(Class, Object)} in that this form accepts - * the entity name of a {@linkplain org.hibernate.metamodel.RepresentationMode#MAP dynamic entity}. - * - * @see #find(Class, Object) - */ + /// {@inheritDoc} + /// + /// @implNote Note that Hibernate's implementation of this method can + /// also be used for loading an entity by its [natural-id][org.hibernate.annotations.NaturalId] + /// by passing [KeyType#NATURAL] as a [FindOption] and the natural-id value as the `key` to load. + /// + /// @param entityType the entity type + /// @param id an identifier + /// @param options options controlling the behavior of the operation + @Override + T find(Class entityType, Object id, FindOption... options); + + /// Return the persistent instance of the named entity type with the given identifier, + /// or null if there is no such persistent instance. + /// + /// Differs from {@linkplain #find(Class, Object)} in that this form accepts + /// the entity name of a [dynamic entity][org.hibernate.metamodel.RepresentationMode#MAP]. + /// + /// @see #find(Class, Object) Object find(String entityName, Object primaryKey); - /** - * Return the persistent instance of the named entity type with the given identifier - * using the specified options, or null if there is no such persistent instance. - *

    - * Differs from {@linkplain #find(Class, Object, FindOption...)} in that this form accepts - * the entity name of a {@linkplain org.hibernate.metamodel.RepresentationMode#MAP dynamic entity}. - * - * @see #find(Class, Object, FindOption...) - */ + /// Return the persistent instance of the named entity type with the given identifier + /// using the specified options, or null if there is no such persistent instance. + /// + /// Differs from [#find(Class, Object, FindOption...)] in that this form accepts + /// the entity name of a [dynamic entity][org.hibernate.metamodel.RepresentationMode#MAP]. + /// + /// @see #find(Class, Object, FindOption...) Object find(String entityName, Object primaryKey, FindOption... options); - /** - * Return the persistent instances of the given entity class with the given identifiers - * as a list. The position of an instance in the returned list matches the position of its - * identifier in the given list of identifiers, and the returned list contains a null value - * if there is no persistent instance matching a given identifier. If an instance is already - * associated with the session, that instance is returned. This method never returns an - * uninitialized instance. - *

    - * Every object returned by {@code findMultiple()} is either an unproxied instance of the - * given entity class, or a fully-fetched proxy object. - *

    - * This method accepts {@link BatchSize} as an option, allowing control over the number of - * records retrieved in a single database request. The performance impact of setting a batch - * size depends on whether a SQL array may be used to pass the list of identifiers to the - * database: - *

      - *
    • for databases which {@linkplain org.hibernate.dialect.Dialect#supportsStandardArrays - * support standard SQL arrays}, a smaller batch size might be extremely inefficient - * compared to a very large batch size or no batching at all, but - *
    • on the other hand, for databases with no SQL array type, a large batch size results - * in long SQL statements with many JDBC parameters. - *
    - * - * @param entityType the entity type - * @param ids the list of identifiers - * @param options options, if any - * - * @return an ordered list of persistent instances, with null elements representing missing - * entities, whose positions in the list match the positions of their ids in the - * given list of identifiers - * - * @see FindMultipleOption - * - * @since 7.0 - */ + /// Return the persistent instances of the given entity class with the given identifiers + /// as a list. The position of an instance in the returned list matches the position of its + /// identifier in the given list of identifiers, and the returned list contains a null value + /// if there is no persistent instance matching a given identifier. If an instance is already + /// associated with the session, that instance is returned. This method never returns an + /// uninitialized instance. + /// + /// Every object returned by `findMultiple()` is either an unproxied instance of the + /// given entity class, or a fully-fetched proxy object. + /// + /// This method accepts [BatchSize] as an option, allowing control over the number of + /// records retrieved in a single database request. The performance impact of setting a batch + /// size depends on whether a SQL array may be used to pass the list of identifiers to the + /// database: + /// + /// - for databases which [support standard SQL arrays][org.hibernate.dialect.Dialect#supportsStandardArrays], + /// a smaller batch size might be extremely inefficient compared to a very large batch size or + /// no batching at all, but + /// - on the other hand, for databases with no SQL array type, a large batch size results + /// in long SQL statements with many JDBC parameters. + /// + /// @param entityType the entity type + /// @param ids the list of identifiers + /// @param options options, if any + /// + /// @return an ordered list of persistent instances, with null elements representing missing + /// entities, whose positions in the list match the positions of their ids in the + /// given list of identifiers + /// + /// @see FindMultipleOption + /// + /// @since 7.0 List findMultiple(Class entityType, List ids, FindOption... options); - /** - * Return the persistent instances of the root entity of the given {@link EntityGraph} - * with the given identifiers as a list, fetching the associations specified by the - * graph, which is interpreted as a {@linkplain org.hibernate.graph.GraphSemantic#LOAD - * load graph}. The position of an instance in the returned list matches the position of - * its identifier in the given list of identifiers, and the returned list contains a null - * value if there is no persistent instance matching a given identifier. If an instance - * is already associated with the session, that instance is returned. This method never - * returns an uninitialized instance. - *

    - * Every object returned by {@code findMultiple()} is either an unproxied instance of the - * given entity class, or a fully-fetched proxy object. - *

    - * This method accepts {@link BatchSize} as an option, allowing control over the number of - * records retrieved in a single database request. The performance impact of setting a batch - * size depends on whether a SQL array may be used to pass the list of identifiers to the - * database: - *

      - *
    • for databases which {@linkplain org.hibernate.dialect.Dialect#supportsStandardArrays - * support standard SQL arrays}, a smaller batch size might be extremely inefficient - * compared to a very large batch size or no batching at all, but - *
    • on the other hand, for databases with no SQL array type, a large batch size results - * in long SQL statements with many JDBC parameters. - *
    - * - * @param entityGraph the entity graph interpreted as a load graph - * @param ids the list of identifiers - * @param options options, if any - * - * @return an ordered list of persistent instances, with null elements representing missing - * entities, whose positions in the list match the positions of their ids in the - * given list of identifiers - * - * @see FindMultipleOption - * - * @since 7.0 - */ + /// Return the persistent instances of the root entity of the given [EntityGraph] + /// with the given identifiers as a list, fetching the associations specified by the + /// graph, which is interpreted as a [load graph][org.hibernate.graph.GraphSemantic#LOAD]. + /// The position of an instance in the returned list matches the position of + /// its identifier in the given list of identifiers, and the returned list contains a null + /// value if there is no persistent instance matching a given identifier. If an instance + /// is already associated with the session, that instance is returned. This method never + /// returns an uninitialized instance. + /// + /// Every object returned by `findMultiple()` is either an unproxied instance of the + /// given entity class, or a fully-fetched proxy object. + /// + /// This method accepts [BatchSize] as an option, allowing control over the number of + /// records retrieved in a single database request. The performance impact of setting a batch + /// size depends on whether a SQL array may be used to pass the list of identifiers to the + /// database: + /// + /// - for databases which [support standard SQL arrays][org.hibernate.dialect.Dialect#supportsStandardArrays] + /// a smaller batch size might be extremely inefficient + /// compared to a very large batch size or no batching at all, but + /// - on the other hand, for databases with no SQL array type, a large batch size results + /// in long SQL statements with many JDBC parameters. + /// + /// @param entityGraph the entity graph interpreted as a load graph + /// @param ids the list of identifiers + /// @param options options, if any + /// + /// @return an ordered list of persistent instances, with null elements representing missing + /// entities, whose positions in the list match the positions of their ids in the + /// given list of identifiers + /// + /// @see FindMultipleOption + /// + /// @since 7.0 List findMultiple(EntityGraph entityGraph, List ids, FindOption... options); - /** - * Read the persistent state associated with the given identifier into the given - * transient instance. - * - * @param object a transient instance of an entity class - * @param id an identifier - */ + /// Read the persistent state associated with the given identifier into the given + /// transient instance. + /// + /// @param object a transient instance of an entity class + /// @param id an identifier void load(Object object, Object id); - /** - * Persist the state of the given detached instance, reusing the current - * identifier value. This operation cascades to associated instances if - * the association is mapped with - * {@link org.hibernate.annotations.CascadeType#REPLICATE}. - * - * @param object a detached instance of a persistent class - * @param replicationMode the replication mode to use - * - * @deprecated With no real replacement. For some use cases try - * {@link StatelessSession#upsert(Object)}. - */ + /// Persist the state of the given detached instance, reusing the current + /// identifier value. This operation cascades to associated instances if + /// the association is mapped with [org.hibernate.annotations.CascadeType#REPLICATE]. + /// + /// @param object a detached instance of a persistent class + /// @param replicationMode the replication mode to use + /// + /// @deprecated With no real replacement. For some use cases try [StatelessSession#upsert(Object)]. @Deprecated( since = "6.0" ) void replicate(Object object, ReplicationMode replicationMode); - /** - * Persist the state of the given detached instance, reusing the current - * identifier value. This operation cascades to associated instances if - * the association is mapped with - * {@link org.hibernate.annotations.CascadeType#REPLICATE}. - * - * @param entityName the entity name - * @param object a detached instance of a persistent class - * @param replicationMode the replication mode to use - * - * @deprecated With no real replacement. For some use cases try - * {@link StatelessSession#upsert(Object)}. - */ + /// Persist the state of the given detached instance, reusing the current + /// identifier value. This operation cascades to associated instances if + /// the association is mapped with [org.hibernate.annotations.CascadeType#REPLICATE]. + /// + /// @param entityName the entity name + /// @param object a detached instance of a persistent class + /// @param replicationMode the replication mode to use + /// + /// @deprecated With no real replacement. For some use cases try [StatelessSession#upsert(Object)]. @Deprecated( since = "6.0" ) void replicate(String entityName, Object object, ReplicationMode replicationMode) ; - /** - * Copy the state of the given object onto the persistent object with the same - * identifier. If there is no persistent instance currently associated with - * the session, it will be loaded. Return the persistent instance. If the - * given instance is unsaved, save a copy and return it as a newly persistent - * instance. The given instance does not become associated with the session. - * This operation cascades to associated instances if the association is mapped - * with {@link jakarta.persistence.CascadeType#MERGE}. - * - * @param object a detached instance with state to be copied - * - * @return an updated persistent instance - */ + /// Copy the state of the given object onto the persistent object with the same + /// identifier. If there is no persistent instance currently associated with + /// the session, it will be loaded. Return the persistent instance. If the + /// given instance is unsaved, save a copy and return it as a newly persistent + /// instance. The given instance does not become associated with the session. + /// This operation cascades to associated instances if the association is mapped + /// with [jakarta.persistence.CascadeType#MERGE]. + /// + /// @param object a detached instance with state to be copied + /// + /// @return an updated persistent instance @Override T merge(T object); - /** - * Copy the state of the given object onto the persistent object with the same - * identifier. If there is no persistent instance currently associated with - * the session, it will be loaded. Return the persistent instance. If the - * given instance is unsaved, save a copy and return it as a newly persistent - * instance. The given instance does not become associated with the session. - * This operation cascades to associated instances if the association is mapped - * with {@link jakarta.persistence.CascadeType#MERGE}. - * - * @param entityName the entity name - * @param object a detached instance with state to be copied - * - * @return an updated persistent instance - */ + /// Copy the state of the given object onto the persistent object with the same + /// identifier. If there is no persistent instance currently associated with + /// the session, it will be loaded. Return the persistent instance. If the + /// given instance is unsaved, save a copy and return it as a newly persistent + /// instance. The given instance does not become associated with the session. + /// This operation cascades to associated instances if the association is mapped + /// with [jakarta.persistence.CascadeType#MERGE]. + /// + /// @param entityName the entity name + /// @param object a detached instance with state to be copied + /// + /// @return an updated persistent instance T merge(String entityName, T object); - /** - * Copy the state of the given object onto the persistent object with the same - * identifier. If there is no persistent instance currently associated with - * the session, it is loaded using the given {@link EntityGraph}, which is - * interpreted as a load graph. Return the persistent instance. If the given - * instance is unsaved, save a copy and return it as a newly persistent instance. - * The given instance does not become associated with the session. This operation - * cascades to associated instances if the association is mapped with - * {@link jakarta.persistence.CascadeType#MERGE}. - * - * @param object a detached instance with state to be copied - * @param loadGraph an entity graph interpreted as a load graph - * - * @return an updated persistent instance - * - * @since 7.0 - */ + /// Copy the state of the given object onto the persistent object with the same + /// identifier. If there is no persistent instance currently associated with + /// the session, it is loaded using the given [EntityGraph], which is + /// interpreted as a load graph. Return the persistent instance. If the given + /// instance is unsaved, save a copy and return it as a newly persistent instance. + /// The given instance does not become associated with the session. This operation + /// cascades to associated instances if the association is mapped with + /// [jakarta.persistence.CascadeType#MERGE]. + /// + /// @param object a detached instance with state to be copied + /// @param loadGraph an entity graph interpreted as a load graph + /// + /// @return an updated persistent instance + /// + /// @since 7.0 T merge(T object, EntityGraph loadGraph); - /** - * Make a transient instance persistent and mark it for later insertion in the - * database. This operation cascades to associated instances if the association - * is mapped with {@link jakarta.persistence.CascadeType#PERSIST}. - *

    - * For an entity with a {@linkplain jakarta.persistence.GeneratedValue generated} - * id, {@code persist()} ultimately results in generation of an identifier for - * the given instance. But this may happen asynchronously, when the session is - * {@linkplain #flush() flushed}, depending on the identifier generation strategy. - * - * @param object a transient instance to be made persistent - */ + /// Make a transient instance persistent and mark it for later insertion in the + /// database. This operation cascades to associated instances if the association + /// is mapped with [jakarta.persistence.CascadeType#PERSIST]. + /// + /// For entities with a [generated id][jakarta.persistence.GeneratedValue], + /// `persist()` ultimately results in generation of an identifier for the + /// given instance. But this may happen asynchronously, when the session is + /// [flushed][#flush()], depending on the identifier generation strategy. + /// + /// @param object a transient instance to be made persistent @Override void persist(Object object); - /** - * Make a transient instance persistent and mark it for later insertion in the - * database. This operation cascades to associated instances if the association - * is mapped with {@link jakarta.persistence.CascadeType#PERSIST}. - *

    - * For entities with a {@link jakarta.persistence.GeneratedValue generated id}, - * {@code persist()} ultimately results in generation of an identifier for the - * given instance. But this may happen asynchronously, when the session is - * {@linkplain #flush() flushed}, depending on the identifier generation strategy. - * - * @param entityName the entity name - * @param object a transient instance to be made persistent - */ + /// Make a transient instance persistent and mark it for later insertion in the + /// database. This operation cascades to associated instances if the association + /// is mapped with [jakarta.persistence.CascadeType#PERSIST]. + /// + /// For entities with a [generated id][jakarta.persistence.GeneratedValue], + /// `persist()` ultimately results in generation of an identifier for the + /// given instance. But this may happen asynchronously, when the session is + /// [flushed][#flush()], depending on the identifier generation strategy. + /// + /// @param entityName the entity name + /// @param object a transient instance to be made persistent void persist(String entityName, Object object); - /** - * Obtain the specified lock level on the given managed instance associated - * with this session. This operation may be used to: - *

      - *
    • perform a version check on an entity read from the second-level cache - * by requesting {@link LockMode#READ}, - *
    • schedule a version check at transaction commit by requesting - * {@link LockMode#OPTIMISTIC}, - *
    • schedule a version increment at transaction commit by requesting - * {@link LockMode#OPTIMISTIC_FORCE_INCREMENT} - *
    • upgrade to a pessimistic lock with {@link LockMode#PESSIMISTIC_READ} - * or {@link LockMode#PESSIMISTIC_WRITE}, or - *
    • immediately increment the version of the given instance by requesting - * {@link LockMode#PESSIMISTIC_FORCE_INCREMENT}. - *
    - *

    - * If the requested lock mode is already held on the given entity, this - * operation has no effect. - *

    - * This operation cascades to associated instances if the association is - * mapped with {@link org.hibernate.annotations.CascadeType#LOCK}. - *

    - * The modes {@link LockMode#WRITE} and {@link LockMode#UPGRADE_SKIPLOCKED} - * are not legal arguments to {@code lock()}. - * - * @param object a persistent instance associated with this session - * @param lockMode the lock level - * - * @see #lock(Object, LockModeType) - */ + /// Obtain the specified lock level on the given managed instance associated + /// with this session. This operation may be used to: + /// + /// - perform a version check on an entity read from the second-level cache + /// by requesting [LockMode#READ], + /// - schedule a version check at transaction commit by requesting + /// [LockMode#OPTIMISTIC], + /// - schedule a version increment at transaction commit by requesting + /// [LockMode#OPTIMISTIC_FORCE_INCREMENT] + /// - upgrade to a pessimistic lock with [LockMode#PESSIMISTIC_READ] + /// or [LockMode#PESSIMISTIC_WRITE], or + /// - immediately increment the version of the given instance by requesting + /// [LockMode#PESSIMISTIC_FORCE_INCREMENT]. + /// + /// If the requested lock mode is already held on the given entity, this + /// operation has no effect. + /// + /// This operation cascades to associated instances if the association is + /// mapped with [org.hibernate.annotations.CascadeType#LOCK]. + /// + /// The modes [LockMode#WRITE] and [LockMode#UPGRADE_SKIPLOCKED] + /// are not legal arguments to `lock()`. + /// + /// @param object a persistent instance associated with this session + /// @param lockMode the lock level + /// + /// @see #lock(Object, LockModeType) void lock(Object object, LockMode lockMode); - /** - * Obtain the specified lock level on the given managed instance associated - * with this session, applying any other specified options. This operation may - * be used to: - *

      - *
    • perform a version check on an entity read from the second-level cache - * by requesting {@link LockMode#READ}, - *
    • schedule a version check at transaction commit by requesting - * {@link LockMode#OPTIMISTIC}, - *
    • schedule a version increment at transaction commit by requesting - * {@link LockMode#OPTIMISTIC_FORCE_INCREMENT} - *
    • upgrade to a pessimistic lock with {@link LockMode#PESSIMISTIC_READ} - * or {@link LockMode#PESSIMISTIC_WRITE}, or - *
    • immediately increment the version of the given instance by requesting - * {@link LockMode#PESSIMISTIC_FORCE_INCREMENT}. - *
    - *

    - * If the requested lock mode is already held on the given entity, this - * operation has no effect. - *

    - * This operation cascades to associated instances if the association is - * mapped with {@link org.hibernate.annotations.CascadeType#LOCK}. - *

    - * The modes {@link LockMode#WRITE} and {@link LockMode#UPGRADE_SKIPLOCKED} - * are not legal arguments to {@code lock()}. - * - * @param object a persistent instance associated with this session - * @param lockMode the lock level - * - * @see #lock(Object, LockModeType, LockOption...) - */ + /// Obtain the specified lock level on the given managed instance associated + /// with this session, applying any other specified options. This operation may + /// be used to: + /// + /// - perform a version check on an entity read from the second-level cache + /// by requesting [LockMode#READ], + /// - schedule a version check at transaction commit by requesting + /// [LockMode#OPTIMISTIC], + /// - schedule a version increment at transaction commit by requesting + /// [LockMode#OPTIMISTIC_FORCE_INCREMENT] + /// - upgrade to a pessimistic lock with [LockMode#PESSIMISTIC_READ] + /// or [LockMode#PESSIMISTIC_WRITE], or + /// - immediately increment the version of the given instance by requesting + /// [LockMode#PESSIMISTIC_FORCE_INCREMENT]. + /// + /// If the requested lock mode is already held on the given entity, this + /// operation has no effect. + /// + /// This operation cascades to associated instances if the association is + /// mapped with [org.hibernate.annotations.CascadeType#LOCK]. + /// + /// The modes [LockMode#WRITE] and [LockMode#UPGRADE_SKIPLOCKED] + /// are not legal arguments to `lock()`. + /// + /// @param object a persistent instance associated with this session + /// @param lockMode the lock level + /// + /// @see #lock(Object, LockModeType, LockOption...) void lock(Object object, LockMode lockMode, LockOption... lockOptions); - /** - * Reread the state of the given managed instance associated with this session - * from the underlying database. This may be useful: - *

      - *
    • when a database trigger alters the object state upon insert or update, - *
    • after {@linkplain #createMutationQuery(String) executing} any HQL update - * or delete statement, - *
    • after {@linkplain #createNativeMutationQuery(String) executing} a native - * SQL statement, or - *
    • after inserting a {@link java.sql.Blob} or {@link java.sql.Clob}. - *
    - *

    - * This operation cascades to associated instances if the association is mapped - * with {@link jakarta.persistence.CascadeType#REFRESH}. - *

    - * This operation requests {@link LockMode#READ}. To obtain a stronger lock, - * call {@link #refresh(Object, RefreshOption...)}, passing the appropriate - * {@link LockMode} as an option. - * - * @param object a persistent instance associated with this session - */ + /// Reread the state of the given managed instance associated with this session + /// from the underlying database. This may be useful: + /// + /// - when a database trigger alters the object state upon insert or update, + /// - after {@linkplain #createMutationQuery(String) executing} any HQL update + /// or delete statement, + /// - after {@linkplain #createNativeMutationQuery(String) executing} a native + /// SQL statement, or + /// - after inserting a [java.sql.Blob] or [java.sql.Clob]. + /// + /// This operation cascades to associated instances if the association is mapped + /// with [jakarta.persistence.CascadeType#REFRESH]. + /// + /// This operation requests [LockMode#READ]. To obtain a stronger lock, + /// call [#refresh(Object,RefreshOption...)], passing the appropriate + /// [LockMode] as an option. + /// + /// @param object a persistent instance associated with this session @Override void refresh(Object object); - /** - * Mark a persistence instance associated with this session for removal from - * the underlying database. Ths operation cascades to associated instances if - * the association is mapped {@link jakarta.persistence.CascadeType#REMOVE}. - *

    - * Except when operating in fully JPA-compliant mode, this operation does, - * contrary to the JPA specification, accept a detached entity instance. - * - * @param object the managed persistent instance to remove, or a detached - * instance unless operating in fully JPA-compliant mode - */ + /// {@inheritDoc} + /// + /// @param object a persistent instance associated with this session + /// @param options options controlling the behavior of the operation + @Override + void refresh(Object object, RefreshOption... options); + + /// Mark a persistence instance associated with this session for removal from + /// the underlying database. This operation cascades to associated instances + /// if the association is mapped [jakarta.persistence.CascadeType#REMOVE]. + /// + /// Except when operating in fully JPA-compliant mode, this operation does, + /// contrary to the JPA specification, accept a detached entity instance. + /// + /// @param object the managed persistent instance to remove, or a detached + /// instance unless operating in fully JPA-compliant mode @Override void remove(Object object); - /** - * Determine the current {@linkplain LockMode lock mode} held on the given - * managed instance associated with this session. - *

    - * Unlike the JPA-standard {@link #getLockMode}, this operation may be - * called when no transaction is active, in which case it should return - * {@link LockMode#NONE}, indicating that no pessimistic lock is held on - * the given entity. - * - * @param object a persistent instance associated with this session - * - * @return the lock mode currently held on the given entity - * - * @throws IllegalStateException if the given instance is not associated - * with this persistence context - * @throws ObjectDeletedException if the given instance was already - * {@linkplain #remove removed} - */ + /// Determine the current [lock mode][LockMode] held on the given + /// managed instance associated with this session. + /// + /// Unlike the JPA-standard [#getLockMode], this operation may be + /// called when no transaction is active, in which case it should return + /// [LockMode#NONE], indicating that no pessimistic lock is held on + /// the given entity. + /// + /// @param object a persistent instance associated with this session + /// + /// @return the lock mode currently held on the given entity + /// + /// @throws IllegalStateException if the given instance is not associated + /// with this persistence context + /// @throws ObjectDeletedException if the given instance was already + /// {@linkplain #remove removed} LockMode getCurrentLockMode(Object object); - /** - * Completely clear the persistence context. Evict all loaded instances, - * causing every managed entity currently associated with this session to - * transition to the detached state, and cancel all pending insertions, - * updates, and deletions. - *

    - * Does not close open iterators or instances of {@link ScrollableResults}. - */ + /// Completely clear the persistence context. Evict all loaded instances, + /// causing every managed entity currently associated with this session to + /// transition to the detached state, and cancel all pending insertions, + /// updates, and deletions. + /// + /// Does not close open iterators or instances of [ScrollableResults]. @Override void clear(); - /** - * Return the persistent instance of the given entity class with the given identifier, - * or null if there is no such persistent instance. If the instance is already associated - * with the session, return that instance. This method never returns an uninitialized - * instance. - *

    - * The object returned by {@code get()} or {@code find()} is either an unproxied instance - * of the given entity class, or a fully-fetched proxy object. - *

    - * This operation requests {@link LockMode#NONE}, that is, no lock, allowing the object - * to be retrieved from the cache without the cost of database access. However, if it is - * necessary to read the state from the database, the object will be returned with the - * lock mode {@link LockMode#READ}. - *

    - * To bypass the second-level cache, and ensure that the state is read from the database, - * either: - *

      - *
    • call {@link #get(Class, Object, LockMode)} with the explicit lock mode - * {@link LockMode#READ}, or - *
    • {@linkplain #setCacheMode set the cache mode} to {@link CacheMode#IGNORE} - * before calling this method. - *
    - * - * @apiNote This operation is very similar to {@link #find(Class, Object)}. - * - * @param entityType the entity type - * @param id an identifier - * - * @return a persistent instance or null - * - * @deprecated Because the semantics of this method may change in a future release. - * Use {@link #find(Class, Object)} instead. - */ + /// Return the persistent instance of the given entity class with the given identifier, + /// or null if there is no such persistent instance. If the instance is already associated + /// with the session, return that instance. This method never returns an uninitialized + /// instance. + /// + /// The object returned by `get()` or `find()` is either an unproxied instance + /// of the given entity class, or a fully-fetched proxy object. + /// + /// This operation requests [LockMode#NONE], that is, no lock, allowing the object + /// to be retrieved from the cache without the cost of database access. However, if it is + /// necessary to read the state from the database, the object will be returned with the + /// lock mode [LockMode#READ]. + /// + /// To bypass the second-level cache, and ensure that the state is read from the database, + /// either: + /// + /// - call [#get(Class,Object,LockMode)] with the explicit lock mode + /// [LockMode#READ], or + /// - {@linkplain #setCacheMode set the cache mode} to [CacheMode#IGNORE] + /// before calling this method. + /// + /// @apiNote This operation is very similar to [#find(Class,Object)]. + /// + /// @param entityType the entity type + /// @param id an identifier + /// + /// @return a persistent instance or null + /// + /// @deprecated Because the semantics of this method may change in a future release. + /// Use [#find(Class,Object)] instead. @Deprecated(since = "7.0", forRemoval = true) T get(Class entityType, Object id); - /** - * Return the persistent instance of the given entity class with the given identifier, - * or null if there is no such persistent instance. If the instance is already associated - * with the session, return that instance. This method never returns an uninitialized - * instance. Obtain the specified lock mode if the instance exists. - * - * @apiNote This operation is very similar to {@link #find(Class, Object, LockModeType)}. - * - * @param entityType the entity type - * @param id an identifier - * @param lockMode the lock mode - * - * @return a persistent instance or null - * - * @deprecated Use {@link #find(Class, Object, FindOption...)} instead. - */ + /// Return the persistent instance of the given entity class with the given identifier, + /// or null if there is no such persistent instance. If the instance is already associated + /// with the session, return that instance. This method never returns an uninitialized + /// instance. Obtain the specified lock mode if the instance exists. + /// + /// @apiNote This operation is very similar to [#find(Class,Object,LockModeType)]. + /// + /// @param entityType the entity type + /// @param id an identifier + /// @param lockMode the lock mode + /// + /// @return a persistent instance or null + /// + /// @deprecated Use [#find(Class,Object,FindOption...)] instead. @Deprecated(since = "7.0", forRemoval = true) T get(Class entityType, Object id, LockMode lockMode); - /** - * Return the persistent instance of the given named entity with the given identifier, - * or null if there is no such persistent instance. If the instance is already associated - * with the session, return that instance. This method never returns an uninitialized - * instance. - * - * @param entityName the entity name - * @param id an identifier - * - * @return a persistent instance or null - * - * @deprecated The semantics of this method may change in a future release. - * Use {@link SessionFactory#createGraphForDynamicEntity(String)} - * together with {@link #find(EntityGraph, Object, FindOption...)} - * to load {@link org.hibernate.metamodel.RepresentationMode#MAP - * dynamic entities}. - * - * @see SessionFactory#createGraphForDynamicEntity(String) - * @see #find(EntityGraph, Object, FindOption...) - */ + /// Return the persistent instance of the given named entity with the given identifier, + /// or null if there is no such persistent instance. If the instance is already associated + /// with the session, return that instance. This method never returns an uninitialized + /// instance. + /// + /// @param entityName the entity name + /// @param id an identifier + /// + /// @return a persistent instance or null + /// + /// @deprecated The semantics of this method may change in a future release. + /// Use [SessionFactory#createGraphForDynamicEntity(String)] + /// together with [#find(EntityGraph,Object,FindOption...)] + /// to load [dynamic entities][org.hibernate.metamodel.RepresentationMode#MAP]. + /// + /// @see SessionFactory#createGraphForDynamicEntity(String) + /// @see #find(EntityGraph, Object, FindOption...) @Deprecated(since = "7", forRemoval = true) Object get(String entityName, Object id); - /** - * Return the persistent instance of the given entity class with the given identifier, - * or null if there is no such persistent instance. If the instance is already associated - * with the session, return that instance. This method never returns an uninitialized - * instance. Obtain the specified lock mode if the instance exists. - * - * @param entityName the entity name - * @param id an identifier - * @param lockMode the lock mode - * - * @return a persistent instance or null - * - * @see #get(String, Object, LockOptions) - * - * @deprecated The semantics of this method may change in a future release. - */ + /// Return the persistent instance of the given entity class with the given identifier, + /// or null if there is no such persistent instance. If the instance is already associated + /// with the session, return that instance. This method never returns an uninitialized + /// instance. Obtain the specified lock mode if the instance exists. + /// + /// @param entityName the entity name + /// @param id an identifier + /// @param lockMode the lock mode + /// + /// @return a persistent instance or null + /// + /// @see #get(String, Object, LockOptions) + /// + /// @deprecated The semantics of this method may change in a future release. @Deprecated(since = "7.0", forRemoval = true) Object get(String entityName, Object id, LockMode lockMode); - /** - * Return the persistent instance of the given entity class with the given identifier, - * or null if there is no such persistent instance. If the instance is already associated - * with the session, return that instance. This method never returns an uninitialized - * instance. Obtain the specified lock mode if the instance exists. - * - * @param entityType the entity type - * @param id an identifier - * @param lockOptions the lock mode - * - * @return a persistent instance or null - * - * @deprecated This method will be removed. - * Use {@link #find(Class, Object, FindOption...)} instead. - */ + /// Return the persistent instance of the given entity class with the given identifier, + /// or null if there is no such persistent instance. If the instance is already associated + /// with the session, return that instance. This method never returns an uninitialized + /// instance. Obtain the specified lock mode if the instance exists. + /// + /// @param entityType the entity type + /// @param id an identifier + /// @param lockOptions the lock mode + /// + /// @return a persistent instance or null + /// + /// @deprecated This method will be removed. + /// Use [#find(Class,Object,FindOption...)] instead. @Deprecated(since = "7.0", forRemoval = true) T get(Class entityType, Object id, LockOptions lockOptions); - /** - * Return the persistent instance of the given entity class with the given identifier, - * or null if there is no such persistent instance. If the instance is already associated - * with the session, return that instance. This method never returns an uninitialized - * instance. Obtain the specified lock mode if the instance exists. - * - * @param entityName the entity name - * @param id an identifier - * @param lockOptions contains the lock mode - * - * @return a persistent instance or null - * - * @deprecated This method will be removed. - * Use {@link SessionFactory#createGraphForDynamicEntity(String)} - * together with {@link #find(EntityGraph, Object, FindOption...)} - * to load {@link org.hibernate.metamodel.RepresentationMode#MAP - * dynamic entities}. - */ + /// Return the persistent instance of the given entity class with the given identifier, + /// or null if there is no such persistent instance. If the instance is already associated + /// with the session, return that instance. This method never returns an uninitialized + /// instance. Obtain the specified lock mode if the instance exists. + /// + /// @param entityName the entity name + /// @param id an identifier + /// @param lockOptions contains the lock mode + /// + /// @return a persistent instance or null + /// + /// @deprecated This method will be removed. + /// Use [SessionFactory#createGraphForDynamicEntity(String)] + /// together with [#find(EntityGraph,Object,FindOption...)] + /// to load [dynamic entities][org.hibernate.metamodel.RepresentationMode#MAP]. @Deprecated(since = "7.0", forRemoval = true) Object get(String entityName, Object id, LockOptions lockOptions); - /** - * Obtain a lock on the given managed instance associated with this session, - * using the given {@linkplain LockOptions lock options}. - *

    - * This operation cascades to associated instances if the association is - * mapped with {@link org.hibernate.annotations.CascadeType#LOCK}. - * - * @param object a persistent instance associated with this session - * @param lockOptions the lock options - * - * @since 6.2 - * - * @deprecated This method will be removed. - * Use {@linkplain #lock(Object, LockModeType, LockOption...)} instead - */ + /// Obtain a lock on the given managed instance associated with this session, + /// using the given [lock options][LockOptions]. + /// + /// This operation cascades to associated instances if the association is + /// mapped with [org.hibernate.annotations.CascadeType#LOCK]. + /// + /// @param object a persistent instance associated with this session + /// @param lockOptions the lock options + /// + /// @since 6.2 + /// + /// @deprecated This method will be removed. + /// Use [#lock(Object, LockModeType, LockOption...)] instead @Deprecated(since = "7.0", forRemoval = true) void lock(Object object, LockOptions lockOptions); - /** - * Reread the state of the given managed instance from the underlying database, - * obtaining the given {@link LockMode}. - * - * @param object a persistent instance associated with this session - * @param lockOptions contains the lock mode to use - * - * @deprecated This method will be removed. - * Use {@linkplain #refresh(Object, RefreshOption...)} instead - */ + /// Reread the state of the given managed instance from the underlying database, + /// obtaining the given [LockMode]. + /// + /// @param object a persistent instance associated with this session + /// @param lockOptions contains the lock mode to use + /// + /// @deprecated This method will be removed. + /// Use [#refresh(Object, RefreshOption...)] instead @Deprecated(since = "7.0", forRemoval = true) void refresh(Object object, LockOptions lockOptions); - /** - * Return the entity name for the given persistent entity. - *

    - * If the given entity is an uninitialized proxy, the proxy is initialized by - * side effect. - * - * @param object a persistent entity associated with this session - * - * @return the entity name - */ + /// Return the entity name for the given persistent entity. + /// + /// If the given entity is an uninitialized proxy, the proxy is initialized by + /// side effect. + /// + /// @param object a persistent entity associated with this session + /// + /// @return the entity name String getEntityName(Object object); - /** - * Return a reference to the persistent instance with the given class and identifier, - * making the assumption that the instance is still persistent in the database. This - * method never results in access to the underlying data store, and thus might return - * a proxy that is initialized on-demand, when a non-identifier method is accessed. - *

    - * Note that {@link Hibernate#createDetachedProxy(SessionFactory, Class, Object)} - * may be used to obtain a detached reference. - *

    - * It's sometimes necessary to narrow a reference returned by {@code getReference()} - * to a subtype of the given entity type. A direct Java typecast should never be used - * in this situation. Instead, the method {@link Hibernate#unproxy(Object, Class)} is - * the recommended way to narrow the type of a proxy object. Alternatively, a new - * reference may be obtained by simply calling {@code getReference()} again, passing - * the subtype. Either way, the narrowed reference will usually not be identical to - * the original reference, when the references are compared using the {@code ==} - * operator. - * - * @param entityType the entity type - * @param id the identifier of a persistent instance that exists in the database - * - * @return the persistent instance or proxy - */ + /// Return a reference to the persistent instance with the given class and identifier, + /// making the assumption that the instance is still persistent in the database. This + /// method never results in access to the underlying data store, and thus might return + /// a proxy that is initialized on-demand, when a non-identifier method is accessed. + /// + /// Note that [Hibernate#createDetachedProxy(SessionFactory,Class,Object)] + /// may be used to obtain a _detached_ reference. + /// + /// It's sometimes necessary to narrow a reference returned by `getReference()` + /// to a subtype of the given entity type. A direct Java typecast should never be used + /// in this situation. Instead, the method [Hibernate#unproxy(Object,Class)] is + /// the recommended way to narrow the type of a proxy object. Alternatively, a new + /// reference may be obtained by simply calling `getReference()` again, passing + /// the subtype. Either way, the narrowed reference will usually not be identical to + /// the original reference, when the references are compared using the `==` + /// operator. + /// + /// @param entityType the entity type + /// @param id the identifier of a persistent instance that exists in the database + /// + /// @return the persistent instance or proxy @Override T getReference(Class entityType, Object id); - /** - * Return a reference to the persistent instance of the given named entity with the - * given identifier, making the assumption that the instance is still persistent in - * the database. This method never results in access to the underlying data store, - * and thus might return a proxy that is initialized on-demand, when a non-identifier - * method is accessed. - * - * @param entityName the entity name - * @param id the identifier of a persistent instance that exists in the database - * - * @return the persistent instance or proxy - */ + /// Return a reference to the persistent instance of the given named entity with the + /// given identifier, making the assumption that the instance is still persistent in + /// the database. This method never results in access to the underlying data store, + /// and thus might return a proxy that is initialized on-demand, when a non-identifier + /// method is accessed. + /// + /// @param entityName the entity name + /// @param id the identifier of a persistent instance that exists in the database + /// + /// @return the persistent instance or proxy Object getReference(String entityName, Object id); - /** - * Return a reference to the persistent instance with the same identity as the given - * instance, which might be detached, making the assumption that the instance is still - * persistent in the database. This method never results in access to the underlying - * data store, and thus might return a proxy that is initialized on-demand, when a - * non-identifier method is accessed. - * - * @param object a detached persistent instance - * - * @return the persistent instance or proxy - * - * @since 6.0 - */ + /// Return a reference to the persistent instance with the same identity as the given + /// instance, which might be detached, making the assumption that the instance is still + /// persistent in the database. This method never results in access to the underlying + /// data store, and thus might return a proxy that is initialized on-demand, when a + /// non-identifier method is accessed. + /// + /// @param object a detached persistent instance + /// + /// @return the persistent instance or proxy + /// + /// @since 6.0 @Override T getReference(T object); - /** - * Create an {@link IdentifierLoadAccess} instance to retrieve an instance of the given - * entity type by its primary key. - * - * @param entityClass the entity type to be retrieved - * - * @return an instance of {@link IdentifierLoadAccess} for executing the lookup - * - * @throws HibernateException If the given class does not resolve as a mapped entity - * - * @deprecated This method will be removed. - * Use {@link #find(Class, Object, FindOption...)} instead. - * See {@link FindOption}. - */ + /// Create an [IdentifierLoadAccess] instance to retrieve an instance of the given + /// entity type by its primary key. + /// + /// @param entityClass the entity type to be retrieved + /// + /// @return an instance of [IdentifierLoadAccess] for executing the lookup + /// + /// @throws HibernateException If the given class does not resolve as a mapped entity + /// + /// @deprecated This method will be removed. + /// Use [#find(Class,Object,FindOption...)] instead. + /// See [FindOption]. @Deprecated(since = "7.1", forRemoval = true) IdentifierLoadAccess byId(Class entityClass); - /** - * Create an {@link IdentifierLoadAccess} instance to retrieve an instance of the named - * entity type by its primary key. - * - * @param entityName the entity name of the entity type to be retrieved - * - * @return an instance of {@link IdentifierLoadAccess} for executing the lookup - * - * @throws HibernateException If the given name does not resolve to a mapped entity - * - * @deprecated This method will be removed. - * Use {@link #find(String, Object, FindOption...)} instead. - * See {@link FindOption}. - */ + /// Create an [IdentifierLoadAccess] instance to retrieve an instance of the named + /// entity type by its primary key. + /// + /// @param entityName the entity name of the entity type to be retrieved + /// + /// @return an instance of [IdentifierLoadAccess] for executing the lookup + /// + /// @throws HibernateException If the given name does not resolve to a mapped entity + /// + /// @deprecated This method will be removed. + /// Use [#find(String,Object,FindOption...)] instead. + /// See [FindOption]. @Deprecated(since = "7.1", forRemoval = true) IdentifierLoadAccess byId(String entityName); - /** - * Create a {@link MultiIdentifierLoadAccess} instance to retrieve multiple instances - * of the given entity type by their primary key values, using batching. - * - * @param entityClass the entity type to be retrieved - * - * @return an instance of {@link MultiIdentifierLoadAccess} for executing the lookup - * - * @throws HibernateException If the given class does not resolve as a mapped entity - * - * @see #findMultiple(Class, List, FindOption...) - * - * @deprecated Use {@link #findMultiple(Class, List, FindOption...)} instead. - */ + /// Create a [MultiIdentifierLoadAccess] instance to retrieve multiple instances + /// of the given entity type by their primary key values, using batching. + /// + /// @param entityClass the entity type to be retrieved + /// + /// @return an instance of [MultiIdentifierLoadAccess] for executing the lookup + /// + /// @throws HibernateException If the given class does not resolve as a mapped entity + /// + /// @see #findMultiple(Class, List, FindOption...) + /// + /// @deprecated Use [#findMultiple(Class,List,FindOption...)] instead. @Deprecated(since = "7.2", forRemoval = true) MultiIdentifierLoadAccess byMultipleIds(Class entityClass); - /** - * Create a {@link MultiIdentifierLoadAccess} instance to retrieve multiple instances - * of the named entity type by their primary key values, using batching. - * - * @param entityName the entity name of the entity type to be retrieved - * - * @return an instance of {@link MultiIdentifierLoadAccess} for executing the lookup - * - * @throws HibernateException If the given name does not resolve to a mapped entity - * - * @deprecated Use {@link #findMultiple(EntityGraph, List, FindOption...)} instead, - * with {@linkplain SessionFactory#createGraphForDynamicEntity(String)}. - */ + /// Create a [MultiIdentifierLoadAccess] instance to retrieve multiple instances + /// of the named entity type by their primary key values, using batching. + /// + /// @param entityName the entity name of the entity type to be retrieved + /// + /// @return an instance of [MultiIdentifierLoadAccess] for executing the lookup + /// + /// @throws HibernateException If the given name does not resolve to a mapped entity + /// + /// @deprecated Use [#findMultiple(EntityGraph,List,FindOption...)] instead, + /// with {@linkplain SessionFactory#createGraphForDynamicEntity(String)}. @Deprecated(since = "7.2", forRemoval = true) MultiIdentifierLoadAccess byMultipleIds(String entityName); - /** - * Create a {@link NaturalIdLoadAccess} instance to retrieve an instance of the given - * entity type by its {@linkplain org.hibernate.annotations.NaturalId natural id}, - * which may be a composite natural id. The entity must have at least one attribute - * annotated {@link org.hibernate.annotations.NaturalId}. - * - * @param entityClass the entity type to be retrieved - * - * @return an instance of {@link NaturalIdLoadAccess} for executing the lookup - * - * @throws HibernateException If the given class does not resolve as a mapped entity, - * or if the entity does not declare a natural id - */ + /// Create a [NaturalIdLoadAccess] instance to retrieve an instance of the given + /// entity type by its {@linkplain org.hibernate.annotations.NaturalId natural id}, + /// which may be a composite natural id. The entity must have at least one attribute + /// annotated [org.hibernate.annotations.NaturalId]. + /// + /// @param entityClass the entity type to be retrieved + /// + /// @return an instance of [NaturalIdLoadAccess] for executing the lookup + /// + /// @throws HibernateException If the given class does not resolve as a mapped entity, + /// or if the entity does not declare a natural id + /// + /// @deprecated (since 7.3) : Use {@linkplain #find} with [KeyType#NATURAL] instead. + @Deprecated NaturalIdLoadAccess byNaturalId(Class entityClass); - /** - * Create a {@link NaturalIdLoadAccess} instance to retrieve an instance of the named - * entity type by its {@linkplain org.hibernate.annotations.NaturalId natural id}, - * which may be a composite natural id. The entity must have at least one attribute - * annotated {@link org.hibernate.annotations.NaturalId}. - * - * @param entityName the entity name of the entity type to be retrieved - * - * @return an instance of {@link NaturalIdLoadAccess} for executing the lookup - * - * @throws HibernateException If the given name does not resolve to a mapped entity, - * or if the entity does not declare a natural id - */ + /// Create a [NaturalIdLoadAccess] instance to retrieve an instance of the named + /// entity type by its {@linkplain org.hibernate.annotations.NaturalId natural id}, + /// which may be a composite natural id. The entity must have at least one attribute + /// annotated [org.hibernate.annotations.NaturalId]. + /// + /// @param entityName the entity name of the entity type to be retrieved + /// + /// @return an instance of [NaturalIdLoadAccess] for executing the lookup + /// + /// @throws HibernateException If the given name does not resolve to a mapped entity, + /// or if the entity does not declare a natural id + /// + /// @deprecated (since 7.3) : Use {@linkplain #find} with [KeyType#NATURAL] instead. + @Deprecated NaturalIdLoadAccess byNaturalId(String entityName); - /** - * Create a {@link SimpleNaturalIdLoadAccess} instance to retrieve an instance of the - * given entity type by its {@linkplain org.hibernate.annotations.NaturalId natural id}, - * which must be a simple (non-composite) value. The entity must have exactly one - * attribute annotated {@link org.hibernate.annotations.NaturalId}. - * - * @param entityClass the entity type to be retrieved - * - * @return an instance of {@link SimpleNaturalIdLoadAccess} for executing the lookup - * - * @throws HibernateException If the given class does not resolve as a mapped entity, - * or if the entity does not declare a natural id - */ + /// Create a [SimpleNaturalIdLoadAccess] instance to retrieve an instance of the + /// given entity type by its {@linkplain org.hibernate.annotations.NaturalId natural id}, + /// which must be a simple (non-composite) value. The entity must have exactly one + /// attribute annotated [org.hibernate.annotations.NaturalId]. + /// + /// @param entityClass the entity type to be retrieved + /// + /// @return an instance of [SimpleNaturalIdLoadAccess] for executing the lookup + /// + /// @throws HibernateException If the given class does not resolve as a mapped entity, + /// or if the entity does not declare a natural id + /// + /// @deprecated (since 7.3) : Use {@linkplain #find} with [KeyType#NATURAL] instead. + @Deprecated SimpleNaturalIdLoadAccess bySimpleNaturalId(Class entityClass); - /** - * Create a {@link SimpleNaturalIdLoadAccess} instance to retrieve an instance of the - * named entity type by its {@linkplain org.hibernate.annotations.NaturalId natural id}, - * which must be a simple (non-composite) value. The entity must have exactly one - * attribute annotated {@link org.hibernate.annotations.NaturalId}. - * - * @param entityName the entity name of the entity type to be retrieved - * - * @return an instance of {@link SimpleNaturalIdLoadAccess} for executing the lookup - * - * @throws HibernateException If the given name does not resolve to a mapped entity, - * or if the entity does not declare a natural id - */ + /// Create a [SimpleNaturalIdLoadAccess] instance to retrieve an instance of the + /// named entity type by its {@linkplain org.hibernate.annotations.NaturalId natural id}, + /// which must be a simple (non-composite) value. The entity must have exactly one + /// attribute annotated [org.hibernate.annotations.NaturalId]. + /// + /// @param entityName the entity name of the entity type to be retrieved + /// + /// @return an instance of [SimpleNaturalIdLoadAccess] for executing the lookup + /// + /// @throws HibernateException If the given name does not resolve to a mapped entity, + /// or if the entity does not declare a natural id + /// + /// @deprecated (since 7.3) : Use {@linkplain #find} with [KeyType#NATURAL] instead. + @Deprecated SimpleNaturalIdLoadAccess bySimpleNaturalId(String entityName); - /** - * Create a {@link MultiIdentifierLoadAccess} instance to retrieve multiple instances - * of the given entity type by their by {@linkplain org.hibernate.annotations.NaturalId - * natural id} values, using batching. - * - * @param entityClass the entity type to be retrieved - * - * @return an instance of {@link NaturalIdMultiLoadAccess} for executing the lookup - * - * @throws HibernateException If the given class does not resolve as a mapped entity, - * or if the entity does not declare a natural id - */ + /// Create a [MultiIdentifierLoadAccess] instance to retrieve multiple instances + /// of the given entity type by their by {@linkplain org.hibernate.annotations.NaturalId + /// natural id} values, using batching. + /// + /// @param entityClass the entity type to be retrieved + /// + /// @return an instance of [NaturalIdMultiLoadAccess] for executing the lookup + /// + /// @throws HibernateException If the given class does not resolve as a mapped entity, + /// or if the entity does not declare a natural id + /// + /// @deprecated (since 7.3) : Use {@linkplain #findMultiple} with [KeyType#NATURAL] instead. + /// + @Deprecated NaturalIdMultiLoadAccess byMultipleNaturalId(Class entityClass); - /** - * Create a {@link MultiIdentifierLoadAccess} instance to retrieve multiple instances - * of the named entity type by their by {@linkplain org.hibernate.annotations.NaturalId - * natural id} values, using batching. - * - * @param entityName the entity name of the entity type to be retrieved - * - * @return an instance of {@link NaturalIdMultiLoadAccess} for executing the lookup - * - * @throws HibernateException If the given name does not resolve to a mapped entity, - * or if the entity does not declare a natural id - */ + /// Create a [MultiIdentifierLoadAccess] instance to retrieve multiple instances + /// of the named entity type by their by {@linkplain org.hibernate.annotations.NaturalId + /// natural id} values, using batching. + /// + /// @param entityName the entity name of the entity type to be retrieved + /// + /// @return an instance of [NaturalIdMultiLoadAccess] for executing the lookup + /// + /// @throws HibernateException If the given name does not resolve to a mapped entity, + /// or if the entity does not declare a natural id + /// + /// @deprecated (since 7.3) : Use {@linkplain #findMultiple} with [KeyType#NATURAL] instead. + @Deprecated NaturalIdMultiLoadAccess byMultipleNaturalId(String entityName); - /** - * Get the {@linkplain SessionStatistics statistics} for this session. - * - * @return the session statistics being collected for this session - */ + /// Get the [statistics][SessionStatistics] for this session. + /// + /// @return the session statistics being collected for this session SessionStatistics getStatistics(); - /** - * Is the specified entity or proxy read-only? - *

    - * To get the default read-only/modifiable setting used for - * entities and proxies that are loaded into the session use - * {@link #isDefaultReadOnly()} - * - * @see #isDefaultReadOnly() - * - * @param entityOrProxy an entity or proxy - * @return {@code true} if the entity or proxy is read-only, - * {@code false} if the entity or proxy is modifiable. - */ + /// Is the specified entity or proxy read-only? + /// + /// To get the default read-only/modifiable setting used for + /// entities and proxies that are loaded into the session use + /// [#isDefaultReadOnly()] + /// + /// @see #isDefaultReadOnly() + /// + /// @param entityOrProxy an entity or proxy + /// @return `true` if the entity or proxy is read-only, + /// `false` if the entity or proxy is modifiable. boolean isReadOnly(Object entityOrProxy); - /** - * Set an unmodified persistent object to read-only mode, or a read-only - * object to modifiable mode. In read-only mode, no snapshot is maintained, - * the instance is never dirty-checked, and mutations to the fields of the - * entity are not made persistent. - *

    - * If the entity or proxy already has the specified read-only/modifiable - * setting, then this method does nothing. - *

    - * To set the default read-only/modifiable setting used for all entities - * and proxies that are loaded into the session use - * {@link #setDefaultReadOnly(boolean)}. - *

    - * To override the default read-only mode of the current session for - * all entities and proxies returned by a given {@code Query}, use - * {@link Query#setReadOnly(boolean)}. - *

    - * Every instance of an {@linkplain org.hibernate.annotations.Immutable - * immutable} entity is loaded in read-only mode. An immutable entity may - * not be set to modifiable. - * - * @see #setDefaultReadOnly(boolean) - * @see Query#setReadOnly(boolean) - * @see IdentifierLoadAccess#withReadOnly(boolean) - * @see org.hibernate.annotations.Immutable - * - * @param entityOrProxy an entity or proxy - * @param readOnly {@code true} if the entity or proxy should be made read-only; - * {@code false} if the entity or proxy should be made modifiable - * - * @throws IllegalStateException if an immutable entity is set to modifiable - */ + /// Set an unmodified persistent object to read-only mode, or a read-only + /// object to modifiable mode. In read-only mode, no snapshot is maintained, + /// the instance is never dirty-checked, and mutations to the fields of the + /// entity are not made persistent. + /// + /// If the entity or proxy already has the specified read-only/modifiable + /// setting, then this method does nothing. + /// + /// To set the default read-only/modifiable setting used for all entities + /// and proxies that are loaded into the session use + /// [#setDefaultReadOnly(boolean)]. + /// + /// To override the default read-only mode of the current session for + /// all entities and proxies returned by a given `Query`, use + /// [Query#setReadOnly(boolean)]. + /// + /// Every instance of an [immutable][org.hibernate.annotations.Immutable] + /// entity is loaded in read-only mode. An immutable entity may + /// not be set to modifiable. + /// + /// @see #setDefaultReadOnly(boolean) + /// @see Query#setReadOnly(boolean) + /// @see IdentifierLoadAccess#withReadOnly(boolean) + /// @see org.hibernate.annotations.Immutable + /// + /// @param entityOrProxy an entity or proxy + /// @param readOnly `true` if the entity or proxy should be made read-only; + /// `false` if the entity or proxy should be made modifiable + /// + /// @throws IllegalStateException if an immutable entity is set to modifiable void setReadOnly(Object entityOrProxy, boolean readOnly); - /** - * Is the {@link org.hibernate.annotations.FetchProfile fetch profile} - * with the given name enabled in this session? - * - * @param name the name of the profile - * @return True if fetch profile is enabled; false if not. - * - * @throws UnknownProfileException Indicates that the given name does not - * match any known fetch profile names - * - * @see org.hibernate.annotations.FetchProfile - */ + /// Is the [fetch profile][org.hibernate.annotations.FetchProfile] + /// with the given name enabled in this session? + /// + /// @param name the name of the profile + /// @return True if fetch profile is enabled; false if not. + /// + /// @throws UnknownProfileException Indicates that the given name does not + /// match any known fetch profile names + /// + /// @see org.hibernate.annotations.FetchProfile boolean isFetchProfileEnabled(String name) throws UnknownProfileException; - /** - * Enable the {@link org.hibernate.annotations.FetchProfile fetch profile} - * with the given name in this session. If the requested fetch profile is - * already enabled, the call has no effect. - * - * @param name the name of the fetch profile to be enabled - * - * @throws UnknownProfileException Indicates that the given name does not - * match any known fetch profile names - * - * @see org.hibernate.annotations.FetchProfile - */ + /// Enable the [fetch profile][org.hibernate.annotations.FetchProfile] + /// with the given name in this session. If the requested fetch profile is + /// already enabled, the call has no effect. + /// + /// @param name the name of the fetch profile to be enabled + /// + /// @throws UnknownProfileException Indicates that the given name does not + /// match any known fetch profile names + /// + /// @see org.hibernate.annotations.FetchProfile void enableFetchProfile(String name) throws UnknownProfileException; - /** - * Disable the {@link org.hibernate.annotations.FetchProfile fetch profile} - * with the given name in this session. If the requested fetch profile is - * not currently enabled, the call has no effect. - * - * @param name the name of the fetch profile to be disabled - * - * @throws UnknownProfileException Indicates that the given name does not - * match any known fetch profile names - * - * @see org.hibernate.annotations.FetchProfile - */ + /// Disable the [fetch profile][org.hibernate.annotations.FetchProfile] + /// with the given name in this session. If the requested fetch profile is + /// not currently enabled, the call has no effect. + /// + /// @param name the name of the fetch profile to be disabled + /// + /// @throws UnknownProfileException Indicates that the given name does not + /// match any known fetch profile names + /// + /// @see org.hibernate.annotations.FetchProfile void disableFetchProfile(String name) throws UnknownProfileException; - /** - * Obtain a {@linkplain LobHelper} for instances of {@link java.sql.Blob} - * and {@link java.sql.Clob}. - * - * @return an instance of {@link LobHelper} - * - * @deprecated Use {@link Hibernate#getLobHelper()} instead. - */ + /// Obtain a {@linkplain LobHelper} for instances of [java.sql.Blob] + /// and [java.sql.Clob]. + /// + /// @return an instance of [LobHelper] + /// + /// @deprecated Use [#getLobHelper()] instead. @Deprecated(since="7.0", forRemoval = true) LobHelper getLobHelper(); - /** - * Obtain the collection of all managed entities which belong to this - * persistence context. - * - * @since 7.0 - */ + /// Obtain the collection of all managed entities which belong to this + /// persistence context. + /// + /// @since 7.0 @Incubating Collection getManagedEntities(); - /** - * Obtain a collection of all managed instances of the entity type with the - * given entity name which belong to this persistence context. - * - * @since 7.0 - */ + /// Obtain a collection of all managed instances of the entity type with the + /// given entity name which belong to this persistence context. + /// + /// @since 7.0 @Incubating Collection getManagedEntities(String entityName); - /** - * Obtain a collection of all managed entities of the given type which belong - * to this persistence context. This operation is not polymorphic, and does - * not return instances of subtypes of the given entity type. - * - * @since 7.0 - */ + /// Obtain a collection of all managed entities of the given type which belong + /// to this persistence context. This operation is not polymorphic, and does + /// not return instances of subtypes of the given entity type. + /// + /// @since 7.0 @Incubating Collection getManagedEntities(Class entityType); - /** - * Obtain a collection of all managed entities of the given type which belong - * to this persistence context. This operation is not polymorphic, and does - * not return instances of subtypes of the given entity type. - * - * @since 7.0 - */ + /// Obtain a collection of all managed entities of the given type which belong + /// to this persistence context. This operation is not polymorphic, and does + /// not return instances of subtypes of the given entity type. + /// + /// @since 7.0 @Incubating Collection getManagedEntities(EntityType entityType); - /** - * Add one or more listeners to the Session - * - * @param listeners the listener(s) to add - */ + /// Add one or more listeners to the Session + /// + /// @param listeners the listener(s) to add void addEventListeners(SessionEventListener... listeners); - /** - * Set a hint. The hints understood by Hibernate are enumerated by - * {@link org.hibernate.jpa.AvailableHints}. - * - * @see org.hibernate.jpa.HibernateHints - * @see org.hibernate.jpa.SpecHints - * - * @apiNote Hints are a - * {@linkplain jakarta.persistence.EntityManager#setProperty - * JPA-standard way} to control provider-specific behavior of the - * {@code EntityManager}. Clients of the native API defined by - * Hibernate should make use of type-safe operations of this - * interface. For example, {@link #enableFetchProfile(String)} - * should be used in preference to the hint - * {@link org.hibernate.jpa.HibernateHints#HINT_FETCH_PROFILE}. - */ + /// Set a hint. The hints understood by Hibernate are enumerated by + /// [org.hibernate.jpa.AvailableHints]. + /// + /// @see org.hibernate.jpa.HibernateHints + /// @see org.hibernate.jpa.SpecHints + /// + /// @apiNote Hints are a [JPA-standard way][jakarta.persistence.EntityManager#setProperty] + /// to control provider-specific behavior of the + /// [EntityManager]. Clients of the native API defined by + /// Hibernate should make use of type-safe operations of this + /// interface. For example, [#enableFetchProfile(String)] + /// should be used in preference to the hint [org.hibernate.jpa.HibernateHints#HINT_FETCH_PROFILE]. @Override void setProperty(String propertyName, Object value); - /** - * Create a new mutable instance of {@link EntityGraph}, with only - * a root node, allowing programmatic definition of the graph from - * scratch. - * - * @param rootType The root entity of the graph - * - * @see #find(EntityGraph, Object, FindOption...) - * @see org.hibernate.query.SelectionQuery#setEntityGraph(EntityGraph, org.hibernate.graph.GraphSemantic) - * @see org.hibernate.graph.EntityGraphs#createGraph(jakarta.persistence.metamodel.EntityType) - */ + /// Create a new mutable instance of [EntityGraph], with only + /// a root node, allowing programmatic definition of the graph from + /// scratch. + /// + /// @param rootType The root entity of the graph + /// + /// @see #find(EntityGraph, Object, FindOption...) + /// @see org.hibernate.query.SelectionQuery#setEntityGraph(EntityGraph, org.hibernate.graph.GraphSemantic) + /// @see org.hibernate.graph.EntityGraphs#createGraph(jakarta.persistence.metamodel.EntityType) @Override RootGraph createEntityGraph(Class rootType); - /** - * Create a new mutable instance of {@link EntityGraph}, based on - * a predefined {@linkplain jakarta.persistence.NamedEntityGraph - * named entity graph}, allowing customization of the graph, or - * return {@code null} if there is no predefined graph with the - * given name. - * - * @param graphName The name of the predefined named entity graph - * - * @apiNote This method returns {@code RootGraph}, requiring an - * unchecked typecast before use. It's cleaner to obtain a graph using - * {@link #createEntityGraph(Class, String)} instead. - * - * @see #find(EntityGraph, Object, FindOption...) - * @see org.hibernate.query.SelectionQuery#setEntityGraph(EntityGraph, org.hibernate.graph.GraphSemantic) - * @see jakarta.persistence.EntityManagerFactory#addNamedEntityGraph(String, EntityGraph) - */ + /// Create a new mutable instance of [EntityGraph], based on + /// a predefined [named entity graph][jakarta.persistence.NamedEntityGraph], + /// allowing customization of the graph, or return `null` if there is no + /// predefined graph with the given name. + /// + /// @param graphName The name of the predefined named entity graph + /// + /// @apiNote This method returns `RootGraph`, requiring an + /// unchecked typecast before use. It's cleaner to obtain a graph using + /// [#createEntityGraph(Class,String)] instead. + /// + /// @see #find(EntityGraph, Object, FindOption...) + /// @see org.hibernate.query.SelectionQuery#setEntityGraph(EntityGraph, org.hibernate.graph.GraphSemantic) + /// @see jakarta.persistence.EntityManagerFactory#addNamedEntityGraph(String, EntityGraph) @Override RootGraph createEntityGraph(String graphName); - /** - * Obtain an immutable reference to a predefined - * {@linkplain jakarta.persistence.NamedEntityGraph named entity graph} - * or return {@code null} if there is no predefined graph with the given - * name. - * - * @param graphName The name of the predefined named entity graph - * - * @apiNote This method returns {@code RootGraph}, requiring an - * unchecked typecast before use. It's cleaner to obtain a graph using - * the static metamodel for the class which defines the graph, or by - * calling {@link SessionFactory#getNamedEntityGraphs(Class)} instead. - * - * @see #find(EntityGraph, Object, FindOption...) - * @see org.hibernate.query.SelectionQuery#setEntityGraph(EntityGraph, org.hibernate.graph.GraphSemantic) - * @see jakarta.persistence.EntityManagerFactory#addNamedEntityGraph(String, EntityGraph) - */ + /// Obtain an immutable reference to a predefined + /// [named entity graph][jakarta.persistence.NamedEntityGraph] + /// or return `null` if there is no predefined graph with the given + /// name. + /// + /// @param graphName The name of the predefined named entity graph + /// + /// @apiNote This method returns `RootGraph`, requiring an + /// unchecked typecast before use. It's cleaner to obtain a graph using + /// the static metamodel for the class which defines the graph, or by + /// calling [SessionFactory#getNamedEntityGraphs(Class)] instead. + /// + /// @see #find(EntityGraph, Object, FindOption...) + /// @see org.hibernate.query.SelectionQuery#setEntityGraph(EntityGraph, org.hibernate.graph.GraphSemantic) + /// @see jakarta.persistence.EntityManagerFactory#addNamedEntityGraph(String, EntityGraph) @Override RootGraph getEntityGraph(String graphName); - /** - * Retrieve all named {@link EntityGraph}s with the given root entity type. - * - * @see jakarta.persistence.EntityManagerFactory#getNamedEntityGraphs(Class) - * @see jakarta.persistence.EntityManagerFactory#addNamedEntityGraph(String, EntityGraph) - */ + /// Retrieve all named [EntityGraph]s with the given root entity type. + /// + /// @see jakarta.persistence.EntityManagerFactory#getNamedEntityGraphs(Class) + /// @see jakarta.persistence.EntityManagerFactory#addNamedEntityGraph(String, EntityGraph) @Override List> getEntityGraphs(Class entityClass); - // The following overrides should not be necessary, + // The following overrides should not be necessary // and are only needed to work around a bug in IntelliJ @Override @@ -1527,6 +1374,9 @@ public interface Session extends SharedSessionContract, EntityManager { @Override @Deprecated @SuppressWarnings("rawtypes") Query createQuery(String queryString); + @Override @Deprecated @SuppressWarnings("rawtypes") + NativeQuery createNativeQuery(String queryString); + @Override Query createNamedQuery(String name, Class resultClass); @@ -1536,19 +1386,15 @@ public interface Session extends SharedSessionContract, EntityManager { @Override Query createQuery(CriteriaQuery criteriaQuery); - /** - * Create a {@link Query} for the given JPA {@link CriteriaDelete}. - * - * @deprecated use {@link #createMutationQuery(CriteriaDelete)} - */ + /// Create a [Query] for the given JPA [CriteriaDelete]. + /// + /// @deprecated use [#createMutationQuery(CriteriaDelete)] @Override @Deprecated(since = "6.0") @SuppressWarnings("rawtypes") Query createQuery(CriteriaDelete deleteQuery); - /** - * Create a {@link Query} for the given JPA {@link CriteriaUpdate}. - * - * @deprecated use {@link #createMutationQuery(CriteriaUpdate)} - */ + /// Create a [Query] for the given JPA [CriteriaUpdate]. + /// + /// @deprecated use [#createMutationQuery(CriteriaUpdate)] @Override @Deprecated(since = "6.0") @SuppressWarnings("rawtypes") Query createQuery(CriteriaUpdate updateQuery); diff --git a/hibernate-core/src/main/java/org/hibernate/SessionBuilder.java b/hibernate-core/src/main/java/org/hibernate/SessionBuilder.java index 87037bb8b111..d38401690fb2 100644 --- a/hibernate-core/src/main/java/org/hibernate/SessionBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/SessionBuilder.java @@ -5,9 +5,11 @@ package org.hibernate; import java.sql.Connection; +import java.time.Instant; import java.util.TimeZone; import java.util.function.UnaryOperator; +import org.hibernate.cfg.StateManagementSettings; import org.hibernate.engine.creation.CommonBuilder; import org.hibernate.resource.jdbc.spi.PhysicalConnectionHandlingMode; import org.hibernate.resource.jdbc.spi.StatementInspector; @@ -244,4 +246,21 @@ default Session open() { * @since 7.2 */ SessionBuilder subselectFetchEnabled(boolean subselectFetchEnabled); + + /** + * Specify the instant for reading + * {@linkplain org.hibernate.annotations.Temporal temporal} entity data. + * Instances of temporal entities retrieved in the session represent the + * revisions effective at the given instant. + */ + SessionBuilder asOf(Instant instant); + + /** + * Specify the + * {@linkplain StateManagementSettings#TRANSACTION_ID_SUPPLIER + * transaction id} for reading {@linkplain org.hibernate.annotations.Temporal + * temporal} entity data. Instances of temporal entities retrieved in the + * session represent revisions effective at the end of the given transaction. + */ + SessionBuilder atTransaction(Object transactionId); } diff --git a/hibernate-core/src/main/java/org/hibernate/SessionCheckMode.java b/hibernate-core/src/main/java/org/hibernate/SessionCheckMode.java index 4e9b21337cdd..22ab1ac0e393 100644 --- a/hibernate-core/src/main/java/org/hibernate/SessionCheckMode.java +++ b/hibernate-core/src/main/java/org/hibernate/SessionCheckMode.java @@ -13,16 +13,16 @@ /** * Indicates whether the persistence context should be checked for entities * matching the identifiers to be loaded -

      - *
    • Entities which are in a managed state are not re-loaded from the database. - * those identifiers are removed from the SQL restriction sent to the database. - *
    • Entities which are in a removed state are {@linkplain RemovalsMode#REPLACE excluded} + *
    • Entities which are in a managed state are not reloaded from the database. + * Those identifiers are removed from the SQL restriction sent to the database. + *
    • Entities which are in a removed state are {@linkplain RemovalsMode#REPLACE replaced with null} * from the result by default, but can be {@linkplain RemovalsMode#INCLUDE included} if desired. *
    - *

    - * The default is {@link #DISABLED} + *

    + * The default is {@link #DISABLED}. * - * @see org.hibernate.Session#findMultiple(Class, List , FindOption...) - * @see org.hibernate.Session#findMultiple(EntityGraph, List , FindOption...) + * @see org.hibernate.Session#findMultiple(Class, List, FindOption...) + * @see org.hibernate.Session#findMultiple(EntityGraph, List, FindOption...) * * @since 7.2 */ diff --git a/hibernate-core/src/main/java/org/hibernate/SessionFactory.java b/hibernate-core/src/main/java/org/hibernate/SessionFactory.java index c03e63bd2ad7..2991a8b956ef 100644 --- a/hibernate-core/src/main/java/org/hibernate/SessionFactory.java +++ b/hibernate-core/src/main/java/org/hibernate/SessionFactory.java @@ -13,7 +13,6 @@ import jakarta.persistence.TypedQueryReference; import org.hibernate.boot.spi.SessionFactoryOptions; import org.hibernate.engine.spi.FilterDefinition; -import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.graph.GraphParser; import org.hibernate.graph.InvalidGraphException; import org.hibernate.graph.RootGraph; @@ -46,7 +45,8 @@ * Typically, a program has a single {@link SessionFactory} instance, and must * obtain a new {@link Session} instance from the factory each time it services * a client request. It is then also responsible for {@linkplain Session#close() - * destroying} the session at the end of the client request. + * destroying} the session at the end of the client request. An instance of + * {@code Session} must never be shared between multiple threads. *

    * The {@link #inSession} and {@link #inTransaction} methods provide a convenient * way to obtain a session, with or without starting a transaction, and have it @@ -108,7 +108,7 @@ * SingularAttribute<Book,String>}. *

*

- * Use of these statically-typed metamodel references is the preferred way of + * Use of these statically typed metamodel references is the preferred way of * working with the {@linkplain jakarta.persistence.criteria.CriteriaBuilder * criteria query API}, and with {@linkplain EntityGraph}s. *

@@ -266,7 +266,7 @@ default void inSession(Consumer action) { * @since 6.3 */ default void inStatelessSession(Consumer action) { - try ( StatelessSession session = openStatelessSession() ) { + try ( var session = openStatelessSession() ) { action.accept( session ); } } @@ -308,7 +308,7 @@ default void inStatelessTransaction(Consumer action) { * @see #fromTransaction(Function) */ default R fromSession(Function action) { - try ( Session session = openSession() ) { + try ( var session = openSession() ) { return action.apply( session ); } } @@ -331,7 +331,7 @@ default R fromSession(Function action) { * @since 6.3 */ default R fromStatelessSession(Function action) { - try ( StatelessSession session = openStatelessSession() ) { + try ( var session = openStatelessSession() ) { return action.apply( session ); } } @@ -522,9 +522,7 @@ default RootGraph createEntityGraph(Class entityType) { * * @since 7.0 */ - default RootGraph parseEntityGraph(Class rootEntityClass, CharSequence graphText) { - return GraphParser.parse( rootEntityClass, graphText.toString(), unwrap( SessionFactoryImplementor.class ) ); - } + RootGraph parseEntityGraph(Class rootEntityClass, CharSequence graphText); /** * Creates a {@link RootGraph} for the given {@code rootEntityName} and parses the graph @@ -543,9 +541,8 @@ default RootGraph parseEntityGraph(Class rootEntityClass, CharSequence * * @since 7.0 */ - default RootGraph parseEntityGraph(String rootEntityName, CharSequence graphText) { - return GraphParser.parse( rootEntityName, graphText.toString(), unwrap( SessionFactoryImplementor.class ) ); - } + @Incubating + RootGraph parseEntityGraph(String rootEntityName, CharSequence graphText); /** * Creates a {@link RootGraph} based on the passed string representation. Here, the @@ -560,9 +557,8 @@ default RootGraph parseEntityGraph(String rootEntityName, CharSequence gr * * @since 7.0 */ - default RootGraph parseEntityGraph(CharSequence graphText) { - return GraphParser.parse( graphText.toString(), unwrap( SessionFactoryImplementor.class ) ); - } + @Incubating + RootGraph parseEntityGraph(CharSequence graphText); /** * Obtain the set of names of all {@linkplain org.hibernate.annotations.FilterDef diff --git a/hibernate-core/src/main/java/org/hibernate/SharedSessionBuilder.java b/hibernate-core/src/main/java/org/hibernate/SharedSessionBuilder.java index 523ee1226649..6b0cc6d2024f 100644 --- a/hibernate-core/src/main/java/org/hibernate/SharedSessionBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/SharedSessionBuilder.java @@ -9,6 +9,7 @@ import org.hibernate.resource.jdbc.spi.StatementInspector; import java.sql.Connection; +import java.time.Instant; import java.util.TimeZone; import java.util.function.UnaryOperator; @@ -26,7 +27,6 @@ * and therefore also the JDBC transaction, should be shared from parent * to child. * - *

*

  * try (var childSession
  *          = session.sessionWithOptions()
@@ -36,7 +36,6 @@
  *     ...
  * }
  * 
- *

* On the other hand, when JTA transaction management is used, all sessions * execute within the same transaction. Typically, connection sharing is * handled automatically by the JTA-enabled {@link javax.sql.DataSource}. @@ -48,6 +47,11 @@ * @see SessionBuilder */ public interface SharedSessionBuilder extends SessionBuilder, CommonSharedBuilder { + /** + * Open the session. + */ + @Override + Session open(); @Override SharedSessionBuilder connection(); @@ -93,6 +97,12 @@ public interface SharedSessionBuilder extends SessionBuilder, CommonSharedBuilde */ SharedSessionBuilder autoClose(); + @Override + SharedSessionBuilder asOf(Instant instant); + + @Override + SharedSessionBuilder atTransaction(Object transactionId); + @Override @Deprecated SharedSessionBuilder statementInspector(StatementInspector statementInspector); diff --git a/hibernate-core/src/main/java/org/hibernate/SharedStatelessSessionBuilder.java b/hibernate-core/src/main/java/org/hibernate/SharedStatelessSessionBuilder.java index 703a748e07cf..2268703011aa 100644 --- a/hibernate-core/src/main/java/org/hibernate/SharedStatelessSessionBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/SharedStatelessSessionBuilder.java @@ -22,8 +22,7 @@ * and therefore also the JDBC transaction, should be shared from parent * to child. * - *

- *

+ * 
{@code
  * try (var statelessSession
  *          = session.statelessWithOptions()
  *                  .connection() // share the JDBC connection
@@ -31,8 +30,8 @@
  *                  .openStatelessSession()) {
  *     ...
  * }
- * 
- *

+ * }

+ * * On the other hand, when JTA transaction management is used, all sessions * execute within the same transaction. Typically, connection sharing is * handled automatically by the JTA-enabled {@link javax.sql.DataSource}. @@ -50,6 +49,7 @@ public interface SharedStatelessSessionBuilder extends StatelessSessionBuilder, /** * Open the stateless session. */ + @Override StatelessSession open(); @Override diff --git a/hibernate-core/src/main/java/org/hibernate/SimpleNaturalIdLoadAccess.java b/hibernate-core/src/main/java/org/hibernate/SimpleNaturalIdLoadAccess.java index 13130fd7c18e..d5583d08c018 100644 --- a/hibernate-core/src/main/java/org/hibernate/SimpleNaturalIdLoadAccess.java +++ b/hibernate-core/src/main/java/org/hibernate/SimpleNaturalIdLoadAccess.java @@ -18,10 +18,9 @@ * {@link org.hibernate.annotations.NaturalId @NaturalId}. If an * entity has multiple attributes annotated {@code @NaturalId}, then * {@link NaturalIdLoadAccess} should be used instead. - *

- *

+ * 
{@code
  * Book book = session.bySimpleNaturalId(Book.class).load(isbn);
- * 
+ * }
* * @author Eric Dalquist * @author Steve Ebersole @@ -29,7 +28,10 @@ * @see Session#bySimpleNaturalId(Class) * @see org.hibernate.annotations.NaturalId * @see NaturalIdLoadAccess + * + * @deprecated (since 7.3) Use {@linkplain Session#find} with {@link KeyType#NATURAL} instead. */ +@Deprecated public interface SimpleNaturalIdLoadAccess { /** diff --git a/hibernate-core/src/main/java/org/hibernate/StatelessSession.java b/hibernate-core/src/main/java/org/hibernate/StatelessSession.java index 17a968aea794..d3b320a814e5 100644 --- a/hibernate-core/src/main/java/org/hibernate/StatelessSession.java +++ b/hibernate-core/src/main/java/org/hibernate/StatelessSession.java @@ -31,8 +31,8 @@ *

* Furthermore, the basic operations of a stateless session do not have * corresponding {@linkplain jakarta.persistence.CascadeType cascade types}, - * and so an operation performed via a stateless session never cascades to - * associated instances. + * and so an operation performed on one entity via a stateless session never + * cascades to associated entities. *

* The basic operations of a stateless session are {@link #get(Class, Object)}, * {@link #insert(Object)}, {@link #update(Object)}, {@link #delete(Object)}, @@ -97,7 +97,9 @@ public interface StatelessSession extends SharedSessionContract { Object insert(Object entity); /** - * Insert multiple records. + * Insert multiple records in the same order as the entity + * instances representing the new records occur in the given + * list. * * @param entities a list of transient instances to be inserted * @@ -130,7 +132,9 @@ public interface StatelessSession extends SharedSessionContract { void update(Object entity); /** - * Update multiple records. + * Update multiple records in the same order as the entity + * instances representing the records occur in the given + * list. * * @param entities a list of detached instances to be updated * @@ -161,7 +165,9 @@ public interface StatelessSession extends SharedSessionContract { void delete(Object entity); /** - * Delete multiple records. + * Delete multiple records in the same order as the entity + * instances representing the records occur in the given + * list. * * @param entities a list of detached instances to be deleted * @@ -206,9 +212,11 @@ public interface StatelessSession extends SharedSessionContract { void upsert(Object entity); /** - * Perform an upsert, that is, to insert the record if it does - * not exist, or update the record if it already exists, for - * each given record. + * Upsert multiple records, that is, for a given record, + * insert the record if it does not exist or update the + * record if it already exists, in the same order as the + * entity instances representing the records occur in + * the given list. * * @param entities a list of detached instances and new * instances with assigned identifiers diff --git a/hibernate-core/src/main/java/org/hibernate/StatelessSessionBuilder.java b/hibernate-core/src/main/java/org/hibernate/StatelessSessionBuilder.java index 0e21fa9a74e2..cbbb8026b156 100644 --- a/hibernate-core/src/main/java/org/hibernate/StatelessSessionBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/StatelessSessionBuilder.java @@ -5,9 +5,11 @@ package org.hibernate; import java.sql.Connection; +import java.time.Instant; import java.util.TimeZone; import java.util.function.UnaryOperator; +import org.hibernate.cfg.StateManagementSettings; import org.hibernate.engine.creation.CommonBuilder; import org.hibernate.resource.jdbc.spi.StatementInspector; @@ -23,11 +25,13 @@ public interface StatelessSessionBuilder extends CommonBuilder { /** * Opens a session with the specified options. - * - * @return The session + * @see #open() */ StatelessSession openStatelessSession(); + @Override + StatelessSession open(); + @Override StatelessSessionBuilder connection(Connection connection); @@ -73,4 +77,24 @@ public interface StatelessSessionBuilder extends CommonBuilder { */ @Deprecated(since = "7.0") StatelessSessionBuilder statementInspector(StatementInspector statementInspector); + + /** + * Specify the instant for reading + * {@linkplain org.hibernate.annotations.Temporal temporal} entity data. + * Instances of temporal entities retrieved in the session will represent + * the revisions effective at the given instant. + */ + StatelessSessionBuilder asOf(Instant instant); + + /** + * Specify the + * {@linkplain StateManagementSettings#TRANSACTION_ID_SUPPLIER + * transaction id} for reading {@linkplain org.hibernate.annotations.Temporal + * temporal} entity data. Instances of temporal entities retrieved in the + * session will represent the revisions effective at the end of the given + * transaction. + * The given value should match the type returned by the configured + * transaction id supplier. + */ + StatelessSessionBuilder atTransaction(Object transactionId); } diff --git a/hibernate-core/src/main/java/org/hibernate/Timeouts.java b/hibernate-core/src/main/java/org/hibernate/Timeouts.java index ddeae2940271..c96315975244 100644 --- a/hibernate-core/src/main/java/org/hibernate/Timeouts.java +++ b/hibernate-core/src/main/java/org/hibernate/Timeouts.java @@ -146,4 +146,17 @@ static int fromHint(Object factoryHint) { } return Integer.parseInt( factoryHint.toString() ); } + + static Timeout fromHintTimeout(Object factoryHint) { + if ( factoryHint == null ) { + return WAIT_FOREVER; + } + if ( factoryHint instanceof Timeout timeout ) { + return timeout; + } + if ( factoryHint instanceof Integer number ) { + return Timeout.milliseconds( number ); + } + return Timeout.milliseconds( Integer.parseInt( factoryHint.toString() ) ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/action/internal/ActionLogging.java b/hibernate-core/src/main/java/org/hibernate/action/internal/ActionLogging.java index c305ea79a3f9..c7bbab5ce95f 100644 --- a/hibernate-core/src/main/java/org/hibernate/action/internal/ActionLogging.java +++ b/hibernate-core/src/main/java/org/hibernate/action/internal/ActionLogging.java @@ -5,6 +5,7 @@ package org.hibernate.action.internal; import java.lang.invoke.MethodHandles; +import java.util.Locale; import java.util.Set; import org.hibernate.Internal; @@ -34,7 +35,7 @@ public interface ActionLogging extends BasicLogger { String NAME = SubSystemLogging.BASE + ".action"; ActionLogging ACTION_LOGGER = Logger.getMessageLogger( - MethodHandles.lookup(), ActionLogging.class, NAME + MethodHandles.lookup(), ActionLogging.class, NAME, Locale.ROOT ); int NAMESPACE = 90032000; diff --git a/hibernate-core/src/main/java/org/hibernate/action/internal/EntityIdentityInsertAction.java b/hibernate-core/src/main/java/org/hibernate/action/internal/EntityIdentityInsertAction.java index cc2b6e5fc4cd..e68e637944e0 100644 --- a/hibernate-core/src/main/java/org/hibernate/action/internal/EntityIdentityInsertAction.java +++ b/hibernate-core/src/main/java/org/hibernate/action/internal/EntityIdentityInsertAction.java @@ -14,9 +14,11 @@ import org.hibernate.event.spi.PostInsertEventListener; import org.hibernate.event.spi.PreInsertEvent; import org.hibernate.generator.values.GeneratedValues; +import org.hibernate.metamodel.mapping.CompositeIdentifierMapping; +import org.hibernate.metamodel.mapping.EmbeddableMappingType; +import org.hibernate.metamodel.spi.EmbeddableInstantiator; import org.hibernate.persister.entity.EntityPersister; -import static org.hibernate.internal.util.NullnessUtil.castNonNull; /** * The action for performing entity insertions when entity is using {@code IDENTITY} column identifier generation @@ -83,7 +85,10 @@ public void execute() throws HibernateException { final GeneratedValues generatedValues; try { generatedValues = persister.getInsertCoordinator().insert( instance, state, session ); - generatedId = castNonNull( generatedValues ).getGeneratedValue( persister.getIdentifierMapping() ); + generatedId = + generatedValues == null + ? null + : generatedValues.getGeneratedValue( persister.getIdentifierMapping() ); success = true; } finally { @@ -96,9 +101,19 @@ public void execute() throws HibernateException { persistenceContext.replaceEntityEntryRowId( getInstance(), rowId ); } } + if ( generatedId == null && generatedValues != null ) { + final Object compositeId = + compositeGeneratedId( persister, instance, generatedValues, session ); + if ( compositeId != null ) { + generatedId = compositeId; + } + } if ( persister.hasInsertGeneratedProperties() ) { persister.processInsertGeneratedProperties( generatedId, instance, state, generatedValues, session ); } + if ( generatedId == null ) { + generatedId = persister.getIdentifier( instance, session ); + } //need to do that here rather than in the save event listener to let //the post insert events to have an id-filled entity when IDENTITY is used (EJB3) persister.setIdentifier( instance, generatedId, session ); @@ -161,6 +176,83 @@ protected void postInsert() { .fireLazyEventOnEachListener( this::newPostInsertEvent, PostInsertEventListener::onPostInsert ); } + private static Object compositeGeneratedId( + EntityPersister persister, + Object entity, + GeneratedValues generatedValues, + SharedSessionContractImplementor session) { + if ( persister.getIdentifierMapping() instanceof CompositeIdentifierMapping compositeIdentifier ) { + final var idMapping = compositeIdentifier.getMappedIdEmbeddableTypeDescriptor(); + final var generatedMapping = compositeIdentifier.getEmbeddableTypeDescriptor(); + final Object currentId = persister.getIdentifier( entity, session ); + if ( currentId == null ) { + final var values = defaultedPrimitiveIds( generatedMapping, idMapping ); + if ( unpackGeneratedValues( generatedValues, generatedMapping, values ) ) { + return idMapping.getRepresentationStrategy() + .getInstantiator().instantiate( () -> values ); + } + else { + return null; + } + } + else { + final var values = idMapping.getValues( currentId ); + if ( unpackGeneratedValues( generatedValues, generatedMapping, values ) ) { + idMapping.setValues( currentId, values ); + return currentId; + } + else { + return null; + } + } + } + else { + return null; + } + } + + /** + * To instantiate a composite id class with primitive fields, + * via an {@link EmbeddableInstantiator}, we need to assign + * their Java default values. + */ + public static Object[] defaultedPrimitiveIds( + EmbeddableMappingType generatedMapping, + EmbeddableMappingType idMapping) { + final int attributeCount = generatedMapping.getNumberOfAttributeMappings(); + final var values = new Object[attributeCount]; + for ( int i = 0; i < attributeCount; i++ ) { + final var attribute = idMapping.getAttributeMapping( i ); + if ( attribute.getPropertyAccess().getGetter().getReturnTypeClass().isPrimitive() ) { + values[i] = attribute.getJavaType().getDefaultValue(); + } + } + return values; + } + + private static boolean unpackGeneratedValues( + GeneratedValues generatedValues, + EmbeddableMappingType generatedMapping, + Object[] values) { + final int attributeCount = + generatedMapping.getNumberOfAttributeMappings(); + boolean updated = false; + for ( int i = 0; i < attributeCount; i++ ) { + final var basicPart = + generatedMapping.getAttributeMapping( i ) + .asBasicValuedModelPart(); + if ( basicPart != null ) { + final Object generatedValue = + generatedValues.getGeneratedValue( basicPart ); + if ( generatedValue != null ) { + values[i] = generatedValue; + updated = true; + } + } + } + return updated; + } + PostInsertEvent newPostInsertEvent() { return new PostInsertEvent( getInstance(), generatedId, getState(), getPersister(), eventSource() ); } diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/Any.java b/hibernate-core/src/main/java/org/hibernate/annotations/Any.java index 82d6efac52a9..40b142484eb2 100644 --- a/hibernate-core/src/main/java/org/hibernate/annotations/Any.java +++ b/hibernate-core/src/main/java/org/hibernate/annotations/Any.java @@ -7,6 +7,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.Target; +import jakarta.persistence.CascadeType; import jakarta.persistence.FetchType; import static java.lang.annotation.ElementType.FIELD; @@ -98,6 +99,13 @@ */ FetchType fetch() default FetchType.EAGER; + /** + * The operations that should be cascaded to the associated entities. + *

By default, no operations are cascaded. + * @since 7.4 + */ + CascadeType[] cascade() default {}; + /** * Whether the association is optional. *

diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/Audited.java b/hibernate-core/src/main/java/org/hibernate/annotations/Audited.java new file mode 100644 index 000000000000..8580257a0cbf --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/annotations/Audited.java @@ -0,0 +1,99 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.annotations; + +import org.hibernate.Incubating; +import org.hibernate.cfg.StateManagementSettings; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PACKAGE; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Specifies that the annotated entity class is an audited entity + * or audited collection. An audited entity or collection keeps + * a historical record of changes over time. Unlike a + * {@linkplain Temporal temporal} entity, it explicitly records + * the nature of each change, such as creation, modification, or + * deletion. An audited entity or collection maps to two tables, + * a table holding the current state of the entity or collection, + * and an audit log table with a record of each change. + *

+ * The audit log contains the following columns: + *

    + *
  • columns holding the state of the entity or collection at + * the moment of creation, modification, or deletion, except + * for state held by {@linkplain Excluded excluded attributes}, + *
  • a {@linkplain StateManagementSettings#TRANSACTION_ID_SUPPLIER + * transaction id} recording the unit of work in which the + * change occurred, and + *
  • a column indicating the type of change, encoded as 0 for + * creation, 1 for modification, and 2 for deletion. + *
+ *

+ * Audited entities are typically used when a supplier of + * transaction identifiers is available to Hibernate. A supplier + * may be specified via the configuration property + * {@value StateManagementSettings#TRANSACTION_ID_SUPPLIER}. + * Transactions ids must be unique and comparable and must + * increase monotonically. Typically, such an id is obtained by + * persisting an instance of an application-defined entity class + * with a generated id which represents the current unit of work. + * This entity associates the transaction id with other information + * about the work being performed, such as the current timestamp, + * current application user, and so on. If no supplier is provided, + * the {@linkplain java.time.Instant#now() current JVM instant} is + * used as the transaction identifier, but relying on this default + * behavior is not recommended. + * + * @author Gavin King + * + * @since 7.4 + */ +@Documented +@Target({PACKAGE, TYPE, FIELD, METHOD, ANNOTATION_TYPE}) +@Retention(RUNTIME) +@Incubating +public @interface Audited { + /** + * The name of the audit log table. Defaults to the + * name of the main table holding currently effective + * data, with the suffix {@code _aud}. + */ + String tableName() default ""; + + /** + * The name of the column holding the transaction identifier. + * @see org.hibernate.engine.spi.SharedSessionContractImplementor#getCurrentTransactionIdentifier() + */ + String transactionId() default "REV"; + + /** + * The name of the column holding the modification type, + * encoded as 0 for creation, 1 for modification, and 2 + * for deletion + */ + String modificationType() default "REVTYPE"; + + /** + * Excludes the annotated attribute from auditing. + * Updates to an excluded attribute modify the current + * row directly without creating a new revision of the + * entity instance. The audit log table does not contain + * columns mapped by excluded attributes. + */ + @Documented + @Target({FIELD, METHOD}) + @Retention(RUNTIME) + @interface Excluded { + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/BatchSize.java b/hibernate-core/src/main/java/org/hibernate/annotations/BatchSize.java index 735679b38fd3..6b91bfa65d63 100644 --- a/hibernate-core/src/main/java/org/hibernate/annotations/BatchSize.java +++ b/hibernate-core/src/main/java/org/hibernate/annotations/BatchSize.java @@ -61,7 +61,7 @@ public @interface BatchSize { /** * The maximum batch size, a strictly positive integer. - *

+ *

* Default is defined by {@link org.hibernate.cfg.FetchSettings#DEFAULT_BATCH_FETCH_SIZE} */ int size(); diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/CacheConcurrencyStrategy.java b/hibernate-core/src/main/java/org/hibernate/annotations/CacheConcurrencyStrategy.java index 77e5e99b2906..b294f06bb1bb 100644 --- a/hibernate-core/src/main/java/org/hibernate/annotations/CacheConcurrencyStrategy.java +++ b/hibernate-core/src/main/java/org/hibernate/annotations/CacheConcurrencyStrategy.java @@ -108,6 +108,10 @@ public enum CacheConcurrencyStrategy { * version. * *

+ * This concurrency strategy should only be used with + * {@linkplain jakarta.persistence.Version versioned} + * entities. + *

* This concurrency strategy is not compatible with * serializable transaction isolation. * diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/Columns.java b/hibernate-core/src/main/java/org/hibernate/annotations/Columns.java deleted file mode 100644 index 55dc7f02ad26..000000000000 --- a/hibernate-core/src/main/java/org/hibernate/annotations/Columns.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * Copyright Red Hat Inc. and Hibernate Authors - */ -package org.hibernate.annotations; - -import java.lang.annotation.Retention; -import java.lang.annotation.Target; -import jakarta.persistence.Column; - -import static java.lang.annotation.ElementType.FIELD; -import static java.lang.annotation.ElementType.METHOD; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -/** - * Support an array of columns. Useful for component user types mappings - * - * @author Emmanuel Bernard - */ -@Target({METHOD, FIELD}) -@Retention(RUNTIME) -public @interface Columns { - /** - * The aggregated columns. - */ - Column[] columns(); -} diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/DialectOverride.java b/hibernate-core/src/main/java/org/hibernate/annotations/DialectOverride.java index 6c2502aa42a7..dfd6e2a6643a 100644 --- a/hibernate-core/src/main/java/org/hibernate/annotations/DialectOverride.java +++ b/hibernate-core/src/main/java/org/hibernate/annotations/DialectOverride.java @@ -47,7 +47,6 @@ *

  • {@link Formula#before() before} specifies that the override applies * to all versions earlier than the given version. * - *

    * * @since 6.0 * @author Gavin King diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/DynamicUpdate.java b/hibernate-core/src/main/java/org/hibernate/annotations/DynamicUpdate.java index a4d2b511719c..192dd9d14527 100644 --- a/hibernate-core/src/main/java/org/hibernate/annotations/DynamicUpdate.java +++ b/hibernate-core/src/main/java/org/hibernate/annotations/DynamicUpdate.java @@ -18,7 +18,6 @@ * This might result in improved performance if it is common to change * only some of the attributes of the entity. However, there is a cost * associated with generating the SQL at runtime. - *

    * * @author Steve Ebersole */ diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/EmbeddedColumnNaming.java b/hibernate-core/src/main/java/org/hibernate/annotations/EmbeddedColumnNaming.java index 3c5029e67897..b06e2f8a4aaa 100644 --- a/hibernate-core/src/main/java/org/hibernate/annotations/EmbeddedColumnNaming.java +++ b/hibernate-core/src/main/java/org/hibernate/annotations/EmbeddedColumnNaming.java @@ -19,7 +19,7 @@ * For example, given a typical embeddable named {@code Address} and * {@code @EmbeddedColumnNaming("home_%s)}, we will get columns named * {@code home_street}, {@code home_city}, etc. - *

    + *

    * Explicit {@linkplain jakarta.persistence.Column @Column(name)} mappings are incorporated * into the result. When embeddables are nested, the affect will be cumulative. Given the following model: * @@ -72,7 +72,7 @@ /** * The naming pattern. It is expected to contain a single pattern marker ({@code %}) * into which the "raw" column name will be injected. - *

    + *

    * The {@code value} may be omitted which will indicate to use the pattern * {@code "{ATTRIBUTE_NAME}_%s"} where {@code {ATTRIBUTE_NAME}} is the name of the attribute * where the annotation is placed - e.g. {@code @Embedded @EmbeddedColumnNaming Address homeAddress} diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/Formula.java b/hibernate-core/src/main/java/org/hibernate/annotations/Formula.java index f281109d79a0..99d6ca4cc80e 100644 --- a/hibernate-core/src/main/java/org/hibernate/annotations/Formula.java +++ b/hibernate-core/src/main/java/org/hibernate/annotations/Formula.java @@ -41,6 +41,12 @@ * BigDecimal totalWithTax; * *

    + * The placeholder {@code {alias}} is resolved to the alias of the entity: + *

    + * @Formula("balance/(select sum(a.balance) from customer a where a.gender={alias}.gender)")
    + * private BigDecimal percentage;
    + * 
    + *

    * For an entity with {@linkplain jakarta.persistence.SecondaryTable secondary tables}, * a formula may involve columns of the primary table, or columns of any one of the * secondary tables. But it may not involve columns of more than one table. diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/IdGeneratorType.java b/hibernate-core/src/main/java/org/hibernate/annotations/IdGeneratorType.java index 318fdfe1509d..d3b5e0833efb 100644 --- a/hibernate-core/src/main/java/org/hibernate/annotations/IdGeneratorType.java +++ b/hibernate-core/src/main/java/org/hibernate/annotations/IdGeneratorType.java @@ -22,7 +22,7 @@ * For example, if we have a custom identifier generator: *

      * public class CustomSequenceGenerator implements BeforeExecutionGenerator {
    - *     public CustomSequenceGenerator(CustomSequence config, Member annotatedMember,
    + *     public CustomSequenceGenerator(CustomSequence config,
      *                                    GeneratorCreationContext context) {
      *         ...
      *     }
    diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/Immutable.java b/hibernate-core/src/main/java/org/hibernate/annotations/Immutable.java
    index a48a5bb9e03c..22d0613b6c8b 100644
    --- a/hibernate-core/src/main/java/org/hibernate/annotations/Immutable.java
    +++ b/hibernate-core/src/main/java/org/hibernate/annotations/Immutable.java
    @@ -14,8 +14,8 @@
      * Marks an entity, collection, or attribute of an entity as immutable. The absence of this
      * annotation means the element is mutable.
      *
    - * 

    Immutable entities

    - *

    + *

    Immutable entities

    + * * Changes made in memory to the state of an immutable entity are never synchronized to * the database. The changes are ignored, with no exception thrown. *

    diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/ManyToAny.java b/hibernate-core/src/main/java/org/hibernate/annotations/ManyToAny.java index dd9377fbb137..c9f76b73ac16 100644 --- a/hibernate-core/src/main/java/org/hibernate/annotations/ManyToAny.java +++ b/hibernate-core/src/main/java/org/hibernate/annotations/ManyToAny.java @@ -7,6 +7,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.Target; +import jakarta.persistence.CascadeType; import jakarta.persistence.FetchType; import static java.lang.annotation.ElementType.FIELD; @@ -79,4 +80,11 @@ * */ FetchType fetch() default FetchType.EAGER; + + /** + * The operations that should be cascaded to the associated entities. + *

    By default, no operations are cascaded. + * @since 7.4 + */ + CascadeType[] cascade() default {}; } diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/Mutability.java b/hibernate-core/src/main/java/org/hibernate/annotations/Mutability.java index 9cebc7c04708..e7b96022fe68 100644 --- a/hibernate-core/src/main/java/org/hibernate/annotations/Mutability.java +++ b/hibernate-core/src/main/java/org/hibernate/annotations/Mutability.java @@ -19,7 +19,7 @@ /** * Specifies a {@link MutabilityPlan} for a basic value mapping. - * + *

    * Mutability refers to whether the internal state of a value can change. * For example, {@linkplain java.util.Date Date} is considered mutable because its * internal state can be changed using {@link java.util.Date#setTime} whereas @@ -31,10 +31,8 @@ * {@linkplain java.util.Date Date}, {@linkplain java.lang.String String}, etc. * {@linkplain Mutability} and friends allow plugging in specific strategies. * + *

    Mutability for basic-typed attributes

    * - * - *

    Mutability for basic-typed attributes

    - *

    * For basic-valued attributes, {@code @Mutability} specifies the mutability * of the basic value type. *

    diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/NamedEntityGraph.java b/hibernate-core/src/main/java/org/hibernate/annotations/NamedEntityGraph.java index aa5aed0ac291..1d20c907f407 100644 --- a/hibernate-core/src/main/java/org/hibernate/annotations/NamedEntityGraph.java +++ b/hibernate-core/src/main/java/org/hibernate/annotations/NamedEntityGraph.java @@ -20,9 +20,9 @@ /** * Defines a named {@linkplain EntityGraph entity graph} * based on Hibernate's {@linkplain org.hibernate.graph.GraphParser entity graph language}. - *

    + *

    * When applied to a root entity class, the root entity name is implied - e.g. {@code "title, isbn, author(name, books)"} - *

    + *

    * When applied to a package, the root entity name must be specified - e.g. {@code "Book: title, isbn, author(name, books)"} * * @see EntityManager#getEntityGraph(String) @@ -39,23 +39,31 @@ @Retention(RUNTIME) @Repeatable(NamedEntityGraphs.class) public @interface NamedEntityGraph { + + /** + * The entity that is the root of the {@linkplain #graph graph}. + * When the annotation is applied to a class, the class itself is assumed. + * When applied to a package, this attribute is required. + */ + Class root() default void.class; + /** * The name used to identify the entity graph in calls to * {@linkplain org.hibernate.Session#getEntityGraph(String)}. * Entity graph names must be unique within the persistence unit. - *

    + *

    * When applied to a root entity class, the name is optional and - * defaults to the entity-name of that entity. + * defaults to the JPA entity name of that entity. */ String name() default ""; /** * The textual representation of the graph. - *

    + *

    * When applied to a package, the syntax requires the entity name - e.g., {@code "Book: title, isbn, author(name, books)"}. - *

    + *

    * When applied to an entity, the entity name should be omitted - e.g., {@code "title, isbn, author(name, books)"}. - *

    + *

    * See {@linkplain org.hibernate.graph.GraphParser} for details about the syntax. */ String graph(); diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/NaturalId.java b/hibernate-core/src/main/java/org/hibernate/annotations/NaturalId.java index 4eca9fc13bab..0d5325aa9a91 100644 --- a/hibernate-core/src/main/java/org/hibernate/annotations/NaturalId.java +++ b/hibernate-core/src/main/java/org/hibernate/annotations/NaturalId.java @@ -4,6 +4,7 @@ */ package org.hibernate.annotations; + import java.lang.annotation.Retention; import java.lang.annotation.Target; @@ -11,80 +12,107 @@ import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.RetentionPolicy.RUNTIME; -/** - * Specifies that a field or property of an entity class is part of - * the natural id of the entity. This annotation is very useful when - * the primary key of an entity class is a surrogate key, that is, - * a {@linkplain jakarta.persistence.GeneratedValue system-generated} - * synthetic identifier, with no domain-model semantics. There should - * always be some other field or combination of fields which uniquely - * identifies an instance of the entity from the point of view of the - * user of the system. This is the natural id of the entity. - *

    - * A natural id may be a single field or property of the entity: - *

    - * @Entity
    - * @Cache @NaturalIdCache
    - * class Person {
    - *
    - *     //synthetic id
    - *     @GeneratedValue @Id
    - *     Long id;
    - *
    - *     @NotNull
    - *     String name;
    - *
    - *     //simple natural id
    - *     @NotNull @NaturalId
    - *     String ssn;
    - *
    - *     ...
    - * }
    - * 
    - *

    - * or it may be a composite value: - *

    - * @Entity
    - * @Cache @NaturalIdCache
    - * class Vehicle {
    - *
    - *     //synthetic id
    - *     @GeneratedValue @Id
    - *     Long id;
    - *
    - *     //composite natural id
    - *
    - *     @Enumerated
    - *     @NotNull @NaturalId
    - *     Region region;
    - *
    - *     @NotNull @NaturalId
    - *     String registration;
    - *
    - *     ...
    - * }
    - * 
    - *

    - * Unlike the {@linkplain jakarta.persistence.Id primary identifier} - * of an entity, a natural id may be {@linkplain #mutable}. - *

    - * On the other hand, a field or property which forms part of a natural - * id may never be null, and so it's a good idea to use {@code @NaturalId} - * in conjunction with the Bean Validation {@code @NotNull} annotation - * or {@link jakarta.persistence.Basic#optional @Basic(optional=false)}. - *

    - * The {@link org.hibernate.Session} interface offers several methods - * that allow an entity instance to be retrieved by its - * {@linkplain org.hibernate.Session#bySimpleNaturalId(Class) simple} - * or {@linkplain org.hibernate.Session#byNaturalId(Class) composite} - * natural id value. If the entity is also marked for {@linkplain - * NaturalIdCache natural id caching}, then these methods may be able - * to avoid a database round trip. - * - * @author NicolÃĄs Lichtmaier - * - * @see NaturalIdCache - */ +/// Specifies that a field or property of an entity class is part of +/// the natural id of the entity. This annotation is very useful when +/// the primary key of an entity class is a surrogate key, that is, +/// a {@linkplain jakarta.persistence.GeneratedValue system-generated} +/// synthetic identifier, with no domain-model semantics. There should +/// always be some other field or combination of fields that uniquely +/// identifies an instance of the entity from the point of view of the +/// user of the system. This is the _natural id_ of the entity. +/// +/// A natural id may be a single (basic or embedded) attribute of the entity: +/// ````java +/// @Entity +/// class Person { +/// +/// //synthetic id +/// @GeneratedValue @Id +/// Long id; +/// +/// @NotNull +/// String name; +/// +/// //simple natural id +/// @NotNull @NaturalId +/// String ssn; +/// +/// ... +/// } +/// ``` +/// +/// or it may be a non-aggregated composite value: +/// ```java +/// @Entity +/// class Vehicle { +/// +/// //synthetic id +/// @GeneratedValue @Id +/// Long id; +/// +/// //composite natural id +/// +/// @Enumerated +/// @NotNull +/// @NaturalId +/// Region region; +/// +/// @NotNull +/// @NaturalId +/// String registration; +/// +/// ... +/// } +/// ``` +/// +/// Unlike the {@linkplain jakarta.persistence.Id primary identifier} +/// of an entity, a natural id may be {@linkplain #mutable}. +/// +/// On the other hand, a field or property which forms part of a natural +/// id may never be null, and so it's a good idea to use `@NaturalId` +/// in conjunction with the Bean Validation `@NotNull` annotation +/// or [@Basic(optional=false)][jakarta.persistence.Basic#optional()]. +/// +/// The [org.hibernate.Session] interface offers several methods +/// that allow retrieval of one or more entity references by natural-id +/// allow an entity instance to be retrieved by its natural-id: +/// * [org.hibernate.Session#find(Class, Object, jakarta.persistence.FindOption...)] +/// allows loading a single entity instance by natural-id. +/// * [org.hibernate.Session#findMultiple(Class, java.util.List, jakarta.persistence.FindOption...)] +/// allows loading multiple entity instances by natural-id. +/// +/// ``` +/// Person person = session.find(Person.class, ssn, KeyType.NATURAL); +/// ``` +/// ``` +/// Vehicle vehicle = +/// session.find(Vehicle.class, +/// Map.of(Vehicle_.REGION, region, +/// Vehicle_.REGISTRATION, registration), +/// KeyType.NATURAL); +/// ``` +/// ``` +/// List people = session.findMultiple(Person.class, ssns, KeyType.NATURAL); +/// ``` +/// +/// If the entity is also marked for [natural id caching][NaturalIdCache], +/// then these methods may be able to avoid a database round trip. +/// +/// @see org.hibernate.Session#find(Class, Object, jakarta.persistence.FindOption...) +/// @see org.hibernate.Session#findMultiple(Class, java.util.List, jakarta.persistence.FindOption...) +/// @see NaturalIdClass +/// @see NaturalIdCache +/// +/// @apiNote For non-aggregated composite natural-id cases, it is recommended to +/// leverage [@NaturalIdClass][NaturalIdClass] for loading. +/// ``` +/// Vehicle vehicle = +/// session.find(Vehicle.class, +/// VehicleKey(region, registration), +/// KeyType.NATURAL); +/// ``` +/// +/// @author NicolÃĄs Lichtmaier @Target({METHOD, FIELD}) @Retention(RUNTIME) public @interface NaturalId { diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/NaturalIdClass.java b/hibernate-core/src/main/java/org/hibernate/annotations/NaturalIdClass.java new file mode 100644 index 000000000000..eafb1a61fc86 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/annotations/NaturalIdClass.java @@ -0,0 +1,50 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.annotation.Retention; + +/// Models a non-aggregated composite natural-id for the purpose of loading. +/// The non-aggregated form uses multiple [@NaturalId][NaturalId] as opposed +/// to the aggregated form which uses a single [@NaturalId][NaturalId] combined +/// with [@Embedded][jakarta.persistence.Embedded]. +/// Functions in a similar fashion as [@IdClass][jakarta.persistence.IdClass] for +/// non-aggregated composite identifiers. +/// +/// ```java +/// @Entity +/// @NaturalIdClass(OrderNaturalId.class) +/// class Order { +/// @Id +/// Integer id; +/// @NaturalId @ManyToOne +/// Customer customer; +/// @NaturalId +/// Integer orderNumber; +/// ... +/// } +/// +/// class OrderNaturalId { +/// Customer customer; +/// Integer orderNumber; +/// ... +/// } +/// ``` +/// +/// @see NaturalId +/// @see jakarta.persistence.IdClass +/// +/// @since 7.3 +/// +/// @author Steve Ebersole +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface NaturalIdClass { + /// The class to use for loading the associated entity by natural-id. + Class value(); +} diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/PartitionKey.java b/hibernate-core/src/main/java/org/hibernate/annotations/PartitionKey.java index baaddf26aac3..0ccfb6970039 100644 --- a/hibernate-core/src/main/java/org/hibernate/annotations/PartitionKey.java +++ b/hibernate-core/src/main/java/org/hibernate/annotations/PartitionKey.java @@ -25,7 +25,6 @@ * part of the identifier, use of this annotation * may improve the performance of SQL {@code update} * and {@code delete} statements. - *

    *

      * @Entity
      * @Table(name  = "partitioned_table",
    diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/SoftDelete.java b/hibernate-core/src/main/java/org/hibernate/annotations/SoftDelete.java
    index 11f8b4f19e9f..d34d26258e65 100644
    --- a/hibernate-core/src/main/java/org/hibernate/annotations/SoftDelete.java
    +++ b/hibernate-core/src/main/java/org/hibernate/annotations/SoftDelete.java
    @@ -24,10 +24,10 @@
     
     /**
      * Describes a soft-delete indicator mapping.
    - * 

    + *

    * Soft deletes handle "deletions" from a database table by setting a column in * the table to indicate deletion. - *

    + *

    * May be defined at various levels

      *
    • * {@linkplain ElementType#PACKAGE PACKAGE}, where it applies to all @@ -60,7 +60,7 @@ public @interface SoftDelete { /** * (Optional) The name of the column. - *

      + *

      * Default depends on the {@linkplain #strategy() strategy} being used. * * @see SoftDeleteType#getDefaultColumnName() @@ -87,7 +87,7 @@ /** * The strategy to use for storing/reading values to/from the database. - *

      + *

      * The strategy also affects the default {@linkplain #columnName() column name} - see * {@linkplain SoftDeleteType#getDefaultColumnName}. */ @@ -102,7 +102,7 @@ *

      {@code false}
      *
      Indicates that the row is considered NOT deleted
      * - *

      + *

      * By default, values are stored as booleans in the database according to * the {@linkplain Dialect#getPreferredSqlTypeCodeForBoolean() dialect} * and {@linkplain org.hibernate.cfg.MappingSettings#PREFERRED_BOOLEAN_JDBC_TYPE settings} @@ -116,7 +116,7 @@ Class> converter() default UnspecifiedConversion.class; /** - * Used as the default for {@linkplain SoftDelete#converter()}, indicating that + * Used as the default for {@linkplain SoftDelete#converter}, indicating that * {@linkplain Dialect#getPreferredSqlTypeCodeForBoolean() dialect} and * {@linkplain org.hibernate.cfg.MappingSettings#PREFERRED_BOOLEAN_JDBC_TYPE settings} * resolution should be used. diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/SoftDeleteType.java b/hibernate-core/src/main/java/org/hibernate/annotations/SoftDeleteType.java index dc6511549994..faa1fd539c29 100644 --- a/hibernate-core/src/main/java/org/hibernate/annotations/SoftDeleteType.java +++ b/hibernate-core/src/main/java/org/hibernate/annotations/SoftDeleteType.java @@ -4,8 +4,6 @@ */ package org.hibernate.annotations; -import java.util.Locale; - /** * Enumeration of defines styles of soft-delete * @@ -42,19 +40,16 @@ public enum SoftDeleteType { *

      indicates that the row is deleted, at the given timestamp
      * */ - TIMESTAMP( "deleted" ); - - private final String defaultColumnName; - - SoftDeleteType() { - this.defaultColumnName = name().toLowerCase( Locale.ROOT ); - } - - SoftDeleteType(String defaultColumnName) { - this.defaultColumnName = defaultColumnName; - } + TIMESTAMP; + /** + * The default column name used with this strategy. + * @see SoftDelete#columnName + */ public String getDefaultColumnName() { - return defaultColumnName; + return switch ( this ) { + case ACTIVE -> "active"; + case DELETED, TIMESTAMP -> "deleted"; + }; } } diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/Temporal.java b/hibernate-core/src/main/java/org/hibernate/annotations/Temporal.java new file mode 100644 index 000000000000..e0dbc50985c1 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/annotations/Temporal.java @@ -0,0 +1,220 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.annotations; + +import org.hibernate.Incubating; +import org.hibernate.cfg.StateManagementSettings; +import org.hibernate.temporal.TemporalTableStrategy; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.time.Instant; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PACKAGE; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Specifies that the annotated entity class is a temporal entity + * or temporal collection. A temporal entity or collection keeps a + * historical record of changes over time. Each row of the mapped + * table or tables represents a single revision of a single instance + * of the entity, or an element of the collection whose existence is + * bounded in time, with: + *
        + *
      • a {@link #rowStart} timestamp representing the instant at + * which the revision became effective, and + *
      • a {@link #rowEnd} timestamp representing the instant at + * which the revision was superseded by a newer revision. + *
      + * The revision which is currently effective has a null value for + * its {@linkplain #rowEnd} timestamp. + *

      + * Given the identifier of an instance of a temporal entity, along + * with an instant, which may be represented by an instance of + * {@link java.time.Instant}, the effective revision of the + * temporal entity with the given identifier at the given instant is + * the revision with: + *

        + *
      • {@link #rowStart} timestamp less than or equal to the given + * instant, and + *
      • {@link #rowEnd} timestamp null or greater than the given + * instant. + *
      + *

      + * There are three {@linkplain TemporalTableStrategy strategies} + * for mapping a temporal entity or collection to a table or tables. + *

        + *
      • In the {@linkplain TemporalTableStrategy#SINGLE_TABLE + * single table} strategy, current and historical data + * is stored together in the same table. Foreign keys + * referencing this table have no constraints, and so + * the database cannot enforce referential integrity. + * The table may be {@linkplain HistoryPartitioning + * partitioned} into current and historical partitions. + *
      • In the {@linkplain TemporalTableStrategy#HISTORY_TABLE + * separate history table} strategy, current data is stored + * in one table, and historical data is stored in a second + * table. Referential integrity may be enforced for current + * data, but not for historical data. The {@code rowStart} + * and {@code rowEnd} columns belong to the history table. + *
      • In the {@linkplain TemporalTableStrategy#NATIVE native} + * strategy, temporal data is stored in a temporal table + * when temporal tables are supported natively by the + * database. The {@code rowStart} and {@code rowEnd} columns + * are managed by the database itself. Depending on the + * capabilities of the database, referential integrity might + * be enforced. + *
      + *

      + * The configuration property + * {@value StateManagementSettings#TEMPORAL_TABLE_STRATEGY} + * controls the temporal table mapping strategy. + *

      + * By default, a session or stateless session reads revisions of + * temporal data which are currently effective. To read historical + * revisions effective at a given {@linkplain Instant instant}, set + * {@linkplain org.hibernate.engine.creation.CommonBuilder#asOf + * the temporal data instant} when creating the session or stateless + * session. + *

      + * The following recommendations do not apply to the native mapping + * strategy: + *

        + *
      • It is recommended that every temporal entity declare a + * {@linkplain jakarta.persistence.Version version} attribute. + * The primary key of a table mapped by a temporal entity + * includes the columns mapped by the identifier of the entity, + * along with the version column, if there is one, or the + * {@link #rowStart} column if there is no version. + *
      • When working with temporal entities, it is important to + * ensure that referential integrity is maintained by the + * application and validated by triggers or offline processes. + *
      + *

      + * By default, timestamps for the {@code rowStart} and + * {@code rowEnd} columns are generated in Java, unless native + * temporal tables are used. If timestamps should be generated on + * the database server, enable the configuration property + * {@value StateManagementSettings#USE_SERVER_TRANSACTION_TIMESTAMPS}. + *

      + * An alternative approach, which is not compatible with the + * {@linkplain TemporalTableStrategy#NATIVE native} mapping strategy, + * is to provide a custom {@link java.util.function.Supplier} of + * transaction ids by specifying the configuration property + * {@value StateManagementSettings#TRANSACTION_ID_SUPPLIER}. + * Transaction ids must be unique and comparable and must increase + * monotonically. Typically, such an id is obtained by persisting + * an instance of an application-defined entity class with a + * generated id which represents the current unit of work. This + * entity associates the transaction id with other information about + * the work being performed, such as the current timestamp, current + * application user, and so on. + * + * @apiNote + * {@linkplain jakarta.persistence.SecondaryTable Secondary tables} and + * {@linkplain org.hibernate.boot.model.source.spi.InheritanceType#JOINED + * joined inheritance mappings} are not supported for temporal entities. + * + * @see org.hibernate.engine.creation.CommonBuilder#asOf(Instant) + * @see org.hibernate.engine.creation.CommonBuilder#atTransaction(Object) + * @see StateManagementSettings#TEMPORAL_TABLE_STRATEGY + * @see StateManagementSettings#TRANSACTION_ID_SUPPLIER + * @see StateManagementSettings#USE_SERVER_TRANSACTION_TIMESTAMPS + * + * @author Gavin King + * + */ +@Documented +@Target({PACKAGE, TYPE, FIELD, METHOD, ANNOTATION_TYPE}) +@Retention(RUNTIME) +@Incubating +public @interface Temporal { + /** + * The name of the column holding the starting timestamp + * or transaction id of a revision; the timestamp or id + * representing the moment the revision became effective; + * that is, the "effective from" timestamp. + */ + String rowStart() default "effective"; + /** + * The name of the column holding the ending timestamp + * or transaction id of a revision; the timestamp or id + * representing the moment the revision was superseded; + * that is, the "effective to" timestamp. + */ + String rowEnd() default "superseded"; + + /** + * The fractional seconds precision for both temporal columns. + * + * @see jakarta.persistence.Column#secondPrecision() + */ + int secondPrecision() default -1; + + /** + * Enables partitioning for a temporal table mapped by + * a {@linkplain Temporal temporal entity or collection} + * in the {@linkplain TemporalTableStrategy#SINGLE_TABLE + * single table} temporal mapping strategy. In other + * mapping strategies this annotation has no effect. + */ + @Documented + @Target({TYPE, FIELD, METHOD}) + @Retention(RUNTIME) + @interface HistoryPartitioning { + /** + * The name of the partition holding currently + * effective data. Defaults to the temporal table + * name with the suffix {@code _current}. + */ + String currentPartition() default ""; + /** + * The name of the partition holding historical + * data. Defaults to the temporal table name with + * the suffix {@code _history}. + */ + String historyPartition() default ""; + } + + /** + * Specifies the name of the separate history table for + * a {@linkplain Temporal temporal entity or collection} + * when the history table strategy is used. + * + * @see TemporalTableStrategy#HISTORY_TABLE + */ + @Documented + @Target({TYPE, FIELD, METHOD}) + @Retention(RUNTIME) + @interface HistoryTable { + /** + * The name of the history table. Defaults to the + * name of the main table holding currently effective + * data, with the suffix {@code _history}. + */ + String name() default ""; + } + + /** + * Excludes the annotated attribute from temporal + * versioning. Updates to an excluded attribute + * modify the current row directly without creating + * a new revision of the entity instance. + *

      For {@linkplain TemporalTableStrategy#NATIVE + * native temporal tables}, this is only supported + * if the database itself provides a way to exclude + * a column from temporal versioning. + */ + @Documented + @Target({FIELD, METHOD}) + @Retention(RUNTIME) + @interface Excluded { + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/Type.java b/hibernate-core/src/main/java/org/hibernate/annotations/Type.java index 9b5724aee625..bb216e05f9f5 100644 --- a/hibernate-core/src/main/java/org/hibernate/annotations/Type.java +++ b/hibernate-core/src/main/java/org/hibernate/annotations/Type.java @@ -8,6 +8,7 @@ import java.lang.annotation.Target; import org.hibernate.usertype.UserType; +import org.hibernate.usertype.UserTypeCreationContext; import static java.lang.annotation.ElementType.ANNOTATION_TYPE; import static java.lang.annotation.ElementType.FIELD; @@ -31,7 +32,7 @@ * BigDecimal amount; *

    *

    - * we may define an annotation type: + * we may define a custom annotation type: *

      * @Retention(RUNTIME)
      * @Target({METHOD,FIELD})
    @@ -47,6 +48,27 @@
      * 

    * which is much cleaner. *

    + * An implementation of {@link UserType} applied via a custom annotation + * may declare a constructor which accepts the annotation instance, + * allowing the annotation to be used to configure the type. + *

    + * @Retention(RUNTIME)
    + * @Target({METHOD,FIELD})
    + * @Type(MonetaryAmountUserType.class)
    + * public @interface MonetaryAmount {
    + *     public Currency currency();
    + * }
    + * 
    + *
    + * public class MonetaryAmountUserType implements UserType<Amount> {
    + *     private final Currency currency;
    + *     public MonetaryAmountUserType(MonetaryAmount annotation) {
    + *         currency = annotation.currency();
    + *     }
    + *     ...
    + * }
    + * 
    + *

    * The use of a {@code UserType} is usually mutually exclusive with the * compositional approach of {@link JavaType} and {@link JdbcType}. * @@ -64,9 +86,14 @@ /** * Parameters to be injected into the custom type after it is - * instantiated. The {@link UserType} implementation must implement + * instantiated. The {@link UserType} implementation may implement * {@link org.hibernate.usertype.ParameterizedType} to receive the - * parameters. + * parameters, or it may obtain them via + * {@link UserTypeCreationContext#getParameters()}. + * + * @apiNote A better approach is to declare a custom annotation type, + * as described {@linkplain Type above}, and specify parameters in a + * type safe way as members of the custom annotation. */ Parameter[] parameters() default {}; } diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/ValueGenerationType.java b/hibernate-core/src/main/java/org/hibernate/annotations/ValueGenerationType.java index 802833f1006f..36cb3e2c37cf 100644 --- a/hibernate-core/src/main/java/org/hibernate/annotations/ValueGenerationType.java +++ b/hibernate-core/src/main/java/org/hibernate/annotations/ValueGenerationType.java @@ -26,8 +26,7 @@ *

      * public class SKUGeneration
      *         implements BeforeExecutionGenerator {
    - *     public SKUGeneration(SKU sku, Member annotatedMember,
    - *                          GeneratorCreationContext context) {
    + *     public SKUGeneration(SKU sku, GeneratorCreationContext context) {
      *         ...
      *     }
      *     ...
    diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/package-info.java b/hibernate-core/src/main/java/org/hibernate/annotations/package-info.java
    index 069cf2952cc2..c8da00bdc57f 100644
    --- a/hibernate-core/src/main/java/org/hibernate/annotations/package-info.java
    +++ b/hibernate-core/src/main/java/org/hibernate/annotations/package-info.java
    @@ -9,14 +9,15 @@
      * The JPA specification perfectly nails many aspects of the O/R persistence problem, but
      * here we address some areas where it falls short.
      *
    - * 

    Basic types in JPA

    - *

    + *

    Basic types in JPA

    + * * A basic type handles the persistence of an attribute of an entity or embeddable * object that is stored in exactly one database column. *

    * JPA supports a very limited set of built-in {@linkplain jakarta.persistence.Basic basic} * types. *

  • Column type per database
    Column typeDatabase
    {@code text}on PostgreSQL
    {@code longtext}on MySQL
    {@code clob}on h2, Oracle, and Db2
    + * * * * diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/processing/Find.java b/hibernate-core/src/main/java/org/hibernate/annotations/processing/Find.java index 9bd9176ed068..fabccf3b274d 100644 --- a/hibernate-core/src/main/java/org/hibernate/annotations/processing/Find.java +++ b/hibernate-core/src/main/java/org/hibernate/annotations/processing/Find.java @@ -106,12 +106,12 @@ *

    * This is reminiscent of traditional DAO-style repositories. *

    - * The return type of an annotated method must be an entity type {@code E}, + * The return type of the annotated method must be an entity type {@code E} * or one of the following types: *

    Supported basic types
    CategoryPackageTypes
    getTableExporter() { return postgresqlTableExporter; } - /** - * @return {@code true}, but only because we can "batch" truncate - */ + /// @return `true`, but only because we can "batch" truncate @Override public boolean canBatchTruncate() { return true; @@ -1582,13 +1614,10 @@ public MutationOperation createOptionalTableUpdateOperation( EntityMutationTarget mutationTarget, OptionalTableUpdate optionalTableUpdate, SessionFactoryImplementor factory) { - if ( supportsMerge ) { - return new PostgreSQLSqlAstTranslator<>( factory, optionalTableUpdate ) - .createMergeOperation( optionalTableUpdate ); - } - else { - return super.createOptionalTableUpdateOperation( mutationTarget, optionalTableUpdate, factory ); - } + return supportsMerge + ? new PostgreSQLSqlAstTranslator<>( factory, optionalTableUpdate ) + .createMergeOperation( optionalTableUpdate ) + : new OptionalTableUpdateWithUpsertOperation( mutationTarget, optionalTableUpdate, factory ); } @Override @@ -1646,21 +1675,31 @@ public boolean supportsArrayConstructor() { @Override public boolean supportsRecursiveCycleClause() { - return getVersion().isSameOrAfter( 14 ); + return true; } @Override public boolean supportsRecursiveCycleUsingClause() { - return getVersion().isSameOrAfter( 14 ); + return true; } @Override public boolean supportsRecursiveSearchClause() { - return getVersion().isSameOrAfter( 14 ); + return true; } @Override public InformationExtractor getInformationExtractor(ExtractionContext extractionContext) { return new InformationExtractorPostgreSQLImpl( extractionContext ); } + + @Override + public boolean causesRollback(SQLException sqlException) { + return true; + } + + @Override + public TemporalTableSupport getTemporalTableSupport() { + return new PostgreSQLTemporalTableSupport( this ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/PostgresPlusDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/PostgresPlusDialect.java index fe925b3a2777..9422f66b101f 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgresPlusDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgresPlusDialect.java @@ -15,7 +15,6 @@ import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.query.sqm.CastType; import org.hibernate.query.common.TemporalUnit; -import org.hibernate.query.sqm.produce.function.StandardFunctionArgumentTypeResolvers; import org.hibernate.sql.ast.SqlAstTranslator; import org.hibernate.sql.ast.SqlAstTranslatorFactory; import org.hibernate.sql.ast.spi.StandardSqlAstTranslatorFactory; @@ -25,6 +24,8 @@ import jakarta.persistence.TemporalType; +import static org.hibernate.query.sqm.produce.function.StandardFunctionArgumentTypeResolvers.ARGUMENT_OR_IMPLIED_RESULT_TYPE; + /** * An SQL dialect for Postgres Plus * @@ -52,33 +53,25 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio super.initializeFunctionRegistry( functionContributions ); final var functionFactory = new CommonFunctionFactory( functionContributions ); + final var functionRegistry = functionContributions.getFunctionRegistry(); + functionFactory.arrayGet_bracket( false ); functionFactory.soundex(); functionFactory.rownumRowid(); functionFactory.sysdate(); functionFactory.systimestamp(); - if ( getVersion().isSameOrAfter( 14 ) ) { - // Support for these functions were apparently only added in version 14 - functionFactory.bitand(); - functionFactory.bitor(); - functionContributions.getFunctionRegistry().patternDescriptorBuilder( - "bitxor", - "(bitor(?1,?2)-bitand(?1,?2))" - ) - .setExactArgumentCount( 2 ) - .setArgumentTypeResolver( StandardFunctionArgumentTypeResolvers.ARGUMENT_OR_IMPLIED_RESULT_TYPE ) - .register(); - } - else { - functionContributions.getFunctionRegistry().patternDescriptorBuilder( - "bitxor", - "((?1|?2)-(?1&?2))" - ) - .setExactArgumentCount( 2 ) - .setArgumentTypeResolver( StandardFunctionArgumentTypeResolvers.ARGUMENT_OR_IMPLIED_RESULT_TYPE ) - .register(); - } + + // These functions were apparently only added in version 14 + functionFactory.bitand(); + functionFactory.bitor(); + functionRegistry.patternDescriptorBuilder( + "bitxor", + "(bitor(?1,?2)-bitand(?1,?2))" + ) + .setExactArgumentCount( 2 ) + .setArgumentTypeResolver( ARGUMENT_OR_IMPLIED_RESULT_TYPE ) + .register(); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java index 46d3de5b485b..598f8b5cc2a9 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java @@ -32,6 +32,8 @@ import org.hibernate.dialect.sequence.SQLServerSequenceSupport; import org.hibernate.dialect.sequence.SequenceSupport; import org.hibernate.dialect.sql.ast.SQLServerSqlAstTranslator; +import org.hibernate.dialect.temporal.SQLServerTemporalTableSupport; +import org.hibernate.dialect.temporal.TemporalTableSupport; import org.hibernate.dialect.temptable.SQLServerLocalTemporaryTableStrategy; import org.hibernate.dialect.temptable.TemporaryTableStrategy; import org.hibernate.dialect.type.SQLServerCastingXmlArrayJdbcTypeConstructor; @@ -77,8 +79,6 @@ import org.hibernate.tool.schema.internal.StandardSequenceExporter; import org.hibernate.tool.schema.internal.StandardTableExporter; import org.hibernate.tool.schema.spi.Exporter; -import org.hibernate.type.BasicType; -import org.hibernate.type.BasicTypeRegistry; import org.hibernate.type.StandardBasicTypes; import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.descriptor.jdbc.JdbcType; @@ -87,10 +87,8 @@ import org.hibernate.type.descriptor.jdbc.UUIDJdbcType; import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; import org.hibernate.type.descriptor.sql.internal.DdlTypeImpl; -import org.hibernate.type.descriptor.sql.spi.DdlTypeRegistry; import java.sql.DatabaseMetaData; -import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Types; import java.time.temporal.ChronoField; @@ -217,11 +215,11 @@ private static DatabaseVersion staticDetermineDatabaseVersion(DialectResolutionI } private static Integer getCompatibilityLevel(DialectResolutionInfo info) { - final DatabaseMetaData databaseMetaData = info.getDatabaseMetadata(); + final var databaseMetaData = info.getDatabaseMetadata(); if ( databaseMetaData != null ) { - try ( var statement = databaseMetaData.getConnection().createStatement() ) { - final ResultSet resultSet = - statement.executeQuery( "SELECT compatibility_level FROM sys.databases where name = db_name();" ); + try ( var statement = databaseMetaData.getConnection().createStatement(); + var resultSet = statement.executeQuery( + "SELECT compatibility_level FROM sys.databases where name = db_name();" ) ) { if ( resultSet.next() ) { return resultSet.getInt( 1 ); } @@ -283,7 +281,7 @@ protected String castType(int sqlTypeCode) { @Override protected void registerColumnTypes(TypeContributions typeContributions, ServiceRegistry serviceRegistry) { super.registerColumnTypes( typeContributions, serviceRegistry ); - final DdlTypeRegistry ddlTypeRegistry = typeContributions.getTypeConfiguration().getDdlTypeRegistry(); + final var ddlTypeRegistry = typeContributions.getTypeConfiguration().getDdlTypeRegistry(); ddlTypeRegistry.addDescriptor( new DdlTypeImpl( GEOMETRY, "geometry", this ) ); ddlTypeRegistry.addDescriptor( new DdlTypeImpl( GEOGRAPHY, "geography", this ) ); ddlTypeRegistry.addDescriptor( new DdlTypeImpl( SQLXML, "xml", this ) ); @@ -362,11 +360,11 @@ public void contributeTypes(TypeContributions typeContributions, ServiceRegistry public void initializeFunctionRegistry(FunctionContributions functionContributions) { super.initializeFunctionRegistry( functionContributions ); - final BasicTypeRegistry basicTypeRegistry = + final var basicTypeRegistry = functionContributions.getTypeConfiguration().getBasicTypeRegistry(); - final BasicType dateType = basicTypeRegistry.resolve( StandardBasicTypes.DATE ); - final BasicType timeType = basicTypeRegistry.resolve( StandardBasicTypes.TIME ); - final BasicType timestampType = basicTypeRegistry.resolve( StandardBasicTypes.TIMESTAMP ); + final var dateType = basicTypeRegistry.resolve( StandardBasicTypes.DATE ); + final var timeType = basicTypeRegistry.resolve( StandardBasicTypes.TIME ); + final var timestampType = basicTypeRegistry.resolve( StandardBasicTypes.TIMESTAMP ); final var functionFactory = new CommonFunctionFactory( functionContributions ); @@ -444,37 +442,14 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.windowFunctions(); functionFactory.inverseDistributionOrderedSetAggregates_windowEmulation(); functionFactory.hypotheticalOrderedSetAggregates_windowEmulation(); - if ( getVersion().isSameOrAfter( 13 ) ) { - functionFactory.jsonValue_sqlserver(); - functionFactory.jsonQuery_sqlserver(); - functionFactory.jsonExists_sqlserver( getVersion().isSameOrAfter( 16 ) ); - functionFactory.jsonObject_sqlserver( getVersion().isSameOrAfter( 16 ) ); - functionFactory.jsonArray_sqlserver( getVersion().isSameOrAfter( 16 ) ); - functionFactory.jsonSet_sqlserver(); - functionFactory.jsonRemove_sqlserver(); - functionFactory.jsonReplace_sqlserver( getVersion().isSameOrAfter( 16 ) ); - functionFactory.jsonInsert_sqlserver( getVersion().isSameOrAfter( 16 ) ); - functionFactory.jsonArrayAppend_sqlserver( getVersion().isSameOrAfter( 16 ) ); - functionFactory.jsonArrayInsert_sqlserver(); - functionFactory.jsonTable_sqlserver(); - } - functionFactory.xmlelement_sqlserver(); - functionFactory.xmlcomment_sqlserver(); - functionFactory.xmlforest_sqlserver(); - functionFactory.xmlconcat_sqlserver(); - functionFactory.xmlpi_sqlserver(); - functionFactory.xmlquery_sqlserver(); - functionFactory.xmlexists_sqlserver(); - functionFactory.xmlagg_sqlserver(); - functionFactory.xmltable_sqlserver(); + registerJsonFunctions( functionFactory ); + registerXmlFunctions( functionFactory ); functionFactory.unnest_sqlserver(); if ( getVersion().isSameOrAfter( 14 ) ) { functionFactory.listagg_stringAggWithinGroup( "varchar(max)" ); - functionFactory.jsonArrayAgg_sqlserver( getVersion().isSameOrAfter( 16 ) ); - functionFactory.jsonObjectAgg_sqlserver( getVersion().isSameOrAfter( 16 ) ); } if ( getVersion().isSameOrAfter( 16 ) ) { functionFactory.leastGreatest(); @@ -501,6 +476,39 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio } } + private static void registerXmlFunctions(CommonFunctionFactory functionFactory) { + functionFactory.xmlelement_sqlserver(); + functionFactory.xmlcomment_sqlserver(); + functionFactory.xmlforest_sqlserver(); + functionFactory.xmlconcat_sqlserver(); + functionFactory.xmlpi_sqlserver(); + functionFactory.xmlquery_sqlserver(); + functionFactory.xmlexists_sqlserver(); + functionFactory.xmlagg_sqlserver(); + functionFactory.xmltable_sqlserver(); + } + + private void registerJsonFunctions(CommonFunctionFactory functionFactory) { + if ( getVersion().isSameOrAfter( 13 ) ) { + functionFactory.jsonValue_sqlserver(); + functionFactory.jsonQuery_sqlserver(); + functionFactory.jsonExists_sqlserver( getVersion().isSameOrAfter( 16 ) ); + functionFactory.jsonObject_sqlserver( getVersion().isSameOrAfter( 16 ) ); + functionFactory.jsonArray_sqlserver( getVersion().isSameOrAfter( 16 ) ); + functionFactory.jsonSet_sqlserver(); + functionFactory.jsonRemove_sqlserver(); + functionFactory.jsonReplace_sqlserver( getVersion().isSameOrAfter( 16 ) ); + functionFactory.jsonInsert_sqlserver( getVersion().isSameOrAfter( 16 ) ); + functionFactory.jsonArrayAppend_sqlserver( getVersion().isSameOrAfter( 16 ) ); + functionFactory.jsonArrayInsert_sqlserver(); + functionFactory.jsonTable_sqlserver(); + } + if ( getVersion().isSameOrAfter( 14 ) ) { + functionFactory.jsonArrayAgg_sqlserver( getVersion().isSameOrAfter( 16 ) ); + functionFactory.jsonObjectAgg_sqlserver( getVersion().isSameOrAfter( 16 ) ); + } + } + /** * SQL Server doesn't support the {@code generate_series} function or {@code lateral} recursive CTEs, * so it has to be emulated with a top level recursive CTE which requires an upper bound on the amount @@ -631,12 +639,17 @@ public String getCurrentSchemaCommand() { @Override public boolean supportsIfExistsBeforeTableName() { - return getVersion().isSameOrAfter( 16 ) || super.supportsIfExistsBeforeTableName(); + return getVersion().isSameOrAfter( 16 ); } @Override public boolean supportsIfExistsBeforeConstraintName() { - return getVersion().isSameOrAfter( 16 ) || super.supportsIfExistsBeforeConstraintName(); + return getVersion().isSameOrAfter( 16 ); + } + + @Override + public boolean supportsIfExistsBeforeIndexName() { + return getVersion().isSameOrAfter( 16 ); } @Override @@ -748,7 +761,7 @@ public String getQuerySequencesString() { @Override public String getQueryHintString(String sql, String hints) { - final StringBuilder buffer = + final var buffer = new StringBuilder( sql.length() + hints.length() + 12 ); final int pos = sql.indexOf( ';' ); if ( pos > -1 ) { @@ -1073,6 +1086,13 @@ public void appendDateTimeLiteral( } } + @Override + public String generatedAs(String generatedAs) { + return generatedAs.startsWith( "row " ) + ? " datetime2 generated always as " + generatedAs + : " as (" + generatedAs + ") persisted"; + } + @Override public TemporaryTableStrategy getLocalTemporaryTableStrategy() { return SQLServerLocalTemporaryTableStrategy.INSTANCE; @@ -1100,7 +1120,7 @@ public String getCreateIndexString(boolean unique) { @Override public String getCreateIndexTail(boolean unique, List columns) { if ( unique ) { - final StringBuilder tail = new StringBuilder(); + final var tail = new StringBuilder(); for ( Column column : columns ) { if ( column.isNullable() ) { tail.append( tail.isEmpty() ? " where " : " and " ) @@ -1161,11 +1181,6 @@ protected String getFormattedSequenceName(QualifiedSequenceName name, Metadata m } } - @Override - public String generatedAs(String generatedAs) { - return " as (" + generatedAs + ") persisted"; - } - @Override public boolean hasDataTypeBeforeGeneratedAs() { return false; @@ -1267,4 +1282,8 @@ public boolean supportsRowValueConstructorSyntaxInInList() { return false; } + @Override + public TemporalTableSupport getTemporalTableSupport() { + return new SQLServerTemporalTableSupport( this ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SimpleDatabaseVersion.java b/hibernate-core/src/main/java/org/hibernate/dialect/SimpleDatabaseVersion.java index e2368af65758..847a51a0f8cd 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SimpleDatabaseVersion.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SimpleDatabaseVersion.java @@ -21,20 +21,20 @@ public SimpleDatabaseVersion(DatabaseVersion copySource) { } public SimpleDatabaseVersion(DatabaseVersion version, boolean noVersionAsZero) { - this.major = version.getDatabaseMajorVersion(); + major = version.getDatabaseMajorVersion(); if ( version.getDatabaseMinorVersion() == NO_VERSION ) { - this.minor = noVersionAsZero ? 0 : NO_VERSION; + minor = noVersionAsZero ? 0 : NO_VERSION; } else { - this.minor = version.getDatabaseMinorVersion(); + minor = version.getDatabaseMinorVersion(); } if ( version.getDatabaseMicroVersion() == NO_VERSION ) { - this.micro = noVersionAsZero ? 0 : NO_VERSION; + micro = noVersionAsZero ? 0 : NO_VERSION; } else { - this.micro = version.getDatabaseMicroVersion(); + micro = version.getDatabaseMicroVersion(); } } @@ -87,7 +87,7 @@ public int getMicro() { @Override public String toString() { - StringBuilder version = new StringBuilder(); + final var version = new StringBuilder(); if ( major != NO_VERSION ) { version.append( major ); } @@ -103,15 +103,18 @@ public String toString() { } @Override - public boolean equals(Object o) { - if ( this == o ) { + public boolean equals(Object object) { + if ( this == object ) { return true; } - if ( o == null || getClass() != o.getClass() ) { + else if ( !( object instanceof SimpleDatabaseVersion that ) ) { return false; } - SimpleDatabaseVersion that = (SimpleDatabaseVersion) o; - return major == that.major && minor == that.minor && micro == that.micro; + else { + return major == that.major + && minor == that.minor + && micro == that.micro; + } } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SpannerDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/SpannerDialect.java index f1d82629dd10..c9c68d58b3a4 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SpannerDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SpannerDialect.java @@ -4,41 +4,69 @@ */ package org.hibernate.dialect; +import jakarta.persistence.GenerationType; import jakarta.persistence.TemporalType; import jakarta.persistence.Timeout; -import org.hibernate.LockMode; import org.hibernate.LockOptions; -import org.hibernate.StaleObjectStateException; -import org.hibernate.boot.Metadata; +import org.hibernate.ScrollMode; import org.hibernate.boot.model.FunctionContributions; -import org.hibernate.boot.model.relational.Exportable; -import org.hibernate.boot.model.relational.Sequence; -import org.hibernate.boot.model.relational.SqlStringGenerationContext; +import org.hibernate.boot.model.TypeContributions; +import org.hibernate.cfg.AvailableSettings; import org.hibernate.dialect.function.CommonFunctionFactory; -import org.hibernate.dialect.function.FormatFunction; -import org.hibernate.dialect.lock.LockingStrategy; -import org.hibernate.dialect.lock.LockingStrategyException; -import org.hibernate.dialect.lock.internal.NoLockingSupport; +import org.hibernate.dialect.function.array.SpannerArrayConcatElementFunction; +import org.hibernate.engine.config.spi.ConfigurationService; +import org.hibernate.engine.config.spi.StandardConverters; +import org.hibernate.engine.jdbc.env.spi.IdentifierCaseStrategy; +import org.hibernate.engine.jdbc.env.spi.IdentifierHelper; +import org.hibernate.engine.jdbc.env.spi.IdentifierHelperBuilder; +import org.hibernate.type.descriptor.sql.internal.DdlTypeImpl; +import org.hibernate.service.ServiceRegistry; +import org.hibernate.dialect.function.InsertSubstringOverlayEmulation; +import org.hibernate.dialect.function.CastingConcatFunction; +import org.hibernate.dialect.function.SpannerFormatFunction; +import org.hibernate.dialect.function.SpannerExtractFunction; +import org.hibernate.dialect.function.SpannerTruncFunction; +import org.hibernate.dialect.function.array.ArrayAggFunction; +import org.hibernate.dialect.function.array.ArrayToStringFunction; +import org.hibernate.dialect.identity.IdentityColumnSupport; +import org.hibernate.dialect.identity.SpannerIdentityColumnSupport; import org.hibernate.dialect.lock.spi.LockingSupport; +import org.hibernate.exception.ConstraintViolationException; +import org.hibernate.exception.SQLGrammarException; +import org.hibernate.exception.spi.SQLExceptionConversionDelegate; import org.hibernate.dialect.pagination.LimitHandler; import org.hibernate.dialect.pagination.LimitOffsetLimitHandler; +import org.hibernate.dialect.sequence.SequenceSupport; +import org.hibernate.dialect.sequence.SpannerSequenceSupport; +import org.hibernate.dialect.type.SpannerJsonJdbcType; +import org.hibernate.dialect.function.json.SpannerJsonValueFunction; +import org.hibernate.dialect.function.json.SpannerJsonQueryFunction; import org.hibernate.dialect.sql.ast.SpannerSqlAstTranslator; +import org.hibernate.dialect.unique.AlterTableUniqueIndexDelegate; import org.hibernate.dialect.unique.UniqueDelegate; import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo; import org.hibernate.engine.jdbc.env.spi.SchemaNameResolver; +import org.hibernate.sql.model.MutationOperation; +import org.hibernate.sql.model.ast.ColumnValueBinding; +import org.hibernate.sql.model.internal.OptionalTableUpdate; +import org.hibernate.persister.entity.mutation.EntityMutationTarget; import org.hibernate.engine.spi.SessionFactoryImplementor; -import org.hibernate.engine.spi.SharedSessionContractImplementor; -import org.hibernate.internal.util.collections.ArrayHelper; -import org.hibernate.mapping.Column; -import org.hibernate.mapping.ForeignKey; import org.hibernate.mapping.Table; -import org.hibernate.mapping.UniqueKey; -import org.hibernate.persister.entity.EntityPersister; import org.hibernate.query.SemanticException; import org.hibernate.query.common.TemporalUnit; +import org.hibernate.query.sqm.CastType; import org.hibernate.query.sqm.IntervalType; +import org.hibernate.query.sqm.SetOperator; +import org.hibernate.query.sqm.TrimSpec; +import org.hibernate.query.sqm.produce.function.FunctionParameterType; import org.hibernate.sql.ast.SqlAstTranslator; import org.hibernate.sql.ast.SqlAstTranslatorFactory; +import org.hibernate.sql.ast.internal.PessimisticLockKind; +import org.hibernate.dialect.lock.PessimisticLockStyle; +import org.hibernate.dialect.lock.internal.LockingSupportSimple; +import org.hibernate.dialect.lock.spi.ConnectionLockTimeoutStrategy; +import org.hibernate.dialect.lock.spi.LockTimeoutType; +import org.hibernate.dialect.lock.spi.OuterJoinLockingType; import org.hibernate.sql.ast.spi.LockingClauseStrategy; import org.hibernate.sql.ast.spi.SqlAppender; import org.hibernate.sql.ast.spi.StandardSqlAstTranslatorFactory; @@ -46,21 +74,39 @@ import org.hibernate.sql.ast.tree.select.QuerySpec; import org.hibernate.sql.exec.spi.JdbcOperation; import org.hibernate.tool.schema.spi.Exporter; -import org.hibernate.type.BasicType; -import org.hibernate.type.BasicTypeRegistry; +import org.hibernate.dialect.function.CountFunction; +import org.hibernate.sql.ast.SqlAstNodeRenderingMode; import org.hibernate.type.StandardBasicTypes; - -import java.util.Date; -import java.util.Map; +import org.hibernate.type.descriptor.java.PrimitiveByteArrayJavaType; +import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; +import org.hibernate.tool.schema.extract.spi.ColumnTypeInformation; +import org.hibernate.type.descriptor.sql.internal.CapacityDependentDdlType; +import org.hibernate.type.descriptor.sql.spi.DdlTypeRegistry; + +import java.sql.DatabaseMetaData; +import java.sql.SQLException; +import java.time.Instant; +import java.time.LocalDate; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; + +import org.hibernate.Timeouts; +import java.util.Calendar; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import static org.hibernate.dialect.SimpleDatabaseVersion.ZERO_VERSION; import static org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers.useArgType; import static org.hibernate.sql.ast.internal.NonLockingClauseStrategy.NON_CLAUSE_STRATEGY; +import static org.hibernate.type.SqlTypes.ARRAY; import static org.hibernate.type.SqlTypes.BIGINT; +import static org.hibernate.type.SqlTypes.TIMESTAMP_WITH_TIMEZONE; import static org.hibernate.type.SqlTypes.BINARY; import static org.hibernate.type.SqlTypes.BLOB; import static org.hibernate.type.SqlTypes.BOOLEAN; import static org.hibernate.type.SqlTypes.CHAR; +import static org.hibernate.type.SqlTypes.JSON; import static org.hibernate.type.SqlTypes.CLOB; import static org.hibernate.type.SqlTypes.DECIMAL; import static org.hibernate.type.SqlTypes.DOUBLE; @@ -77,10 +123,12 @@ import static org.hibernate.type.SqlTypes.SMALLINT; import static org.hibernate.type.SqlTypes.TIME; 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.VARBINARY; import static org.hibernate.type.SqlTypes.VARCHAR; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsDate; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTimestampWithMillis; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTimestampWithNanos; /** * A {@linkplain Dialect SQL dialect} for Cloud Spanner. @@ -89,16 +137,30 @@ * @author Chengyuan Zhao * @author Daniel Zou * @author Dmitry Solomakha + * @author Rayudu Abbireddy */ public class SpannerDialect extends Dialect { - private final Exporter
    spannerTableExporter = new SpannerDialectTableExporter( this ); + private final UniqueDelegate SPANNER_UNIQUE_DELEGATE = new AlterTableUniqueIndexDelegate( this ); + private final Exporter
    SPANNER_TABLE_EXPORTER = new SpannerDialectTableExporter( this ); + private final SequenceSupport SPANNER_SEQUENCE_SUPPORT = new SpannerSequenceSupport(this); - private static final LockingStrategy LOCKING_STRATEGY = new DoNothingLockingStrategy(); + private static final Pattern NOT_NULL_PATTERN = Pattern.compile( ".*Cannot specify a null value for column(?:[:]? (.*?) in table|: (.*?(?=$))).*" ); + private static final Pattern NOT_NULL_PATTERN_2 = Pattern.compile( ".*A new row in table .* does not specify a non-null value for NOT NULL column: (.*?)\\s*" ); + private static final Pattern UNIQUE_INDEX_PATTERN = Pattern.compile( ".*UNIQUE violation on index (.*?)(?:,|$).*" ); + private static final Pattern CHECK_PATTERN = Pattern.compile( ".*Check constraint (.*?) is violated.*" ); + private static final Pattern FK_PATTERN = Pattern.compile( ".*Foreign key (.*?) constraint violation.*" ); - private static final EmptyExporter NOOP_EXPORTER = new EmptyExporter(); + private static final String USE_INTEGER_FOR_PRIMARY_KEY = "hibernate.dialect.spanner.use_integer_for_primary_key"; + private boolean useIntegerForPrimaryKey; - private static final UniqueDelegate NOOP_UNIQUE_DELEGATE = new DoNothingUniqueDelegate(); + private static final LockingSupport SPANNER_LOCKING_SUPPORT = new LockingSupportSimple( + PessimisticLockStyle.CLAUSE, + RowLockStrategy.NONE, + LockTimeoutType.NONE, + OuterJoinLockingType.FULL, + ConnectionLockTimeoutStrategy.NONE + ); public SpannerDialect() { super( ZERO_VERSION ); @@ -108,69 +170,162 @@ public SpannerDialect(DialectResolutionInfo info) { super(info); } + public boolean useIntegerForPrimaryKey() { + return useIntegerForPrimaryKey; + } + @Override - protected String columnType(int sqlTypeCode) { - switch ( sqlTypeCode ) { - case BOOLEAN: - return "bool"; - - case TINYINT: - case SMALLINT: - case INTEGER: - case BIGINT: - return "int64"; - - case REAL: - case FLOAT: - case DOUBLE: - case DECIMAL: - case NUMERIC: - return "float64"; + public void contributeTypes(TypeContributions typeContributions, ServiceRegistry serviceRegistry) { + super.contributeTypes( typeContributions, serviceRegistry ); + final var configurationService = serviceRegistry.requireService( ConfigurationService.class ); + this.useIntegerForPrimaryKey = configurationService.getSetting( + USE_INTEGER_FOR_PRIMARY_KEY, + StandardConverters.BOOLEAN, + false + ); + final var jdbcTypeRegistry = typeContributions.getTypeConfiguration().getJdbcTypeRegistry(); + jdbcTypeRegistry.addDescriptor( SpannerJsonJdbcType.INSTANCE ); + } - //there is no time type of any kind - case TIME: - //timestamp does not accept precision - case TIMESTAMP: - case TIMESTAMP_WITH_TIMEZONE: - return "timestamp"; + @Override + public MutationOperation createOptionalTableUpdateOperation( + EntityMutationTarget mutationTarget, + OptionalTableUpdate optionalTableUpdate, + SessionFactoryImplementor factory) { + final boolean hasUpdatableBindings = optionalTableUpdate.getValueBindings().stream() + .anyMatch( ColumnValueBinding::isAttributeUpdatable ); + if ( hasUpdatableBindings ) { + // If an entity contains BOTH updatable properties and read-only properties + // (like `@Column(updatable = false)`), we MUST fall back to Hibernate's core UPDATE-then-INSERT + // mutation workflow to protect the immutable state from being unintentionally overwritten, + // as Spanner's native `INSERT OR UPDATE` statement updates all columns. + // Spanner's native `INSERT OR UPDATE` statement does not support a `WHERE` clause, + // so optimistic locking checks cannot be applied there. + final boolean hasNonUpdatableBindings = optionalTableUpdate.getValueBindings().stream() + .anyMatch( binding -> !binding.isAttributeUpdatable() ); + if ( hasNonUpdatableBindings || optionalTableUpdate.getNumberOfOptimisticLockBindings() > 0 ) { + return super.createOptionalTableUpdateOperation( mutationTarget, optionalTableUpdate, factory ); + } + } + return new SpannerSqlAstTranslator<>( factory, optionalTableUpdate ) + .createMergeOperation( optionalTableUpdate, hasUpdatableBindings ); + } - case CHAR: - case NCHAR: - case VARCHAR: - case NVARCHAR: - return "string($l)"; + @Override + protected void initDefaultProperties() { + super.initDefaultProperties(); + getDefaultProperties().setProperty( AvailableSettings.PREFERRED_POOLED_OPTIMIZER, "none" ); + } - case BINARY: - case VARBINARY: - return "bytes($l)"; + @Override + public JdbcType resolveSqlTypeDescriptor( + String columnTypeName, + int jdbcTypeCode, + int precision, + int scale, + JdbcTypeRegistry jdbcTypeRegistry) { + if ( jdbcTypeCode == ARRAY ) { + final int startIndex = columnTypeName.indexOf( '<' ) + 1; + final int endIndex = columnTypeName.lastIndexOf( '>' ); + final String componentTypeName = columnTypeName.substring( startIndex, endIndex ).trim(); + // Spanner uses STRING for VARCHAR/CLOB. DdlTypeRegistry prefers CLOB for "string". + final Integer sqlTypeCode = componentTypeName.equalsIgnoreCase( "STRING" ) + ? VARCHAR + : resolveSqlTypeCode( componentTypeName, jdbcTypeRegistry.getTypeConfiguration() ); + if ( sqlTypeCode != null ) { + return jdbcTypeRegistry.resolveTypeConstructorDescriptor( + jdbcTypeCode, + jdbcTypeRegistry.getDescriptor( sqlTypeCode ), + ColumnTypeInformation.EMPTY + ); + } + } + return super.resolveSqlTypeDescriptor( columnTypeName, jdbcTypeCode, precision, scale, jdbcTypeRegistry ); + } - case CLOB: - case NCLOB: - return "string(max)"; - case BLOB: - return "bytes(max)"; + @Override + protected void registerColumnTypes(TypeContributions typeContributions, ServiceRegistry serviceRegistry) { + super.registerColumnTypes( typeContributions, serviceRegistry ); + final DdlTypeRegistry ddlTypeRegistry = typeContributions.getTypeConfiguration().getDdlTypeRegistry(); + ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON, columnType( JSON ),this )); + ddlTypeRegistry.addDescriptor( + CapacityDependentDdlType.builder( + VARCHAR, + columnType( VARCHAR ), + castType( VARCHAR ), + castType( VARCHAR ), + this + ) + .withTypeCapacity( getMaxVarcharLength(), columnType( VARCHAR ) ) + .build() + ); + ddlTypeRegistry.addDescriptor( + CapacityDependentDdlType.builder( + NVARCHAR, + columnType( NVARCHAR ), + castType( NVARCHAR ), + castType( NVARCHAR ), + this + ) + .withTypeCapacity( getMaxNVarcharLength(), columnType( NVARCHAR ) ) + .build() + ); + ddlTypeRegistry.addDescriptor( + CapacityDependentDdlType.builder( + VARBINARY, + columnType( VARBINARY ), + castType( VARBINARY ), + castType( VARBINARY ), + this + ) + .withTypeCapacity( getMaxVarbinaryLength(), columnType( VARBINARY ) ) + .build() + ); + } - default: - return super.columnType( sqlTypeCode ); + @Override + protected String columnType(int sqlTypeCode) { + return switch ( sqlTypeCode ) { + case BOOLEAN -> "bool"; + case TINYINT, SMALLINT, INTEGER, BIGINT -> "int64"; + case REAL -> "float32"; + case FLOAT, DOUBLE -> "float64"; + case DECIMAL, NUMERIC -> "numeric"; + //there is no time type of any kind + //timestamp does not accept precision + case TIME, TIMESTAMP, TIMESTAMP_WITH_TIMEZONE -> "timestamp"; + case CHAR, NCHAR, VARCHAR, NVARCHAR -> "string($l)"; + case BINARY, VARBINARY -> "bytes($l)"; + case CLOB, NCLOB -> "string(max)"; + case BLOB -> "bytes(max)"; + case JSON -> "json"; + default -> super.columnType( sqlTypeCode ); + }; + } + + @Override + public String castPattern(CastType from, CastType to) { + if ( to == CastType.TIME && from == CastType.STRING ) { + return "cast('1970-01-01 ' || ?1 as timestamp)"; + } + if ( to == CastType.STRING && from == CastType.TIME ) { + return "format_timestamp('%H:%M:%E*S', ?1)"; } + return super.castPattern( from, to ); } @Override protected String castType(int sqlTypeCode) { - switch ( sqlTypeCode ) { - case CHAR: - case NCHAR: - case VARCHAR: - case NVARCHAR: - case LONG32VARCHAR: - case LONG32NVARCHAR: - return "string"; - case BINARY: - case VARBINARY: - case LONG32VARBINARY: - return "bytes"; - } - return super.castType( sqlTypeCode ); + return switch ( sqlTypeCode ) { + case CHAR, NCHAR, VARCHAR, NVARCHAR, LONG32VARCHAR, LONG32NVARCHAR, CLOB, NCLOB -> "string"; + case BINARY, VARBINARY, LONG32VARBINARY, BLOB -> "bytes"; + default -> super.castType( sqlTypeCode ); + }; + } + + @Override + public boolean supportsTruncateWithCast() { + return false; } @Override @@ -198,35 +353,48 @@ public String getArrayTypeName(String javaElementTypeName, String elementTypeNam @Override public void initializeFunctionRegistry(FunctionContributions functionContributions) { super.initializeFunctionRegistry( functionContributions ); - - final BasicTypeRegistry basicTypeRegistry = functionContributions.getTypeConfiguration().getBasicTypeRegistry(); - final BasicType byteArrayType = basicTypeRegistry.resolve( StandardBasicTypes.BINARY ); - final BasicType longType = basicTypeRegistry.resolve( StandardBasicTypes.LONG ); - final BasicType booleanType = basicTypeRegistry.resolve( StandardBasicTypes.BOOLEAN ); - final BasicType stringType = basicTypeRegistry.resolve( StandardBasicTypes.STRING ); - final BasicType dateType = basicTypeRegistry.resolve( StandardBasicTypes.DATE ); - final BasicType timestampType = basicTypeRegistry.resolve( StandardBasicTypes.TIMESTAMP ); + final var basicTypeRegistry = functionContributions.getTypeConfiguration().getBasicTypeRegistry(); + final var byteArrayType = basicTypeRegistry.resolve( StandardBasicTypes.BINARY ); + final var intType = basicTypeRegistry.resolve( StandardBasicTypes.INTEGER ); + final var longType = basicTypeRegistry.resolve( StandardBasicTypes.LONG ); + final var doubleType = basicTypeRegistry.resolve( StandardBasicTypes.DOUBLE ); + final var booleanType = basicTypeRegistry.resolve( StandardBasicTypes.BOOLEAN ); + final var charType = basicTypeRegistry.resolve( StandardBasicTypes.CHARACTER ); + final var stringType = basicTypeRegistry.resolve( StandardBasicTypes.STRING ); + final var dateType = basicTypeRegistry.resolve( StandardBasicTypes.DATE ); + final var timestampType = basicTypeRegistry.resolve( StandardBasicTypes.TIMESTAMP ); + + final var functionRegistry = functionContributions.getFunctionRegistry(); // Aggregate Functions - functionContributions.getFunctionRegistry().namedAggregateDescriptorBuilder( "any_value" ) + functionRegistry.namedAggregateDescriptorBuilder( "any_value" ) .setExactArgumentCount( 1 ) .register(); - functionContributions.getFunctionRegistry().namedAggregateDescriptorBuilder( "array_agg" ) - .setExactArgumentCount( 1 ) - .register(); - functionContributions.getFunctionRegistry().namedAggregateDescriptorBuilder( "countif" ) + functionRegistry.register( + "count", + new CountFunction( + this, + functionContributions.getTypeConfiguration(), + SqlAstNodeRenderingMode.DEFAULT, + "||", + "string", + false + ) + ); + functionRegistry.register( ArrayAggFunction.FUNCTION_NAME, new ArrayAggFunction( "array_agg", false, true ) ); + functionRegistry.namedAggregateDescriptorBuilder( "countif" ) .setInvariantType( longType ) .setExactArgumentCount( 1 ) .register(); - functionContributions.getFunctionRegistry().namedAggregateDescriptorBuilder( "logical_and" ) + functionRegistry.namedAggregateDescriptorBuilder( "logical_and" ) .setInvariantType( booleanType ) .setExactArgumentCount( 1 ) .register(); - functionContributions.getFunctionRegistry().namedAggregateDescriptorBuilder( "logical_or" ) + functionRegistry.namedAggregateDescriptorBuilder( "logical_or" ) .setInvariantType( booleanType ) .setExactArgumentCount( 1 ) .register(); - functionContributions.getFunctionRegistry().namedAggregateDescriptorBuilder( "string_agg" ) + functionRegistry.namedAggregateDescriptorBuilder( "string_agg" ) .setInvariantType( stringType ) .setArgumentCountBetween( 1, 2 ) .register(); @@ -243,37 +411,56 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.tanh(); functionFactory.moreHyperbolic(); - functionFactory.bitandorxornot_bitAndOrXorNot(); + functionRegistry.registerPattern( + "var_pop", + "(avg(?1 * ?1)-power(avg(?1),2))" ); + functionRegistry.registerPattern( + "stddev_pop", + "sqrt(avg(?1 * ?1)-power(avg(?1),2))" ); - functionContributions.getFunctionRegistry().namedDescriptorBuilder( "is_inf" ) + functionFactory.bitandorxornot_operator(); + + functionRegistry.namedDescriptorBuilder( "is_inf" ) .setInvariantType( booleanType ) .setExactArgumentCount( 1 ) .register(); - functionContributions.getFunctionRegistry().namedDescriptorBuilder( "is_nan" ) + functionRegistry.namedDescriptorBuilder( "is_nan" ) .setInvariantType( booleanType ) .setExactArgumentCount( 1 ) .register(); - functionContributions.getFunctionRegistry().namedDescriptorBuilder( "ieee_divide" ) + functionRegistry.namedDescriptorBuilder( "ieee_divide" ) .setInvariantType( booleanType ) .setExactArgumentCount( 2 ) .register(); - functionContributions.getFunctionRegistry().namedDescriptorBuilder( "div" ) + functionRegistry.namedDescriptorBuilder( "div" ) .setInvariantType( longType ) .setExactArgumentCount( 2 ) .register(); + functionRegistry.registerPattern( + "degrees", + "(?1 * 180 / acos(-1))", + doubleType ); + functionRegistry.registerPattern( + "radians", + "(?1 * acos(-1) / 180)", + doubleType ); + functionRegistry.registerPattern( + "log", + "log(?2, ?1)", + doubleType ); functionFactory.sha1(); // Hash Functions - functionContributions.getFunctionRegistry().namedDescriptorBuilder( "farm_fingerprint" ) + functionRegistry.namedDescriptorBuilder( "farm_fingerprint" ) .setInvariantType( longType ) .setExactArgumentCount( 1 ) .register(); - functionContributions.getFunctionRegistry().namedDescriptorBuilder( "sha256" ) + functionRegistry.namedDescriptorBuilder( "sha256" ) .setInvariantType( byteArrayType ) .setExactArgumentCount( 1 ) .register(); - functionContributions.getFunctionRegistry().namedDescriptorBuilder( "sha512" ) + functionRegistry.namedDescriptorBuilder( "sha512" ) .setInvariantType( byteArrayType ) .setExactArgumentCount( 1 ) .register(); @@ -285,205 +472,245 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.repeat(); functionFactory.substr(); functionFactory.substring_substr(); - functionContributions.getFunctionRegistry().namedDescriptorBuilder( "byte_length" ) + functionFactory.octetLength(); + functionFactory.bitLength_pattern( "(octet_length(?1) * 8)" ); + functionRegistry.namedDescriptorBuilder( "byte_length" ) .setInvariantType( longType ) .setExactArgumentCount( 1 ) .register(); - functionContributions.getFunctionRegistry().namedDescriptorBuilder( "code_points_to_bytes" ) + functionRegistry.namedDescriptorBuilder( "code_points_to_bytes" ) .setInvariantType( byteArrayType ) .setExactArgumentCount( 1 ) .register(); - functionContributions.getFunctionRegistry().namedDescriptorBuilder( "code_points_to_string" ) + functionRegistry.namedDescriptorBuilder( "code_points_to_string" ) .setInvariantType( stringType ) .setExactArgumentCount( 1 ) .register(); - functionContributions.getFunctionRegistry().namedDescriptorBuilder( "ends_with" ) + functionRegistry.namedDescriptorBuilder( "ends_with" ) .setInvariantType( booleanType ) .setExactArgumentCount( 2 ) .register(); // queryEngine.getSqmFunctionRegistry().namedTemplateBuilder( "format" ) // .setInvariantType( StandardBasicTypes.STRING ) // .register(); - functionContributions.getFunctionRegistry().namedDescriptorBuilder( "from_base64" ) + functionRegistry.namedDescriptorBuilder( "from_base64" ) .setInvariantType( byteArrayType ) .setExactArgumentCount( 1 ) .register(); - functionContributions.getFunctionRegistry().namedDescriptorBuilder( "from_hex" ) + functionRegistry.namedDescriptorBuilder( "from_hex" ) .setInvariantType( byteArrayType ) .setExactArgumentCount( 1 ) .register(); - functionContributions.getFunctionRegistry().namedDescriptorBuilder( "regexp_contains" ) + functionRegistry.namedDescriptorBuilder( "regexp_contains" ) .setInvariantType( booleanType ) .setExactArgumentCount( 2 ) .register(); - functionContributions.getFunctionRegistry().namedDescriptorBuilder( "regexp_extract" ) + functionRegistry.namedDescriptorBuilder( "regexp_extract" ) .setExactArgumentCount( 2 ) .register(); - functionContributions.getFunctionRegistry().namedDescriptorBuilder( "regexp_extract_all" ) + functionRegistry.namedDescriptorBuilder( "regexp_extract_all" ) .setExactArgumentCount( 2 ) .register(); - functionContributions.getFunctionRegistry().namedDescriptorBuilder( "regexp_replace" ) + functionRegistry.namedDescriptorBuilder( "regexp_replace" ) .setExactArgumentCount( 3 ) .register(); - functionContributions.getFunctionRegistry().namedDescriptorBuilder( "safe_convert_bytes_to_string" ) + functionRegistry.namedDescriptorBuilder( "safe_convert_bytes_to_string" ) .setInvariantType( stringType ) .setExactArgumentCount( 1 ) .register(); - functionContributions.getFunctionRegistry().namedDescriptorBuilder( "split" ) + functionRegistry.namedDescriptorBuilder( "split" ) .setArgumentCountBetween( 1, 2 ) .register(); - functionContributions.getFunctionRegistry().namedDescriptorBuilder( "starts_with" ) + functionRegistry.namedDescriptorBuilder( "starts_with" ) .setInvariantType( booleanType ) .setExactArgumentCount( 2 ) .register(); - functionContributions.getFunctionRegistry().namedDescriptorBuilder( "strpos" ) + functionRegistry.namedDescriptorBuilder( "strpos" ) .setInvariantType( longType ) .setExactArgumentCount( 2 ) .register(); - functionContributions.getFunctionRegistry().namedDescriptorBuilder( "to_base64" ) + functionRegistry.namedDescriptorBuilder( "to_base64" ) .setInvariantType( stringType ) .setExactArgumentCount( 1 ) .register(); - functionContributions.getFunctionRegistry().namedDescriptorBuilder( "to_code_points" ) + functionRegistry.namedDescriptorBuilder( "to_code_points" ) .setExactArgumentCount( 1 ) .register(); - functionContributions.getFunctionRegistry().namedDescriptorBuilder( "to_hex" ) + functionRegistry.namedDescriptorBuilder( "to_hex" ) .setInvariantType( stringType ) .setExactArgumentCount( 1 ) .register(); + functionRegistry.registerPattern( + "hex", + "to_hex(cast(?1 as bytes))", + stringType ); + functionRegistry.registerPattern( + "ascii", + "to_code_points(?1)[offset(0)]", + intType ); + functionRegistry.registerPattern( + "chr", + "code_points_to_string([?1])", + charType ); + functionRegistry.registerPattern( + "left", + "substr(?1, 1, ?2)", + stringType ); + functionRegistry.registerPattern( + "right", + "substr(?1, -?2)", + stringType ); + functionRegistry.register( + "overlay", + new InsertSubstringOverlayEmulation( functionContributions.getTypeConfiguration(), false ) ); + functionRegistry.registerBinaryTernaryPattern( + "locate", + intType, + "strpos(?2,?1)", + "(strpos(substr(?2,?3),?1)+case when strpos(substr(?2,?3),?1)>0 then ?3-1 else 0 end)", + FunctionParameterType.STRING, FunctionParameterType.STRING, FunctionParameterType.INTEGER, + functionContributions.getTypeConfiguration() + ) + .setArgumentListSignature( "(STRING pattern, STRING string[, INTEGER start])" ); // JSON Functions - functionContributions.getFunctionRegistry().namedDescriptorBuilder( "json_query" ) - .setInvariantType( stringType ) - .setExactArgumentCount( 2 ) - .register(); - functionContributions.getFunctionRegistry().namedDescriptorBuilder( "json_value" ) - .setInvariantType( stringType ) - .setExactArgumentCount( 2 ) - .register(); + functionRegistry.register( + "json_value", + new SpannerJsonValueFunction( functionContributions.getTypeConfiguration() ) ); + functionRegistry.register( + "json_query", + new SpannerJsonQueryFunction( functionContributions.getTypeConfiguration() ) ); // Array Functions - functionContributions.getFunctionRegistry().namedDescriptorBuilder( "array" ) + functionRegistry.namedDescriptorBuilder( "array" ) .setExactArgumentCount( 1 ) .register(); - functionContributions.getFunctionRegistry().namedDescriptorBuilder( "array_concat" ) - .register(); - functionContributions.getFunctionRegistry().namedDescriptorBuilder( "array_length" ) - .setInvariantType( longType ) - .setExactArgumentCount( 1 ) - .register(); - functionContributions.getFunctionRegistry().namedDescriptorBuilder( "array_to_string" ) - .setInvariantType( stringType ) - .setArgumentCountBetween( 2, 3 ) - .register(); - functionContributions.getFunctionRegistry().namedDescriptorBuilder( "array_reverse" ) + functionFactory.arrayConcat_operator(); + functionRegistry.register( "array_append", new SpannerArrayConcatElementFunction( false ) ); + functionRegistry.register( "array_prepend", new SpannerArrayConcatElementFunction( true ) ); + functionFactory.arrayLength_spanner(); + functionRegistry.register( "array_to_string", new ArrayToStringFunction( functionContributions.getTypeConfiguration() ) ); + functionRegistry.namedDescriptorBuilder( "array_reverse" ) .setExactArgumentCount( 1 ) .register(); // Date functions - functionContributions.getFunctionRegistry().namedDescriptorBuilder( "date" ) + functionRegistry.namedDescriptorBuilder( "date" ) .setInvariantType( dateType ) .setArgumentCountBetween( 1, 3 ) .register(); - functionContributions.getFunctionRegistry().namedDescriptorBuilder( "date_add" ) + functionRegistry.namedDescriptorBuilder( "date_add" ) .setInvariantType( dateType ) .setExactArgumentCount( 2 ) .register(); - functionContributions.getFunctionRegistry().namedDescriptorBuilder( "date_sub" ) + functionRegistry.namedDescriptorBuilder( "date_sub" ) .setInvariantType( dateType ) .setExactArgumentCount( 2 ) .register(); - functionContributions.getFunctionRegistry().namedDescriptorBuilder( "date_diff" ) + functionRegistry.namedDescriptorBuilder( "date_diff" ) .setInvariantType( longType ) .setExactArgumentCount( 3 ) .register(); - functionContributions.getFunctionRegistry().namedDescriptorBuilder( "date_trunc" ) + functionRegistry.namedDescriptorBuilder( "date_trunc" ) .setReturnTypeResolver( useArgType( 1 ) ) .setExactArgumentCount( 2 ) .register(); - functionContributions.getFunctionRegistry().namedDescriptorBuilder( "date_from_unix_date" ) + functionRegistry.namedDescriptorBuilder( "date_from_unix_date" ) .setInvariantType( dateType ) .setExactArgumentCount( 1 ) .register(); - functionContributions.getFunctionRegistry().namedDescriptorBuilder( "format_date" ) + functionRegistry.namedDescriptorBuilder( "format_date" ) .setInvariantType( stringType ) .setExactArgumentCount( 2 ) .register(); - functionContributions.getFunctionRegistry().namedDescriptorBuilder( "parse_date" ) + functionRegistry.namedDescriptorBuilder( "parse_date" ) .setInvariantType( dateType ) .setExactArgumentCount( 2 ) .register(); - functionContributions.getFunctionRegistry().namedDescriptorBuilder( "unix_date" ) + functionRegistry.namedDescriptorBuilder( "unix_date" ) .setInvariantType( longType ) .setExactArgumentCount( 1 ) .register(); // Timestamp functions - functionContributions.getFunctionRegistry().namedDescriptorBuilder( "string" ) - .setInvariantType( stringType ) - .setArgumentCountBetween( 1, 2 ) - .register(); - functionContributions.getFunctionRegistry().namedDescriptorBuilder( "timestamp" ) + functionRegistry.namedDescriptorBuilder( "timestamp" ) .setInvariantType( timestampType ) .setArgumentCountBetween( 1, 2 ) .register(); - functionContributions.getFunctionRegistry().namedDescriptorBuilder( "timestamp_add" ) + functionRegistry.namedDescriptorBuilder( "timestamp_add" ) .setInvariantType( timestampType ) .setExactArgumentCount( 2 ) .register(); - functionContributions.getFunctionRegistry().namedDescriptorBuilder( "timestamp_sub" ) + functionRegistry.namedDescriptorBuilder( "timestamp_sub" ) .setInvariantType( timestampType ) .setExactArgumentCount( 2 ) .register(); - functionContributions.getFunctionRegistry().namedDescriptorBuilder( "timestamp_diff" ) + functionRegistry.namedDescriptorBuilder( "timestamp_diff" ) .setInvariantType( longType ) .setExactArgumentCount( 3 ) .register(); - functionContributions.getFunctionRegistry().namedDescriptorBuilder( "timestamp_trunc" ) + functionRegistry.namedDescriptorBuilder( "timestamp_trunc" ) .setInvariantType( timestampType ) .setArgumentCountBetween( 2, 3 ) .register(); - functionContributions.getFunctionRegistry().namedDescriptorBuilder( "format_timestamp" ) + functionRegistry.namedDescriptorBuilder( "format_timestamp" ) .setInvariantType( stringType ) .setArgumentCountBetween( 2, 3 ) .register(); - functionContributions.getFunctionRegistry().namedDescriptorBuilder( "parse_timestamp" ) + functionRegistry.namedDescriptorBuilder( "parse_timestamp" ) .setInvariantType( timestampType ) .setArgumentCountBetween( 2, 3 ) .register(); - functionContributions.getFunctionRegistry().namedDescriptorBuilder( "timestamp_seconds" ) + functionRegistry.namedDescriptorBuilder( "timestamp_seconds" ) .setInvariantType( timestampType ) .setExactArgumentCount( 1 ) .register(); - functionContributions.getFunctionRegistry().namedDescriptorBuilder( "timestamp_millis" ) + functionRegistry.namedDescriptorBuilder( "timestamp_millis" ) .setInvariantType( timestampType ) .setExactArgumentCount( 1 ) .register(); - functionContributions.getFunctionRegistry().namedDescriptorBuilder( "timestamp_micros" ) + functionRegistry.namedDescriptorBuilder( "timestamp_micros" ) .setInvariantType( timestampType ) .setExactArgumentCount( 1 ) .register(); - functionContributions.getFunctionRegistry().namedDescriptorBuilder( "unix_seconds" ) + functionRegistry.namedDescriptorBuilder( "unix_seconds" ) .setInvariantType( longType ) .setExactArgumentCount( 1 ) .register(); - functionContributions.getFunctionRegistry().namedDescriptorBuilder( "unix_millis" ) + functionRegistry.namedDescriptorBuilder( "unix_millis" ) .setInvariantType( longType ) .setExactArgumentCount( 1 ) .register(); - functionContributions.getFunctionRegistry().namedDescriptorBuilder( "unix_micros" ) + functionRegistry.namedDescriptorBuilder( "unix_micros" ) .setInvariantType( longType ) .setExactArgumentCount( 1 ) .register(); + functionFactory.listagg_stringAgg( "string" ); + functionFactory.array_spanner(); - functionContributions.getFunctionRegistry().register( + functionRegistry.register( + "extract", + new SpannerExtractFunction( this, functionContributions.getTypeConfiguration() ) + ); + + functionRegistry.register( "format", - new FormatFunction( "format_timestamp", true, true, functionContributions.getTypeConfiguration() ) + new SpannerFormatFunction( functionContributions.getTypeConfiguration() ) ); - functionFactory.listagg_stringAgg( "string" ); - functionFactory.inverseDistributionOrderedSetAggregates(); - functionFactory.hypotheticalOrderedSetAggregates(); - functionFactory.array_spanner(); + + functionRegistry.register( + "concat", + new CastingConcatFunction( + this, + "||", + false, + SqlAstNodeRenderingMode.DEFAULT, + functionContributions.getTypeConfiguration() + ) + ); + + functionRegistry.register( "trunc", new SpannerTruncFunction() ); + functionRegistry.registerAlternateKey( "truncate", "trunc" ); } @Override @@ -498,8 +725,8 @@ protected SqlAstTranslator buildTranslator( } @Override - public Exporter
    getTableExporter() { - return this.spannerTableExporter; + public boolean supportsIfExistsBeforeTableName() { + return true; } /* SELECT-related functions */ @@ -524,6 +751,178 @@ public void appendBooleanValueString(SqlAppender appender, boolean bool) { appender.appendSql( bool ); } + @Override + public void appendBinaryLiteral(SqlAppender appender, byte[] bytes) { + appender.appendSql( "FROM_HEX('" ); + PrimitiveByteArrayJavaType.INSTANCE.appendString( appender, bytes ); + appender.appendSql( "')" ); + } + + @Override + public void appendLiteral(SqlAppender appender, String literal) { + // Spanner uses backslash escaping, so escape single quotes and backslashes with \ + // We also explicitly escape newlines (\n) because Spanner forbids raw line breaks + // inside standard quoted strings + StringBuilder builder = new StringBuilder( literal.length() + 2 ); + builder.append( '\'' ); + for ( int i = 0; i < literal.length(); i++ ) { + final char c = literal.charAt( i ); + switch ( c ) { + case '\'': + case '\\': + builder.append( '\\' ); + builder.append( c ); + break; + case '\n': + builder.append( "\\n" ); + break; + default: + builder.append( c ); + } + } + builder.append( '\'' ); + appender.appendSql( builder.toString() ); + } + + @Override + public String currentTime() { + return currentTimestamp(); + } + + @Override + public boolean supportsTemporalLiteralOffset() { + return true; + } + + @Override + public void appendDateTimeLiteral( + org.hibernate.sql.ast.spi.SqlAppender appender, + java.time.temporal.TemporalAccessor temporalAccessor, + jakarta.persistence.TemporalType precision, + java.util.TimeZone jdbcTimeZone) { + switch ( precision ) { + case DATE: + appender.appendSql( "DATE '" ); + appendAsDate( appender, temporalAccessor ); + appender.appendSql( "'" ); + break; + case TIME: + appender.appendSql( "TIMESTAMP '" ); + if ( temporalAccessor instanceof java.time.LocalTime localTime ) { + final OffsetDateTime offsetDateTime = localTime + .atDate( LocalDate.of( 1970, 1, 1 ) ) + .atOffset( ZoneOffset.UTC ); + appendAsTimestampWithNanos( appender, offsetDateTime, true, jdbcTimeZone ); + } + else if ( temporalAccessor instanceof java.time.OffsetTime offsetTime ) { + OffsetDateTime offsetDateTime = + offsetTime.atDate( LocalDate.of( 1970, 1, 1 ) ); + appendAsTimestampWithNanos( appender, offsetDateTime, true, jdbcTimeZone ); + } + appender.appendSql( "'" ); + break; + case TIMESTAMP: + appender.appendSql( "TIMESTAMP '" ); + if ( temporalAccessor instanceof java.time.LocalDateTime ldt ) { + appendAsTimestampWithNanos( + appender, + ldt.atOffset( ZoneOffset.UTC ), + supportsTemporalLiteralOffset(), + jdbcTimeZone + ); + } + else { + appendAsTimestampWithNanos( + appender, + temporalAccessor, + supportsTemporalLiteralOffset(), + jdbcTimeZone + ); + } + appender.appendSql( "'" ); + break; + default: + throw new IllegalArgumentException( "Unsupported TemporalType: " + precision ); + } + } + + @Override + public void appendDateTimeLiteral( + org.hibernate.sql.ast.spi.SqlAppender appender, + java.util.Date date, + jakarta.persistence.TemporalType precision, + java.util.TimeZone jdbcTimeZone) { + switch ( precision ) { + case DATE: + appender.appendSql( "DATE '" ); + appendAsDate( appender, date ); + appender.appendSql( "'" ); + break; + case TIME: + appender.appendSql( "TIMESTAMP '" ); + if ( date instanceof java.sql.Time time ) { + final OffsetDateTime offsetDateTime = time.toLocalTime() + .atDate( LocalDate.of( 1970, 1, 1 ) ) + .atOffset( ZoneOffset.UTC ); + appendAsTimestampWithNanos( appender, offsetDateTime, true, jdbcTimeZone ); + } + appender.appendSql( "'" ); + break; + case TIMESTAMP: + appender.appendSql( "TIMESTAMP '" ); + appendAsTimestampWithNanos( + appender, + date.toInstant(), + supportsTemporalLiteralOffset(), + jdbcTimeZone + ); + appender.appendSql( "'" ); + break; + default: + throw new IllegalArgumentException( "Unsupported TemporalType: " + precision ); + } + } + + @Override + public void appendDateTimeLiteral( + org.hibernate.sql.ast.spi.SqlAppender appender, + java.util.Calendar calendar, + jakarta.persistence.TemporalType precision, + java.util.TimeZone jdbcTimeZone) { + switch ( precision ) { + case DATE: + appender.appendSql( "DATE '" ); + appendAsDate( appender, calendar ); + appender.appendSql( "'" ); + break; + case TIME: + appender.appendSql( "TIMESTAMP '" ); + final OffsetDateTime offsetDateTime = Instant.EPOCH.atOffset( ZoneOffset.UTC ) + .withHour( calendar.get( Calendar.HOUR_OF_DAY ) ) + .withMinute( calendar.get( Calendar.MINUTE ) ) + .withSecond( calendar.get( Calendar.SECOND ) ) + .withNano( calendar.get( Calendar.MILLISECOND ) * 1_000_000 ); + appendAsTimestampWithMillis( appender, offsetDateTime, true, jdbcTimeZone ); + appender.appendSql( "'" ); + break; + case TIMESTAMP: + appender.appendSql( "TIMESTAMP '" ); + final OffsetDateTime odt = OffsetDateTime.ofInstant( + calendar.toInstant(), + calendar.getTimeZone().toZoneId() ); + appendAsTimestampWithMillis( + appender, + odt, + supportsTemporalLiteralOffset(), + jdbcTimeZone + ); + appender.appendSql( "'" ); + break; + default: + throw new IllegalArgumentException( "Unsupported TemporalType: " + precision ); + } + } + @Override public String translateExtractField(TemporalUnit unit) { switch (unit) { @@ -542,52 +941,64 @@ public String translateExtractField(TemporalUnit unit) { @Override public String timestampaddPattern(TemporalUnit unit, TemporalType temporalType, IntervalType intervalType) { - if ( temporalType == TemporalType.TIMESTAMP ) { - switch (unit) { + if ( temporalType == TemporalType.TIMESTAMP || temporalType == TemporalType.TIME ) { + switch ( unit ) { case YEAR: case QUARTER: case MONTH: - throw new SemanticException("Illegal unit for timestamp_add(): " + unit); + throw new SemanticException( "Illegal unit for timestamp_add(): " + unit ); + case WEEK: + return "timestamp_add(?3, interval cast(?2 * 7 as int64) day)"; + case SECOND: + return "timestamp_add(?3, interval cast(?2 * 1000000000 as int64) nanosecond)"; default: - return "timestamp_add(?3,interval ?2 ?1)"; + return "timestamp_add(?3, interval cast(?2 as int64) ?1)"; } } else { - switch (unit) { + switch ( unit ) { case NANOSECOND: case SECOND: case MINUTE: case HOUR: case NATIVE: - throw new SemanticException("Illegal unit for date_add(): " + unit); + throw new SemanticException( "Illegal unit for date_add(): " + unit ); default: - return "date_add(?3,interval ?2 ?1)"; + return "date_add(?3, interval cast(?2 as int64) ?1)"; } } } @Override public String timestampdiffPattern(TemporalUnit unit, TemporalType fromTemporalType, TemporalType toTemporalType) { - if ( toTemporalType == TemporalType.TIMESTAMP || fromTemporalType == TemporalType.TIMESTAMP ) { - switch (unit) { + if ( toTemporalType == TemporalType.TIMESTAMP || fromTemporalType == TemporalType.TIMESTAMP + || toTemporalType == TemporalType.TIME || fromTemporalType == TemporalType.TIME ) { + switch ( unit ) { case YEAR: case QUARTER: case MONTH: - throw new SemanticException("Illegal unit for timestamp_diff(): " + unit); + throw new SemanticException( "Illegal unit for timestamp_diff(): " + unit ); + case WEEK: + return "div(timestamp_diff(?3, ?2, day), 7)"; + case NATIVE: + return "timestamp_diff(?3, ?2, nanosecond)"; default: - return "timestamp_diff(?3,?2,?1)"; + return "timestamp_diff(?3, ?2, ?1)"; } } else { - switch (unit) { + switch ( unit ) { case NANOSECOND: + case NATIVE: + return "(date_diff(?3, ?2, day) * 86400000000000)"; case SECOND: + return "(date_diff(?3, ?2, day) * 86400)"; case MINUTE: + return "(date_diff(?3, ?2, day) * 1440)"; case HOUR: - case NATIVE: - throw new SemanticException("Illegal unit for date_diff(): " + unit); + return "(date_diff(?3, ?2, day) * 24)"; default: - return "date_diff(?3,?2,?1)"; + return "date_diff(?3, ?2, ?1)"; } } } @@ -634,51 +1045,40 @@ public static Replacer datetimeFormat(String format) { .replace("xx", "%z"); //note special case } - /* DDL-related functions */ - - @Override - public boolean canCreateSchema() { - return false; - } - @Override - public String[] getCreateSchemaCommand(String schemaName) { - throw new UnsupportedOperationException( - "No create schema syntax supported by " + getClass().getName() ); + public String trimPattern(TrimSpec specification, boolean isWhitespace) { + return switch ( specification ) { + case LEADING -> isWhitespace ? "ltrim(?1)" : "ltrim(?1, ?2)"; + case TRAILING -> isWhitespace ? "rtrim(?1)" : "rtrim(?1, ?2)"; + default -> isWhitespace ? "trim(?1)" : "trim(?1, ?2)"; + }; } - @Override - public String[] getDropSchemaCommand(String schemaName) { - throw new UnsupportedOperationException( - "No drop schema syntax supported by " + getClass().getName() ); - } + /* DDL-related functions */ @Override - public String getCurrentSchemaCommand() { - throw new UnsupportedOperationException( - "No current schema syntax supported by " + getClass().getName() ); + public Exporter
    getTableExporter() { + return SPANNER_TABLE_EXPORTER; } @Override - public SchemaNameResolver getSchemaNameResolver() { - // Spanner does not have a notion of database name schemas, so return "". - return (connection, dialect) -> ""; + public boolean supportsColumnCheck() { + return false; } @Override - public boolean dropConstraints() { - return false; + public String getCreateIndexString(boolean unique) { + return unique ? "create unique null_filtered index" : "create index"; } @Override - public boolean qualifyIndexName() { + public boolean supportsUniqueConstraints() { return false; } @Override - public String getDropForeignKeyString() { - throw new UnsupportedOperationException( - "Cannot drop foreign-key constraint because Cloud Spanner does not support foreign keys." ); + public UniqueDelegate getUniqueDelegate() { + return SPANNER_UNIQUE_DELEGATE; } @Override @@ -688,176 +1088,212 @@ public String getAddForeignKeyConstraintString( String referencedTable, String[] primaryKey, boolean referencesPrimaryKey) { - throw new UnsupportedOperationException( - "Cannot add foreign-key constraint because Cloud Spanner does not support foreign keys." ); + // Spanner requires the referenced columns to specify in all cases, including + // if the foreign key is referencing the primary key of the referenced table. Setting referencesPrimaryKey to + // false will add all the referenced columns. + return super.getAddForeignKeyConstraintString( constraintName, foreignKey, referencedTable, primaryKey, false ); } @Override - public String getAddForeignKeyConstraintString( - String constraintName, - String foreignKeyDefinition) { - throw new UnsupportedOperationException( - "Cannot add foreign-key constraint because Cloud Spanner does not support foreign keys." ); + public boolean supportsCircularCascadeDeleteConstraints() { + return false; } @Override - public String getAddPrimaryKeyConstraintString(String constraintName) { - throw new UnsupportedOperationException( "Cannot add primary key constraint in Cloud Spanner." ); + public String getColumnDefaultString(String defaultValue) { + if ( defaultValue != null && !defaultValue.startsWith( "(" ) ) { + return "(" + defaultValue + ")"; + } + return defaultValue; } - // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Lock acquisition functions - - @Override - public LockingSupport getLockingSupport() { - return NoLockingSupport.NO_LOCKING_SUPPORT; + public boolean requiresNotNullBeforeDefault() { + return true; } @Override - public LockingClauseStrategy getLockingClauseStrategy(QuerySpec querySpec, LockOptions lockOptions) { - // Spanner does not support the FOR UPDATE clause - return NON_CLAUSE_STRATEGY; + public String generatedAs(String generatedAs) { + return " as (" + generatedAs + ") stored"; } @Override - public LockingStrategy getLockingStrategy(EntityPersister lockable, LockMode lockMode) { - return LOCKING_STRATEGY; + public boolean supportsNotNullAfterGeneratedAs() { + return false; } @Override - public String getForUpdateString(LockOptions lockOptions) { - return ""; + public boolean supportsNoColumnsInsert() { + return false; } @Override - public String getForUpdateString() { - return ""; + public IdentityColumnSupport getIdentityColumnSupport() { + return SpannerIdentityColumnSupport.INSTANCE; } @Override - public String getForUpdateString(String aliases) { - throw new UnsupportedOperationException( - "Cloud Spanner does not support selecting for lock acquisition." ); + public SequenceSupport getSequenceSupport() { + return SPANNER_SEQUENCE_SUPPORT; } @Override - public String getForUpdateString(String aliases, LockOptions lockOptions) { - throw new UnsupportedOperationException( - "Cloud Spanner does not support selecting for lock acquisition." ); + public String getQuerySequencesString() { + return """ + select seq.CATALOG as sequence_catalog, + seq.SCHEMA as sequence_schema, + seq.NAME as sequence_name, + coalesce(kind.OPTION_VALUE, 'bit_reversed_positive') as KIND, + coalesce(safe_cast(initial.OPTION_VALUE AS INT64), + case coalesce(kind.OPTION_VALUE, 'bit_reversed_positive') + when 'bit_reversed_positive' then 1 + when 'bit_reversed_signed' then -pow(2, 63) + else 1 + end + ) as start_value, 1 as minimum_value, 9223372036854775807 as maximum_value, + 1 as increment, + safe_cast(skip_range_min.OPTION_VALUE as int64) as skip_range_min, + safe_cast(skip_range_max.OPTION_VALUE as int64) as skip_range_max + from INFORMATION_SCHEMA.SEQUENCES seq + left outer join INFORMATION_SCHEMA.SEQUENCE_OPTIONS kind + on seq.CATALOG=kind.CATALOG and seq.SCHEMA=kind.SCHEMA and seq.NAME=kind.NAME and kind.OPTION_NAME='sequence_kind' + left outer join INFORMATION_SCHEMA.SEQUENCE_OPTIONS initial + on seq.CATALOG=initial.CATALOG and seq.SCHEMA=initial.SCHEMA and seq.NAME=initial.NAME and initial.OPTION_NAME='start_with_counter' + left outer join INFORMATION_SCHEMA.SEQUENCE_OPTIONS skip_range_min + on seq.CATALOG=skip_range_min.CATALOG and seq.SCHEMA=skip_range_min.SCHEMA and seq.NAME=skip_range_min.NAME and skip_range_min.OPTION_NAME='skip_range_min' + left outer join INFORMATION_SCHEMA.SEQUENCE_OPTIONS skip_range_max + on seq.CATALOG=skip_range_max.CATALOG and seq.SCHEMA=skip_range_max.SCHEMA and seq.NAME=skip_range_max.NAME and skip_range_max.OPTION_NAME='skip_range_max' + """; } @Override - public String getWriteLockString(Timeout timeout) { - throw new UnsupportedOperationException( - "Cloud Spanner does not support selecting for lock acquisition." ); + public GenerationType getNativeValueGenerationStrategy() { + return GenerationType.SEQUENCE; } @Override - public String getWriteLockString(String aliases, Timeout timeout) { - throw new UnsupportedOperationException( - "Cloud Spanner does not support selecting for lock acquisition." ); + public boolean canCreateSchema() { + return false; } @Override - public String getReadLockString(Timeout timeout) { + public String[] getCreateSchemaCommand(String schemaName) { throw new UnsupportedOperationException( - "Cloud Spanner does not support selecting for lock acquisition." ); + "No create schema syntax supported by " + getClass().getName() ); } @Override - public String getReadLockString(String aliases, Timeout timeout) { + public String[] getDropSchemaCommand(String schemaName) { throw new UnsupportedOperationException( - "Cloud Spanner does not support selecting for lock acquisition." ); + "No drop schema syntax supported by " + getClass().getName() ); } @Override - public String getWriteLockString(int timeout) { + public String getCurrentSchemaCommand() { throw new UnsupportedOperationException( - "Cloud Spanner does not support selecting for lock acquisition." ); + "No current schema syntax supported by " + getClass().getName() ); } @Override - public String getWriteLockString(String aliases, int timeout) { - throw new UnsupportedOperationException( - "Cloud Spanner does not support selecting for lock acquisition." ); + public SchemaNameResolver getSchemaNameResolver() { + // Spanner does not have a notion of database name schemas, so return "". + return (connection, dialect) -> ""; } @Override - public String getReadLockString(int timeout) { - throw new UnsupportedOperationException( - "Cloud Spanner does not support selecting for lock acquisition." ); + public boolean qualifyIndexName() { + return false; } @Override - public String getReadLockString(String aliases, int timeout) { - throw new UnsupportedOperationException( - "Cloud Spanner does not support selecting for lock acquisition." ); + public String getAddPrimaryKeyConstraintString(String constraintName) { + throw new UnsupportedOperationException( "Cannot add primary key constraint in Cloud Spanner." ); } + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Lock acquisition functions + @Override - public String getForUpdateNowaitString() { - throw new UnsupportedOperationException( - "Cloud Spanner does not support selecting for lock acquisition." ); + public LockingSupport getLockingSupport() { + return SPANNER_LOCKING_SUPPORT; } @Override - public String getForUpdateNowaitString(String aliases) { - throw new UnsupportedOperationException( - "Cloud Spanner does not support selecting for lock acquisition." ); + public LockingClauseStrategy getLockingClauseStrategy(QuerySpec querySpec, LockOptions lockOptions) { + if ( getPessimisticLockStyle() != PessimisticLockStyle.CLAUSE || lockOptions == null ) { + return NON_CLAUSE_STRATEGY; + } + final var lockKind = PessimisticLockKind.interpret( lockOptions.getLockMode() ); + if ( lockKind == PessimisticLockKind.NONE ) { + return NON_CLAUSE_STRATEGY; + } + if ( lockOptions.getTimeout() != null ) { + validateSpannerLockTimeout( lockOptions.getTimeout().milliseconds() ); + } + return buildLockingClauseStrategy( + lockKind, RowLockStrategy.NONE, lockOptions, querySpec.getRootPathsForLocking() ); } - @Override - public String getForUpdateSkipLockedString() { - throw new UnsupportedOperationException( - "Cloud Spanner does not support selecting for lock acquisition." ); + public String getForUpdateString(LockOptions lockOptions) { + if ( lockOptions != null && lockOptions.getTimeout() != null ) { + validateSpannerLockTimeout( lockOptions.getTimeout().milliseconds() ); + } + return getForUpdateString(); } @Override - public String getForUpdateSkipLockedString(String aliases) { - throw new UnsupportedOperationException( - "Cloud Spanner does not support selecting for lock acquisition." ); + public String getWriteLockString(int timeout) { + validateSpannerLockTimeout( timeout ); + return getForUpdateString(); } - /* Unsupported Hibernate Exporters */ - @Override - public Exporter getSequenceExporter() { - return NOOP_EXPORTER; + public String getWriteLockString(Timeout timeout) { + return getWriteLockString( timeout.milliseconds() ); } @Override - public Exporter getForeignKeyExporter() { - return NOOP_EXPORTER; + public String getReadLockString(int timeout) { + validateSpannerLockTimeout( timeout ); + return getForUpdateString(); } @Override - public Exporter getUniqueKeyExporter() { - return NOOP_EXPORTER; + public String getReadLockString(Timeout timeout) { + return getReadLockString( timeout.milliseconds() ); } @Override - public String applyLocksToSql( - String sql, - LockOptions aliasedLockOptions, - Map keyColumnNames) { - return sql; + public String getForUpdateNowaitString() { + throw new UnsupportedOperationException( "Spanner does not support no wait." ); } @Override - public UniqueDelegate getUniqueDelegate() { - return NOOP_UNIQUE_DELEGATE; + public String getForUpdateNowaitString(String aliases) { + throw new UnsupportedOperationException( "Spanner does not support no wait." ); } @Override - public boolean supportsCircularCascadeDeleteConstraints() { - return false; + public String getForUpdateSkipLockedString() { + throw new UnsupportedOperationException( "Spanner does not support skip locked." ); } @Override - public boolean supportsCascadeDelete() { - return false; + public String getForUpdateSkipLockedString(String aliases) { + throw new UnsupportedOperationException( "Spanner does not support skip locked." ); + } + + private static void validateSpannerLockTimeout(int millis) { + if ( Timeouts.isRealTimeout( millis ) ) { + throw new UnsupportedOperationException( "Spanner does not support lock timeout." ); + } + if ( millis == Timeouts.SKIP_LOCKED_MILLI ) { + throw new UnsupportedOperationException( "Spanner does not support skip locked." ); + } + if ( millis == Timeouts.NO_WAIT_MILLI ) { + throw new UnsupportedOperationException( "Spanner does not support no wait." ); + } } @Override @@ -895,68 +1331,145 @@ public boolean supportsRowValueConstructorSyntaxInInList() { return false; } - /* Type conversion and casting */ - - /** - * A no-op {@link Exporter} which is responsible for returning empty Create and Drop SQL strings. - * - * @author Daniel Zou - */ - static class EmptyExporter implements Exporter { + @Override + public DmlTargetColumnQualifierSupport getDmlTargetColumnQualifierSupport() { + return DmlTargetColumnQualifierSupport.TABLE_ALIAS; + } - @Override - public String[] getSqlCreateStrings(T exportable, Metadata metadata, SqlStringGenerationContext context) { - return ArrayHelper.EMPTY_STRING_ARRAY; + @Override + public IdentifierHelper buildIdentifierHelper( + IdentifierHelperBuilder builder, + DatabaseMetaData metadata) throws SQLException { + if ( metadata == null ) { + builder.setUnquotedCaseStrategy( IdentifierCaseStrategy.MIXED ); } + builder.applyReservedWords( metadata ); + builder.setAutoQuoteKeywords( true ); + builder.setAutoQuoteDollar( true ); + return super.buildIdentifierHelper( builder, metadata ); + } - @Override - public String[] getSqlDropStrings(T exportable, Metadata metadata, SqlStringGenerationContext context) { - return ArrayHelper.EMPTY_STRING_ARRAY; - } + @Override + public ScrollMode defaultScrollMode() { + return ScrollMode.FORWARD_ONLY; } - /** - * A locking strategy for the Cloud Spanner dialect that does nothing. Cloud Spanner does not - * support locking. - * - * @author Chengyuan Zhao - */ - static class DoNothingLockingStrategy implements LockingStrategy { + @Override + public String getTruncateTableStatement(String tableName) { + // spanner doesn't have truncate command, so we delete + return "delete from " + tableName + " where true"; + } - @Override - public void lock( - Object id, Object version, Object object, int timeout, SharedSessionContractImplementor session) - throws StaleObjectStateException, LockingStrategyException { - // Do nothing. Cloud Spanner doesn't have have locking strategies. - } + @Override + public String getSetOperatorSqlString(SetOperator operator) { + return switch ( operator ) { + case UNION -> "union distinct"; + case INTERSECT -> "intersect distinct"; + case EXCEPT -> "except distinct"; + default -> super.getSetOperatorSqlString( operator ); + }; } - /** - * A no-op delegate for generating Unique-Constraints. Cloud Spanner offers unique-restrictions - * via interleaved indexes with the "UNIQUE" option. This is not currently supported. - * - * @author Chengyuan Zhao - */ - static class DoNothingUniqueDelegate implements UniqueDelegate { + @Override + public String getDual() { + return "unnest([1])"; + } - @Override - public String getColumnDefinitionUniquenessFragment(Column column, SqlStringGenerationContext context) { - return ""; - } + @Override + public String getFromDualForSelectOnly() { + return " from " + getDual() + " dual"; + } - @Override - public String getTableCreationUniqueConstraintsFragment(Table table, SqlStringGenerationContext context) { - return ""; - } + @Override + public boolean supportsLateral() { + // Spanner does not support the `LATERAL` keyword natively. + // However, we return true here because `SpannerSqlAstTranslator` emulates + // lateral joins using the `UNNEST(ARRAY(select as struct..)) alias` syntax. + return true; + } - @Override - public String getAlterTableToAddUniqueKeyCommand(UniqueKey uniqueKey, Metadata metadata, SqlStringGenerationContext context) { - return ""; - } + @Override + public boolean supportsLobValueChangePropagation() { + return false; + } + + @Override + public NullOrdering getNullOrdering() { + return NullOrdering.SMALLEST; + } + + @Override + public boolean supportsNullPrecedence() { + return false; + } + + @Override + public boolean supportsWithClauseInSubquery() { + return false; + } - @Override - public String getAlterTableToDropUniqueKeyCommand(UniqueKey uniqueKey, Metadata metadata, SqlStringGenerationContext context) { - return ""; + @Override + public boolean supportsCteHeaderColumnList() { + return false; + } + + @Override + public boolean supportsTupleDistinctCounts() { + return false; + } + + @Override + public SQLExceptionConversionDelegate buildSQLExceptionConversionDelegate() { + return (sqlException, message, sql) -> { + final String sqlMessage = sqlException.getMessage(); + if ( sqlMessage != null ) { + Matcher matcher = NOT_NULL_PATTERN.matcher( sqlMessage ); + if ( matcher.matches() ) { + String group = matcher.group( 1 ) != null ? matcher.group( 1 ) : matcher.group( 2 ); + return new ConstraintViolationException( message, sqlException, sql, ConstraintViolationException.ConstraintKind.NOT_NULL, extractConstraintName( group ) ); + } + + matcher = NOT_NULL_PATTERN_2.matcher( sqlMessage ); + if ( matcher.matches() ) { + return new ConstraintViolationException( message, sqlException, sql, ConstraintViolationException.ConstraintKind.NOT_NULL, extractConstraintName( matcher.group( 1 ) ) ); + } + + matcher = UNIQUE_INDEX_PATTERN.matcher( sqlMessage ); + if ( matcher.matches() ) { + return new ConstraintViolationException( message, sqlException, sql, ConstraintViolationException.ConstraintKind.UNIQUE, extractConstraintName( matcher.group( 1 ) ) ); + } + + if ( sqlMessage.contains( "Failed to insert row with primary key" ) ) { + return new ConstraintViolationException( message, sqlException, sql, ConstraintViolationException.ConstraintKind.UNIQUE, null ); + } + + if ( sqlMessage.contains( "Table not found" ) ) { + return new SQLGrammarException( message, sqlException, sql ); + } + + matcher = CHECK_PATTERN.matcher( sqlMessage ); + if ( matcher.matches() ) { + return new ConstraintViolationException( message, sqlException, sql, ConstraintViolationException.ConstraintKind.CHECK, extractConstraintName( matcher.group( 1 ) ) ); + } + + matcher = FK_PATTERN.matcher( sqlMessage ); + if ( matcher.matches() ) { + return new ConstraintViolationException( message, sqlException, sql, ConstraintViolationException.ConstraintKind.FOREIGN_KEY, extractConstraintName( matcher.group( 1 ) ) ); + } + } + return null; + }; + } + + private String extractConstraintName(String name) { + if ( name == null ) { + return null; + } + name = name.replace( "`", "" ).replace( "\"", "" ).replace( "'", "" ).trim(); + int dotIndex = name.lastIndexOf( '.' ); + if ( dotIndex > -1 ) { + name = name.substring( dotIndex + 1 ); } + return name; } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SpannerDialectTableExporter.java b/hibernate-core/src/main/java/org/hibernate/dialect/SpannerDialectTableExporter.java index a5e9d0dc339a..47ca8999727d 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SpannerDialectTableExporter.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SpannerDialectTableExporter.java @@ -4,22 +4,19 @@ */ package org.hibernate.dialect; -import java.text.MessageFormat; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.StringJoiner; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; - import org.hibernate.boot.Metadata; import org.hibernate.boot.model.relational.SqlStringGenerationContext; import org.hibernate.mapping.Column; -import org.hibernate.mapping.Index; +import org.hibernate.mapping.PrimaryKey; import org.hibernate.mapping.Table; -import org.hibernate.tool.schema.spi.Exporter; +import org.hibernate.mapping.UniqueKey; +import org.hibernate.mapping.RootClass; +import org.hibernate.tool.schema.internal.ColumnValue; +import org.hibernate.tool.schema.internal.StandardTableExporter; -import static org.hibernate.internal.util.collections.ArrayHelper.EMPTY_STRING_ARRAY; +import java.sql.Types; +import java.util.ArrayList; +import java.util.stream.Stream; /** * The exporter for Cloud Spanner CREATE and DROP table statements. @@ -27,90 +24,73 @@ * @author Chengyuan Zhao * @author Daniel Zou */ -class SpannerDialectTableExporter implements Exporter
    { +class SpannerDialectTableExporter extends StandardTableExporter { - private final SpannerDialect spannerDialect; - - private final String createTableTemplate; - - /** - * Constructor. - * - * @param spannerDialect a Cloud Spanner dialect. - */ public SpannerDialectTableExporter(SpannerDialect spannerDialect) { - this.spannerDialect = spannerDialect; - this.createTableTemplate = - this.spannerDialect.getCreateTableString() + " {0} ({1}) PRIMARY KEY ({2})"; + super( spannerDialect ); } @Override public String[] getSqlCreateStrings(Table table, Metadata metadata, SqlStringGenerationContext context) { + // Spanner mandates that primary key should be present in all the tables. For element collection tables or + // sequence tables, there will be no primary key. In order to fix the problem, we randomly generate + // the ID column with BIT_REVERSED_POSITIVE sequence + if ( !table.hasPrimaryKey() && (!table.getForeignKeyCollection().isEmpty() || isSequenceTable(table, context) || isHistoryOrAuditedTable(table, metadata))) { + Column column = getAutoGeneratedPrimaryKeyColumn( table, metadata ); + table.addColumn( column ); - Collection keyColumns; + PrimaryKey primaryKey = new PrimaryKey( table ); + primaryKey.addColumn( column ); - if ( table.hasPrimaryKey() ) { - // a typical table that corresponds to an entity type - keyColumns = table.getPrimaryKey().getColumns(); - } - else if ( !table.getForeignKeyCollection().isEmpty() ) { - // a table with no PK's but has FK's; often corresponds to element collection properties - keyColumns = table.getColumns(); - } - else { - // the case corresponding to a sequence-table that will only have 1 row. - keyColumns = Collections.emptyList(); + table.setPrimaryKey( primaryKey ); } - return getTableString( table, metadata, keyColumns, context ); + return super.getSqlCreateStrings( table, metadata, context ); } - private String[] getTableString(Table table, Metadata metadata, Iterable keyColumns, SqlStringGenerationContext context) { - String primaryKeyColNames = StreamSupport.stream( keyColumns.spliterator(), false ) - .map( Column::getName ) - .collect( Collectors.joining( "," ) ); - - StringJoiner colsAndTypes = new StringJoiner( "," ); - - - for ( Column column : table.getColumns() ) { - final String sqlType = column.getSqlType( metadata ); - final String columnDeclaration = - column.getName() - + " " + sqlType - + ( column.isNullable() ? this.spannerDialect.getNullColumnString( sqlType ) : " not null" ); - colsAndTypes.add( columnDeclaration ); - } - - ArrayList statements = new ArrayList<>(); - statements.add( - MessageFormat.format( - this.createTableTemplate, - context.format( table.getQualifiedTableName() ), - colsAndTypes.toString(), - primaryKeyColNames - ) - ); - - return statements.toArray(EMPTY_STRING_ARRAY); + private boolean isSequenceTable(Table table, SqlStringGenerationContext context) { + return table.getColumnSpan() == 1 && table.getInitCommands( context ).size() == 1; } @Override public String[] getSqlDropStrings(Table table, Metadata metadata, SqlStringGenerationContext context) { - - /* Cloud Spanner requires examining the metadata to find all indexes and interleaved tables. - * These must be dropped before the given table can be dropped. - * The current implementation does not support interleaved tables. - */ - - ArrayList dropStrings = new ArrayList<>(); - - for ( Index index : table.getIndexes().values() ) { - dropStrings.add( "drop index " + index.getName() ); + final ArrayList sqlDropIndexStrings = new ArrayList<>(); + for ( var index : table.getIndexes().values() ) { + sqlDropIndexStrings.add( sqlDropIndexString( index.getName() ) ); + } + for ( UniqueKey uniqueKey : table.getUniqueKeys().values() ) { + sqlDropIndexStrings.add( sqlDropIndexString( uniqueKey.getName() ) ); } + for ( Column column : table.getColumns() ) { + if ( column.isUnique() ) { + sqlDropIndexStrings.add( sqlDropIndexString( column.getUniqueKeyName() ) ); + } + } + String[] sqlDropStrings = super.getSqlDropStrings( table, metadata, context ); + return Stream.concat( sqlDropIndexStrings.stream(), Stream.of( sqlDropStrings ) ) + .toArray( String[]::new ); + } - dropStrings.add( this.spannerDialect.getDropTableString( context.format( table.getQualifiedTableName() ) ) ); + private String sqlDropIndexString(String indexName) { + return "drop index if exists " + indexName; + } + + private Column getAutoGeneratedPrimaryKeyColumn(Table table, Metadata metadata) { + Column column = new Column( "rowid" ); + column.setSqlTypeCode( Types.BIGINT ); + column.setNullable( false ); + column.setSqlType( "int64" ); + column.setIdentity( true ); + column.setOptions( "hidden" ); + column.setValue( new ColumnValue( metadata.getDatabase(), table, column, metadata.getDatabase().getTypeConfiguration().getBasicTypeForJavaType( Long.class ) ) ); + return column; + } - return dropStrings.toArray( new String[0] ); + private boolean isHistoryOrAuditedTable(Table targetTable, Metadata metadata) { + return metadata.getCollectionBindings().stream() + .anyMatch( collection -> collection.getAuxiliaryTable() == targetTable ) + || metadata.getEntityBindings().stream() + .filter( entity -> entity instanceof RootClass ) + .anyMatch( entity -> ((RootClass) entity).getAuxiliaryTable() == targetTable ); } } 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 fccfbd3fa031..594644b06e99 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SybaseASEDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseASEDialect.java @@ -42,7 +42,6 @@ import org.hibernate.type.descriptor.jdbc.TimestampJdbcType; import org.hibernate.type.descriptor.jdbc.TinyIntJdbcType; import org.hibernate.type.descriptor.sql.internal.CapacityDependentDdlType; -import org.hibernate.type.descriptor.sql.spi.DdlTypeRegistry; import java.sql.SQLException; import java.sql.Types; @@ -119,6 +118,21 @@ public SybaseASEDialect(DialectResolutionInfo info) { pageSize = pageSize( info ); } + @Override + public DatabaseVersion determineDatabaseVersion(DialectResolutionInfo info) { + if ( SybaseDriverKind.determineKind( info ) == SybaseDriverKind.JTDS + && info.getDatabaseMinorVersion() != DatabaseVersion.NO_VERSION ) { + // The jTDS driver encodes the SP part into the minor version, so we have to unpack this + final int infoMinorVersion = info.getDatabaseMinorVersion(); + final int minorVersion = infoMinorVersion / 10; + final int microVersion = infoMinorVersion % 10; + return new SimpleDatabaseVersion( info.getDatabaseMajorVersion(), minorVersion, microVersion ); + } + else { + return super.determineDatabaseVersion( info ); + } + } + @Override protected String columnType(int sqlTypeCode) { return switch ( sqlTypeCode ) { @@ -138,7 +152,7 @@ protected String columnType(int sqlTypeCode) { @Override protected void registerColumnTypes(TypeContributions typeContributions, ServiceRegistry serviceRegistry) { super.registerColumnTypes( typeContributions, serviceRegistry ); - final DdlTypeRegistry ddlTypeRegistry = typeContributions.getTypeConfiguration().getDdlTypeRegistry(); + final var ddlTypeRegistry = typeContributions.getTypeConfiguration().getDdlTypeRegistry(); // According to Wikipedia bigdatetime and bigtime were added in 15.5 // But with jTDS we can't use them as the driver can't handle the types @@ -205,8 +219,8 @@ public long getDefaultLobLength() { private static boolean isAnsiNull(DialectResolutionInfo info) { final var databaseMetaData = info.getDatabaseMetadata(); if ( databaseMetaData != null ) { - try ( var statement = databaseMetaData.getConnection().createStatement() ) { - final var resultSet = statement.executeQuery( "SELECT @@options" ); + try ( var statement = databaseMetaData.getConnection().createStatement(); + var resultSet = statement.executeQuery( "SELECT @@options" ) ) { if ( resultSet.next() ) { final byte[] optionBytes = resultSet.getBytes( 1 ); // By trial and error, enabling and disabling ansinull revealed that this bit is the indicator @@ -224,8 +238,8 @@ private static boolean isAnsiNull(DialectResolutionInfo info) { private int pageSize(DialectResolutionInfo info) { final var databaseMetaData = info.getDatabaseMetadata(); if ( databaseMetaData != null ) { - try ( var statement = databaseMetaData.getConnection().createStatement() ) { - final var resultSet = statement.executeQuery( "SELECT @@maxpagesize" ); + try ( var statement = databaseMetaData.getConnection().createStatement(); + var resultSet = statement.executeQuery( "SELECT @@maxpagesize" ) ) { if ( resultSet.next() ) { return resultSet.getInt( 1 ); } @@ -343,17 +357,15 @@ public String timestampaddPattern(TemporalUnit unit, TemporalType temporalType, @Override public String timestampdiffPattern(TemporalUnit unit, TemporalType fromTemporalType, TemporalType toTemporalType) { - switch ( unit ) { - case NANOSECOND: - return "(cast(datediff(ms,?2,?3) as numeric(21))*1000000)"; -// return "(cast(datediff(mcs,?2,?3) as numeric(21))*1000)"; -// } - case NATIVE: - return "cast(datediff(ms,?2,?3) as numeric(21))"; -// return "cast(datediff(mcs,cast(?2 as bigdatetime),cast(?3 as bigdatetime)) as numeric(21))"; - default: - return "datediff(?1,?2,?3)"; - } + return switch ( unit ) { + case NANOSECOND -> +// "(cast(datediff(mcs,?2,?3) as numeric(21))*1000)"; + "(cast(datediff(ms,?2,?3) as numeric(21))*1000000)"; + case NATIVE -> +// "cast(datediff(mcs,cast(?2 as bigdatetime),cast(?3 as bigdatetime)) as numeric(21))"; + "cast(datediff(ms,?2,?3) as numeric(21))"; + default -> "datediff(?1,?2,?3)"; + }; } @Override @@ -644,13 +656,15 @@ public String toQuotedIdentifier(String name) { if ( name == null || name.isEmpty() ) { return name; } - if ( name.charAt( 0 ) == '#' ) { + else if ( name.charAt( 0 ) == '#' ) { // Temporary tables must start with a '#' character, // but Sybase doesn't support quoting of such identifiers, // so we simply don't apply quoting in this case return name; } - return super.toQuotedIdentifier( name ); + else { + return super.toQuotedIdentifier( name ); + } } @Override @@ -695,7 +709,15 @@ public SQLExceptionConversionDelegate buildSQLExceptionConversionDelegate() { new QueryTimeoutException( message, sqlException, sql ); case "JZ0TO", "JZ006" -> new LockTimeoutException( message, sqlException, sql ); - case "S1000", "23000" -> + case "S1000" -> { + if ( errorCode == 12205 ) { + yield new LockTimeoutException( message, sqlException, sql ); + } + else { + yield convertConstraintViolation( sqlException, message, sql, errorCode ); + } + } + case "23000" -> convertConstraintViolation( sqlException, message, sql, errorCode ); case "ZZZZZ" -> { if ( 515 == errorCode ) { @@ -703,6 +725,9 @@ public SQLExceptionConversionDelegate buildSQLExceptionConversionDelegate() { yield new ConstraintViolationException( message, sqlException, sql, ConstraintKind.NOT_NULL, getViolatedConstraintNameExtractor().extractConstraintName( sqlException ) ); } + else if ( errorCode == 12205 ) { + yield new LockTimeoutException( message, sqlException, sql ); + } else { yield null; } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SybaseDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseDialect.java index a7bca1173da3..805f168a8305 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SybaseDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseDialect.java @@ -231,25 +231,35 @@ public int getParameterCountLimit() { @Override public void contributeTypes(TypeContributions typeContributions, ServiceRegistry serviceRegistry) { super.contributeTypes(typeContributions, serviceRegistry); - final JdbcTypeRegistry jdbcTypeRegistry = typeContributions.getTypeConfiguration() + final var jdbcTypeRegistry = typeContributions.getTypeConfiguration() .getJdbcTypeRegistry(); if ( driverKind == SybaseDriverKind.JTDS ) { - jdbcTypeRegistry.addDescriptor( Types.TINYINT, TinyIntAsSmallIntJdbcType.INSTANCE ); + jdbcTypeRegistry.addDescriptor( TinyIntAsSmallIntJdbcType.INSTANCE ); // The jTDS driver doesn't support the JDBC4 signatures using 'long length' for stream bindings - jdbcTypeRegistry.addDescriptor( Types.CLOB, ClobJdbcType.CLOB_BINDING ); - jdbcTypeRegistry.addDescriptor( Types.NCLOB, NClobJdbcType.NCLOB_BINDING ); + jdbcTypeRegistry.addDescriptor( ClobJdbcType.CLOB_BINDING ); + + // Need to register specialized JdbcType instances for jTDS because it throws an AbstractMethodError + // when invoking nationalized methods and requires binding through UTF-16LE bytes + jdbcTypeRegistry.addDescriptor( SybaseJtdsNClobJdbcType.JTDS_INSTANCE ); + jdbcTypeRegistry.addDescriptor( SybaseJtdsNCharJdbcType.JTDS_INSTANCE ); + jdbcTypeRegistry.addDescriptor( SybaseJtdsNVarcharJdbcType.JTDS_INSTANCE ); + jdbcTypeRegistry.addDescriptor( SybaseJtdsLongNVarcharJdbcType.JTDS_INSTANCE ); + jdbcTypeRegistry.addDescriptor( SybaseJtdsJsonAsStringJdbcType.JTDS_INSTANCE ); + jdbcTypeRegistry.addDescriptor( SybaseJtdsXmlAsStringJdbcType.JTDS_INSTANCE ); + jdbcTypeRegistry.addTypeConstructor( SybaseJtdsJsonAsStringArrayJdbcTypeConstructor.INSTANCE ); + jdbcTypeRegistry.addTypeConstructor( SybaseJtdsXmlAsStringArrayJdbcTypeConstructor.INSTANCE ); } else { // jConnect driver only conditionally supports getClob/getNClob depending on a server setting. See // - https://help.sap.com/doc/e3cb6844decf441e85e4670e1cf48c9b/16.0.3.6/en-US/SAP_jConnect_Programmers_Reference_en.pdf // - https://infocenter.sybase.com/help/index.jsp?topic=/com.sybase.infocenter.dc20155.1570/html/OS_SDK_nf/CIHJFDDH.htm // - HHH-7889 - jdbcTypeRegistry.addDescriptor( Types.CLOB, ClobJdbcType.STREAM_BINDING_EXTRACTING ); - jdbcTypeRegistry.addDescriptor( Types.NCLOB, ClobJdbcType.STREAM_BINDING_EXTRACTING ); + jdbcTypeRegistry.addDescriptor( ClobJdbcType.STREAM_BINDING_EXTRACTING ); + jdbcTypeRegistry.addDescriptor( NClobJdbcType.STREAM_BINDING_EXTRACTING ); } - jdbcTypeRegistry.addDescriptor( Types.BLOB, BlobJdbcType.PRIMITIVE_ARRAY_BINDING ); + jdbcTypeRegistry.addDescriptor( BlobJdbcType.PRIMITIVE_ARRAY_BINDING ); // Sybase requires a custom binder for binding untyped nulls with the NULL type typeContributions.contributeJdbcType( ObjectNullAsBinaryTypeJdbcType.INSTANCE ); @@ -279,6 +289,12 @@ public NationalizationSupport getNationalizationSupport() { return super.getNationalizationSupport(); } + @Override + public boolean supportsNationalizedMethods() { + // The jTDS driver doesn't support nationalized methods, but the jconn driver does + return driverKind != SybaseDriverKind.JTDS; + } + @Override public boolean stripsTrailingSpacesFromChar() { return true; @@ -604,10 +620,9 @@ public String resolveSchemaName(Connection connection, Dialect dialect) throws S ); } - try ( var statement = connection.createStatement() ) { - try ( var resultSet = statement.executeQuery( command ) ) { - return resultSet.next() ? resultSet.getString( 1 ) : null; - } + try ( var statement = connection.createStatement(); + var resultSet = statement.executeQuery( command ) ) { + return resultSet.next() ? resultSet.getString( 1 ) : null; } } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SybaseDriverKind.java b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseDriverKind.java index f6eac930a591..d8f7cb50d2c1 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SybaseDriverKind.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseDriverKind.java @@ -21,13 +21,10 @@ public static SybaseDriverKind determineKind(DialectResolutionInfo dialectResolu if ( driverName == null ) { return OTHER; } - switch ( driverName ) { - case "jConnect (TM) for JDBC (TM)": - return JCONNECT; - case "jTDS Type 4 JDBC Driver for MS SQL Server and Sybase": - return JTDS; - default: - return OTHER; - } + return switch ( driverName ) { + case "jConnect (TM) for JDBC (TM)" -> JCONNECT; + case "jTDS Type 4 JDBC Driver for MS SQL Server and Sybase" -> JTDS; + default -> OTHER; + }; } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SybaseJtdsJsonAsStringArrayJdbcType.java b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseJtdsJsonAsStringArrayJdbcType.java new file mode 100644 index 000000000000..e8490d5ca34a --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseJtdsJsonAsStringArrayJdbcType.java @@ -0,0 +1,134 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect; + +import org.hibernate.type.SqlTypes; +import org.hibernate.type.descriptor.ValueBinder; +import org.hibernate.type.descriptor.ValueExtractor; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.jdbc.BasicBinder; +import org.hibernate.type.descriptor.jdbc.BasicExtractor; +import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators; +import org.hibernate.type.descriptor.jdbc.JsonAsStringArrayJdbcType; + +import java.nio.charset.StandardCharsets; +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * Specialized type mapping for {@code JSON} that binds UTF-16LE bytes, + * because the jTDS driver can't handle Unicode characters and doesn't support nationalized methods. + * For extraction, the {@code getString} method works fine though. + */ +public class SybaseJtdsJsonAsStringArrayJdbcType extends JsonAsStringArrayJdbcType { + + public SybaseJtdsJsonAsStringArrayJdbcType(JdbcType elementJdbcType) { + super( elementJdbcType, SqlTypes.NCLOB ); + } + + public SybaseJtdsJsonAsStringArrayJdbcType(JdbcType elementJdbcType, int ddlTypeCode) { + super( elementJdbcType, ddlTypeCode ); + } + + @Override + public String toString() { + return "SybaseJtdsXmlAsStringArrayJdbcType"; + } + + @Override + public JdbcType resolveIndicatedType(JdbcTypeIndicators indicators, JavaType domainJtd) { + // Depending on the size of the column, we might have to adjust the jdbc type code for DDL. + // In some DBMS we can compare LOBs with special functions which is handled in the SqlAstTranslators, + // but that requires the correct jdbc type code to be available, which we ensure this way + if ( needsLob( indicators ) ) { + return indicators.isNationalized() + ? new SybaseJtdsJsonAsStringArrayJdbcType( getElementJdbcType(), SqlTypes.NCLOB ) + : new SybaseJtdsJsonAsStringArrayJdbcType( getElementJdbcType(), SqlTypes.CLOB ); + } + else { + return indicators.isNationalized() + ? new SybaseJtdsJsonAsStringArrayJdbcType( getElementJdbcType(), SqlTypes.LONG32NVARCHAR ) + : new SybaseJtdsJsonAsStringArrayJdbcType( getElementJdbcType(), SqlTypes.LONG32VARCHAR ); + } + } + + @Override + public ValueBinder getBinder(JavaType javaType) { + if ( !isNationalized() ) { + return super.getBinder( javaType ); + } + return new BasicBinder<>( javaType, this ) { + + private SybaseJtdsJsonAsStringArrayJdbcType getJsonAsStringArrayJdbcType() { + return (SybaseJtdsJsonAsStringArrayJdbcType) getJdbcType(); + } + + @Override + protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) + throws SQLException { + final SybaseJtdsJsonAsStringArrayJdbcType jdbcType = getJsonAsStringArrayJdbcType(); + final String xml = jdbcType.toString( value, getJavaType(), options ); + st.setBytes( index, xml.getBytes( StandardCharsets.UTF_16LE ) ); + } + + @Override + protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) + throws SQLException { + final SybaseJtdsJsonAsStringArrayJdbcType jdbcType = getJsonAsStringArrayJdbcType(); + final String xml = jdbcType.toString( value, getJavaType(), options ); + st.setBytes( name, xml.getBytes( StandardCharsets.UTF_16LE ) ); + } + + @Override + protected void doBindNull(PreparedStatement st, int index, WrapperOptions options) throws SQLException { + st.setNull( index, SqlTypes.VARCHAR ); + } + + @Override + protected void doBindNull(CallableStatement st, String name, WrapperOptions options) + throws SQLException { + st.setNull( name, SqlTypes.VARCHAR ); + } + }; + } + + @Override + public ValueExtractor getExtractor(JavaType javaType) { + if ( !isNationalized() ) { + return super.getExtractor( javaType ); + } + return new BasicExtractor<>( javaType, this ) { + + private SybaseJtdsJsonAsStringArrayJdbcType getJsonAsStringArrayJdbcType() { + return (SybaseJtdsJsonAsStringArrayJdbcType) getJdbcType(); + } + + @Override + protected X doExtract(ResultSet rs, int paramIndex, WrapperOptions options) throws SQLException { + final String value = rs.getString( paramIndex ); + return getJsonAsStringArrayJdbcType().fromString( value, getJavaType(), options ); + } + + @Override + protected X doExtract(CallableStatement statement, int index, WrapperOptions options) + throws SQLException { + final String value = statement.getString( index ); + return getJsonAsStringArrayJdbcType().fromString( value, getJavaType(), options ); + } + + @Override + protected X doExtract(CallableStatement statement, String name, WrapperOptions options) + throws SQLException { + final String value = statement.getString( name ); + return getJsonAsStringArrayJdbcType().fromString( value, getJavaType(), options ); + } + + }; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SybaseJtdsJsonAsStringArrayJdbcTypeConstructor.java b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseJtdsJsonAsStringArrayJdbcTypeConstructor.java new file mode 100644 index 000000000000..2c6b4763aa3b --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseJtdsJsonAsStringArrayJdbcTypeConstructor.java @@ -0,0 +1,41 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect; + + +import org.hibernate.tool.schema.extract.spi.ColumnTypeInformation; +import org.hibernate.type.BasicType; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.descriptor.jdbc.JdbcTypeConstructor; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * Factory for {@link SybaseJtdsJsonAsStringArrayJdbcType}. + */ +public class SybaseJtdsJsonAsStringArrayJdbcTypeConstructor implements JdbcTypeConstructor { + public static final SybaseJtdsJsonAsStringArrayJdbcTypeConstructor INSTANCE = new SybaseJtdsJsonAsStringArrayJdbcTypeConstructor(); + + public JdbcType resolveType( + TypeConfiguration typeConfiguration, + Dialect dialect, + BasicType elementType, + ColumnTypeInformation columnTypeInformation) { + return resolveType( typeConfiguration, dialect, elementType.getJdbcType(), columnTypeInformation ); + } + + public JdbcType resolveType( + TypeConfiguration typeConfiguration, + Dialect dialect, + JdbcType elementType, + ColumnTypeInformation columnTypeInformation) { + return new SybaseJtdsJsonAsStringArrayJdbcType( elementType, SqlTypes.NCLOB ); + } + + @Override + public int getDefaultSqlTypeCode() { + return SqlTypes.JSON_ARRAY; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SybaseJtdsJsonAsStringJdbcType.java b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseJtdsJsonAsStringJdbcType.java new file mode 100644 index 000000000000..1af946157aaa --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseJtdsJsonAsStringJdbcType.java @@ -0,0 +1,164 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect; + +import org.hibernate.metamodel.mapping.EmbeddableMappingType; +import org.hibernate.metamodel.spi.RuntimeModelCreationContext; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.descriptor.ValueBinder; +import org.hibernate.type.descriptor.ValueExtractor; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.jdbc.AggregateJdbcType; +import org.hibernate.type.descriptor.jdbc.BasicBinder; +import org.hibernate.type.descriptor.jdbc.BasicExtractor; +import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators; +import org.hibernate.type.descriptor.jdbc.JsonAsStringJdbcType; + +import java.nio.charset.StandardCharsets; +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * Specialized type mapping for {@code JSON} that binds UTF-16LE bytes, + * because the jTDS driver can't handle Unicode characters and doesn't support nationalized methods. + * For extraction, the {@code getString} method works fine though. + */ +public class SybaseJtdsJsonAsStringJdbcType extends JsonAsStringJdbcType { + + public static final SybaseJtdsJsonAsStringJdbcType JTDS_VARCHAR_INSTANCE = + new SybaseJtdsJsonAsStringJdbcType( SqlTypes.LONG32VARCHAR, null ); + public static final SybaseJtdsJsonAsStringJdbcType JTDS_NVARCHAR_INSTANCE = + new SybaseJtdsJsonAsStringJdbcType( SqlTypes.LONG32NVARCHAR, null ); + public static final SybaseJtdsJsonAsStringJdbcType JTDS_CLOB_INSTANCE = + new SybaseJtdsJsonAsStringJdbcType( SqlTypes.CLOB, null ); + public static final SybaseJtdsJsonAsStringJdbcType JTDS_INSTANCE = + new SybaseJtdsJsonAsStringJdbcType( SqlTypes.NCLOB, null ); + + public SybaseJtdsJsonAsStringJdbcType(int ddlTypeCode, EmbeddableMappingType embeddableMappingType) { + super( ddlTypeCode, embeddableMappingType ); + } + + @Override + public AggregateJdbcType resolveAggregateJdbcType( + EmbeddableMappingType mappingType, + String sqlType, + RuntimeModelCreationContext creationContext) { + return new SybaseJtdsJsonAsStringJdbcType( getDdlTypeCode(), mappingType ); + } + + @Override + public String toString() { + return "SybaseJtdsJsonAsStringJdbcType"; + } + + @Override + public JdbcType resolveIndicatedType(JdbcTypeIndicators indicators, JavaType domainJtd) { + // Depending on the size of the column, we might have to adjust the jdbc type code for DDL. + // In some DBMS we can compare LOBs with special functions which is handled in the SqlAstTranslators, + // but that requires the correct jdbc type code to be available, which we ensure this way + if ( getEmbeddableMappingType() == null ) { + if ( needsLob( indicators ) ) { + return indicators.isNationalized() ? JTDS_INSTANCE : JTDS_CLOB_INSTANCE; + } + else { + return indicators.isNationalized() ? JTDS_NVARCHAR_INSTANCE : JTDS_VARCHAR_INSTANCE; + } + } + else { + if ( needsLob( indicators ) ) { + return new SybaseJtdsJsonAsStringJdbcType( + indicators.isNationalized() ? SqlTypes.NCLOB : SqlTypes.CLOB, + getEmbeddableMappingType() + ); + } + else { + return new SybaseJtdsJsonAsStringJdbcType( + indicators.isNationalized() ? SqlTypes.LONG32NVARCHAR : SqlTypes.LONG32VARCHAR, + getEmbeddableMappingType() + ); + } + } + } + + @Override + public ValueBinder getBinder(JavaType javaType) { + if ( !isNationalized() ) { + return super.getBinder( javaType ); + } + return new BasicBinder<>( javaType, this ) { + + private SybaseJtdsJsonAsStringJdbcType getJsonAsStringJdbcType() { + return (SybaseJtdsJsonAsStringJdbcType) getJdbcType(); + } + + private String getXml(X value, WrapperOptions options) throws SQLException { + return getJsonAsStringJdbcType().toString( value, getJavaType(), options ); + } + + @Override + protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) + throws SQLException { + final String xml = getXml( value, options ); + st.setBytes( index, xml.getBytes( StandardCharsets.UTF_16LE ) ); + } + + @Override + protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) + throws SQLException { + final String xml = getXml( value, options ); + st.setBytes( name, xml.getBytes( StandardCharsets.UTF_16LE ) ); + } + + @Override + protected void doBindNull(PreparedStatement st, int index, WrapperOptions options) throws SQLException { + st.setNull( index, SqlTypes.VARCHAR ); + } + + @Override + protected void doBindNull(CallableStatement st, String name, WrapperOptions options) + throws SQLException { + st.setNull( name, SqlTypes.VARCHAR ); + } + }; + } + + @Override + public ValueExtractor getExtractor(JavaType javaType) { + if ( !isNationalized() ) { + return super.getExtractor( javaType ); + } + return new BasicExtractor<>( javaType, this ) { + + @Override + protected X doExtract(ResultSet rs, int paramIndex, WrapperOptions options) throws SQLException { + return getObject( rs.getString( paramIndex ), options ); + } + + @Override + protected X doExtract(CallableStatement statement, int index, WrapperOptions options) + throws SQLException { + return getObject( statement.getString( index ), options ); + } + + @Override + protected X doExtract(CallableStatement statement, String name, WrapperOptions options) + throws SQLException { + return getObject( statement.getString( name ), options ); + } + + private X getObject(String xml, WrapperOptions options) throws SQLException { + return xml == null ? null : getJsonAsStringJdbcType().fromString( xml, getJavaType(), options ); + } + + private SybaseJtdsJsonAsStringJdbcType getJsonAsStringJdbcType() { + return (SybaseJtdsJsonAsStringJdbcType) getJdbcType(); + } + }; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SybaseJtdsLongNVarcharJdbcType.java b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseJtdsLongNVarcharJdbcType.java new file mode 100644 index 000000000000..de152f70ec09 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseJtdsLongNVarcharJdbcType.java @@ -0,0 +1,54 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect; + +import org.hibernate.type.SqlTypes; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators; +import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; + +import java.sql.Types; + +public class SybaseJtdsLongNVarcharJdbcType extends SybaseJtdsNVarcharJdbcType { + + public static final SybaseJtdsLongNVarcharJdbcType JTDS_INSTANCE = new SybaseJtdsLongNVarcharJdbcType(); + + public SybaseJtdsLongNVarcharJdbcType() { + } + + @Override + public int getJdbcTypeCode() { + return Types.LONGNVARCHAR; + } + + @Override + public String toString() { + return "SybaseJtdsLongNVarcharJdbcType"; + } + + @Override + public JdbcType resolveIndicatedType( + JdbcTypeIndicators indicators, + JavaType domainJtd) { + assert domainJtd != null; + + final var typeConfiguration = indicators.getTypeConfiguration(); + final JdbcTypeRegistry jdbcTypeRegistry = typeConfiguration.getJdbcTypeRegistry(); + + final int jdbcTypeCode; + if ( indicators.isLob() ) { + jdbcTypeCode = indicators.isNationalized() ? Types.NCLOB : Types.CLOB; + } + else if ( shouldUseMaterializedLob( indicators ) ) { + jdbcTypeCode = indicators.isNationalized() ? SqlTypes.MATERIALIZED_NCLOB : SqlTypes.MATERIALIZED_CLOB; + } + else { + jdbcTypeCode = indicators.isNationalized() ? Types.LONGNVARCHAR : Types.LONGVARCHAR; + } + + return jdbcTypeRegistry.getDescriptor( indicators.resolveJdbcTypeCode( jdbcTypeCode ) ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SybaseJtdsNCharJdbcType.java b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseJtdsNCharJdbcType.java new file mode 100644 index 000000000000..0a70b60c165c --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseJtdsNCharJdbcType.java @@ -0,0 +1,50 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect; + +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators; +import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; + +import java.sql.Types; + +public class SybaseJtdsNCharJdbcType extends SybaseJtdsNVarcharJdbcType { + + public static final SybaseJtdsNCharJdbcType JTDS_INSTANCE = new SybaseJtdsNCharJdbcType(); + + public SybaseJtdsNCharJdbcType() { + } + + @Override + public int getJdbcTypeCode() { + return Types.NCHAR; + } + + @Override + public String toString() { + return "SybaseJtdsNCharJdbcType"; + } + + @Override + public JdbcType resolveIndicatedType( + JdbcTypeIndicators indicators, + JavaType domainJtd) { + assert domainJtd != null; + + final var typeConfiguration = indicators.getTypeConfiguration(); + final JdbcTypeRegistry jdbcTypeRegistry = typeConfiguration.getJdbcTypeRegistry(); + + final int jdbcTypeCode; + if ( indicators.isLob() ) { + jdbcTypeCode = indicators.isNationalized() ? Types.NCLOB : Types.CLOB; + } + else { + jdbcTypeCode = indicators.isNationalized() ? Types.NCHAR : Types.CHAR; + } + + return jdbcTypeRegistry.getDescriptor( indicators.resolveJdbcTypeCode( jdbcTypeCode ) ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SybaseJtdsNClobJdbcType.java b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseJtdsNClobJdbcType.java new file mode 100644 index 000000000000..a2794b5462d6 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseJtdsNClobJdbcType.java @@ -0,0 +1,88 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect; + +import org.hibernate.type.descriptor.ValueExtractor; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.jdbc.BasicBinder; +import org.hibernate.type.descriptor.jdbc.BasicExtractor; +import org.hibernate.type.descriptor.jdbc.NClobJdbcType; + +import java.nio.charset.StandardCharsets; +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; + +/** + * Specialized type mapping for {@code NCLOB} that binds UTF-16LE bytes, + * because the jTDS driver can't handle Unicode characters and doesn't support nationalized methods. + * For extraction, the {@code getString} method works fine though. + */ +public class SybaseJtdsNClobJdbcType extends NClobJdbcType { + + public static final SybaseJtdsNClobJdbcType JTDS_INSTANCE = new SybaseJtdsNClobJdbcType(); + + public SybaseJtdsNClobJdbcType() { + } + + @Override + public String toString() { + return "SybaseJtdsNClobJdbcType"; + } + + @Override + protected BasicBinder getNClobBinder(JavaType javaType) { + return new BasicBinder<>( javaType, this ) { + @Override + protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) throws SQLException { + final String string = javaType.unwrap( value, String.class, options ); + st.setBytes( index, string == null ? null : string.getBytes( StandardCharsets.UTF_16LE ) ); + } + + @Override + protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) + throws SQLException { + final String string = javaType.unwrap( value, String.class, options ); + st.setBytes( name, string == null ? null : string.getBytes( StandardCharsets.UTF_16LE ) ); + } + + @Override + protected void doBindNull(PreparedStatement st, int index, WrapperOptions options) throws SQLException { + st.setNull( index, Types.VARCHAR ); + } + + @Override + protected void doBindNull(CallableStatement st, String name, WrapperOptions options) + throws SQLException { + st.setNull( name, Types.VARCHAR ); + } + }; + } + + @Override + public ValueExtractor getExtractor(final JavaType javaType) { + return new BasicExtractor<>( javaType, this ) { + @Override + protected X doExtract(ResultSet rs, int paramIndex, WrapperOptions options) throws SQLException { + return javaType.wrap( rs.getCharacterStream( paramIndex ), options ); + } + + @Override + protected X doExtract(CallableStatement statement, int index, WrapperOptions options) + throws SQLException { + return javaType.wrap( statement.getCharacterStream( index ), options ); + } + + @Override + protected X doExtract(CallableStatement statement, String name, WrapperOptions options) + throws SQLException { + return javaType.wrap( statement.getCharacterStream( name ), options ); + } + }; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SybaseJtdsNVarcharJdbcType.java b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseJtdsNVarcharJdbcType.java new file mode 100644 index 000000000000..75609ae87fc5 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseJtdsNVarcharJdbcType.java @@ -0,0 +1,90 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect; + +import org.hibernate.type.descriptor.ValueBinder; +import org.hibernate.type.descriptor.ValueExtractor; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.jdbc.BasicBinder; +import org.hibernate.type.descriptor.jdbc.BasicExtractor; +import org.hibernate.type.descriptor.jdbc.NVarcharJdbcType; + +import java.nio.charset.StandardCharsets; +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; + +/** + * Specialized type mapping for {@code NVARCHAR} that binds/extracts UTF-16LE bytes, + * because the jTDS driver can't handle Unicode characters and doesn't support nationalized methods. + */ +public class SybaseJtdsNVarcharJdbcType extends NVarcharJdbcType { + + public static final SybaseJtdsNVarcharJdbcType JTDS_INSTANCE = new SybaseJtdsNVarcharJdbcType(); + + public SybaseJtdsNVarcharJdbcType() { + } + + @Override + public String toString() { + return "SybaseJtdsNVarcharJdbcType"; + } + + + @Override + public ValueBinder getBinder(final JavaType javaType) { + return new BasicBinder<>( javaType, this ) { + @Override + protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) throws SQLException { + final String string = javaType.unwrap( value, String.class, options ); + st.setBytes( index, string == null ? null : string.getBytes( StandardCharsets.UTF_16LE ) ); + } + + @Override + protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) + throws SQLException { + final String string = javaType.unwrap( value, String.class, options ); + st.setBytes( name, string == null ? null : string.getBytes( StandardCharsets.UTF_16LE ) ); + } + + @Override + protected void doBindNull(PreparedStatement st, int index, WrapperOptions options) throws SQLException { + st.setNull( index, Types.VARCHAR ); + } + + @Override + protected void doBindNull(CallableStatement st, String name, WrapperOptions options) + throws SQLException { + st.setNull( name, Types.VARCHAR ); + } + }; + } + + @Override + public ValueExtractor getExtractor(final JavaType javaType) { + return new BasicExtractor<>( javaType, this ) { + @Override + protected X doExtract(ResultSet rs, int paramIndex, WrapperOptions options) throws SQLException { + final byte[] bytes = rs.getBytes( paramIndex ); + return bytes == null ? null : javaType.wrap( new String( bytes, StandardCharsets.UTF_16LE ), options ); + } + + @Override + protected X doExtract(CallableStatement statement, int index, WrapperOptions options) throws SQLException { + final byte[] bytes = statement.getBytes( index ); + return bytes == null ? null : javaType.wrap( new String( bytes, StandardCharsets.UTF_16LE ), options ); + } + + @Override + protected X doExtract(CallableStatement statement, String name, WrapperOptions options) throws SQLException { + final byte[] bytes = statement.getBytes( name ); + return bytes == null ? null : javaType.wrap( new String( bytes, StandardCharsets.UTF_16LE ), options ); + } + }; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SybaseJtdsXmlAsStringArrayJdbcType.java b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseJtdsXmlAsStringArrayJdbcType.java new file mode 100644 index 000000000000..764c108e00d3 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseJtdsXmlAsStringArrayJdbcType.java @@ -0,0 +1,134 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect; + +import org.hibernate.type.SqlTypes; +import org.hibernate.type.descriptor.ValueBinder; +import org.hibernate.type.descriptor.ValueExtractor; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.jdbc.BasicBinder; +import org.hibernate.type.descriptor.jdbc.BasicExtractor; +import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators; +import org.hibernate.type.descriptor.jdbc.XmlAsStringArrayJdbcType; + +import java.nio.charset.StandardCharsets; +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * Specialized type mapping for {@code XML} that binds UTF-16LE bytes, + * because the jTDS driver can't handle Unicode characters and doesn't support nationalized methods. + * For extraction, the {@code getString} method works fine though. + */ +public class SybaseJtdsXmlAsStringArrayJdbcType extends XmlAsStringArrayJdbcType { + + public SybaseJtdsXmlAsStringArrayJdbcType(JdbcType elementJdbcType) { + super( elementJdbcType, SqlTypes.NCLOB ); + } + + public SybaseJtdsXmlAsStringArrayJdbcType(JdbcType elementJdbcType, int ddlTypeCode) { + super( elementJdbcType, ddlTypeCode ); + } + + @Override + public String toString() { + return "SybaseJtdsXmlAsStringArrayJdbcType"; + } + + @Override + public JdbcType resolveIndicatedType(JdbcTypeIndicators indicators, JavaType domainJtd) { + // Depending on the size of the column, we might have to adjust the jdbc type code for DDL. + // In some DBMS we can compare LOBs with special functions which is handled in the SqlAstTranslators, + // but that requires the correct jdbc type code to be available, which we ensure this way + if ( needsLob( indicators ) ) { + return indicators.isNationalized() + ? new SybaseJtdsXmlAsStringArrayJdbcType( getElementJdbcType(), SqlTypes.NCLOB ) + : new SybaseJtdsXmlAsStringArrayJdbcType( getElementJdbcType(), SqlTypes.CLOB ); + } + else { + return indicators.isNationalized() + ? new SybaseJtdsXmlAsStringArrayJdbcType( getElementJdbcType(), SqlTypes.LONG32NVARCHAR ) + : new SybaseJtdsXmlAsStringArrayJdbcType( getElementJdbcType(), SqlTypes.LONG32VARCHAR ); + } + } + + @Override + public ValueBinder getBinder(JavaType javaType) { + if ( !isNationalized() ) { + return super.getBinder( javaType ); + } + return new BasicBinder<>( javaType, this ) { + + private SybaseJtdsXmlAsStringArrayJdbcType getXmlAsStringArrayJdbcType() { + return (SybaseJtdsXmlAsStringArrayJdbcType) getJdbcType(); + } + + @Override + protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) + throws SQLException { + final SybaseJtdsXmlAsStringArrayJdbcType jdbcType = getXmlAsStringArrayJdbcType(); + final String xml = jdbcType.toString( value, getJavaType(), options ); + st.setBytes( index, xml.getBytes( StandardCharsets.UTF_16LE ) ); + } + + @Override + protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) + throws SQLException { + final SybaseJtdsXmlAsStringArrayJdbcType jdbcType = getXmlAsStringArrayJdbcType(); + final String xml = jdbcType.toString( value, getJavaType(), options ); + st.setBytes( name, xml.getBytes( StandardCharsets.UTF_16LE ) ); + } + + @Override + protected void doBindNull(PreparedStatement st, int index, WrapperOptions options) throws SQLException { + st.setNull( index, SqlTypes.VARCHAR ); + } + + @Override + protected void doBindNull(CallableStatement st, String name, WrapperOptions options) + throws SQLException { + st.setNull( name, SqlTypes.VARCHAR ); + } + }; + } + + @Override + public ValueExtractor getExtractor(JavaType javaType) { + if ( !isNationalized() ) { + return super.getExtractor( javaType ); + } + return new BasicExtractor<>( javaType, this ) { + + private SybaseJtdsXmlAsStringArrayJdbcType getXmlAsStringArrayJdbcType() { + return (SybaseJtdsXmlAsStringArrayJdbcType) getJdbcType(); + } + + @Override + protected X doExtract(ResultSet rs, int paramIndex, WrapperOptions options) throws SQLException { + final String value = rs.getString( paramIndex ); + return getXmlAsStringArrayJdbcType().fromString( value, getJavaType(), options ); + } + + @Override + protected X doExtract(CallableStatement statement, int index, WrapperOptions options) + throws SQLException { + final String value = statement.getString( index ); + return getXmlAsStringArrayJdbcType().fromString( value, getJavaType(), options ); + } + + @Override + protected X doExtract(CallableStatement statement, String name, WrapperOptions options) + throws SQLException { + final String value = statement.getString( name ); + return getXmlAsStringArrayJdbcType().fromString( value, getJavaType(), options ); + } + + }; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SybaseJtdsXmlAsStringArrayJdbcTypeConstructor.java b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseJtdsXmlAsStringArrayJdbcTypeConstructor.java new file mode 100644 index 000000000000..43d50bbacba6 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseJtdsXmlAsStringArrayJdbcTypeConstructor.java @@ -0,0 +1,41 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect; + + +import org.hibernate.tool.schema.extract.spi.ColumnTypeInformation; +import org.hibernate.type.BasicType; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.descriptor.jdbc.JdbcTypeConstructor; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * Factory for {@link SybaseJtdsXmlAsStringArrayJdbcType}. + */ +public class SybaseJtdsXmlAsStringArrayJdbcTypeConstructor implements JdbcTypeConstructor { + public static final SybaseJtdsXmlAsStringArrayJdbcTypeConstructor INSTANCE = new SybaseJtdsXmlAsStringArrayJdbcTypeConstructor(); + + public JdbcType resolveType( + TypeConfiguration typeConfiguration, + Dialect dialect, + BasicType elementType, + ColumnTypeInformation columnTypeInformation) { + return resolveType( typeConfiguration, dialect, elementType.getJdbcType(), columnTypeInformation ); + } + + public JdbcType resolveType( + TypeConfiguration typeConfiguration, + Dialect dialect, + JdbcType elementType, + ColumnTypeInformation columnTypeInformation) { + return new SybaseJtdsXmlAsStringArrayJdbcType( elementType, SqlTypes.NCLOB ); + } + + @Override + public int getDefaultSqlTypeCode() { + return SqlTypes.XML_ARRAY; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SybaseJtdsXmlAsStringJdbcType.java b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseJtdsXmlAsStringJdbcType.java new file mode 100644 index 000000000000..65af341ad300 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseJtdsXmlAsStringJdbcType.java @@ -0,0 +1,164 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect; + +import org.hibernate.metamodel.mapping.EmbeddableMappingType; +import org.hibernate.metamodel.spi.RuntimeModelCreationContext; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.descriptor.ValueBinder; +import org.hibernate.type.descriptor.ValueExtractor; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.jdbc.AggregateJdbcType; +import org.hibernate.type.descriptor.jdbc.BasicBinder; +import org.hibernate.type.descriptor.jdbc.BasicExtractor; +import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators; +import org.hibernate.type.descriptor.jdbc.XmlAsStringJdbcType; + +import java.nio.charset.StandardCharsets; +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * Specialized type mapping for {@code XML} that binds UTF-16LE bytes, + * because the jTDS driver can't handle Unicode characters and doesn't support nationalized methods. + * For extraction, the {@code getString} method works fine though. + */ +public class SybaseJtdsXmlAsStringJdbcType extends XmlAsStringJdbcType { + + public static final SybaseJtdsXmlAsStringJdbcType JTDS_VARCHAR_INSTANCE = + new SybaseJtdsXmlAsStringJdbcType( SqlTypes.LONG32VARCHAR, null ); + public static final SybaseJtdsXmlAsStringJdbcType JTDS_NVARCHAR_INSTANCE = + new SybaseJtdsXmlAsStringJdbcType( SqlTypes.LONG32NVARCHAR, null ); + public static final SybaseJtdsXmlAsStringJdbcType JTDS_CLOB_INSTANCE = + new SybaseJtdsXmlAsStringJdbcType( SqlTypes.CLOB, null ); + public static final SybaseJtdsXmlAsStringJdbcType JTDS_INSTANCE = + new SybaseJtdsXmlAsStringJdbcType( SqlTypes.NCLOB, null ); + + public SybaseJtdsXmlAsStringJdbcType(int ddlTypeCode, EmbeddableMappingType embeddableMappingType) { + super( ddlTypeCode, embeddableMappingType ); + } + + @Override + public AggregateJdbcType resolveAggregateJdbcType( + EmbeddableMappingType mappingType, + String sqlType, + RuntimeModelCreationContext creationContext) { + return new SybaseJtdsXmlAsStringJdbcType( getDdlTypeCode(), mappingType ); + } + + @Override + public String toString() { + return "SybaseJtdsXmlAsStringJdbcType"; + } + + @Override + public JdbcType resolveIndicatedType(JdbcTypeIndicators indicators, JavaType domainJtd) { + // Depending on the size of the column, we might have to adjust the jdbc type code for DDL. + // In some DBMS we can compare LOBs with special functions which is handled in the SqlAstTranslators, + // but that requires the correct jdbc type code to be available, which we ensure this way + if ( getEmbeddableMappingType() == null ) { + if ( needsLob( indicators ) ) { + return indicators.isNationalized() ? JTDS_INSTANCE : JTDS_CLOB_INSTANCE; + } + else { + return indicators.isNationalized() ? JTDS_NVARCHAR_INSTANCE : JTDS_VARCHAR_INSTANCE; + } + } + else { + if ( needsLob( indicators ) ) { + return new SybaseJtdsXmlAsStringJdbcType( + indicators.isNationalized() ? SqlTypes.NCLOB : SqlTypes.CLOB, + getEmbeddableMappingType() + ); + } + else { + return new SybaseJtdsXmlAsStringJdbcType( + indicators.isNationalized() ? SqlTypes.LONG32NVARCHAR : SqlTypes.LONG32VARCHAR, + getEmbeddableMappingType() + ); + } + } + } + + @Override + public ValueBinder getBinder(JavaType javaType) { + if ( !isNationalized() ) { + return super.getBinder( javaType ); + } + return new BasicBinder<>( javaType, this ) { + + private SybaseJtdsXmlAsStringJdbcType getXmlAsStringJdbcType() { + return (SybaseJtdsXmlAsStringJdbcType) getJdbcType(); + } + + private String getXml(X value, WrapperOptions options) throws SQLException { + return getXmlAsStringJdbcType().toString( value, getJavaType(), options ); + } + + @Override + protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) + throws SQLException { + final String xml = getXml( value, options ); + st.setBytes( index, xml.getBytes( StandardCharsets.UTF_16LE ) ); + } + + @Override + protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) + throws SQLException { + final String xml = getXml( value, options ); + st.setBytes( name, xml.getBytes( StandardCharsets.UTF_16LE ) ); + } + + @Override + protected void doBindNull(PreparedStatement st, int index, WrapperOptions options) throws SQLException { + st.setNull( index, SqlTypes.VARCHAR ); + } + + @Override + protected void doBindNull(CallableStatement st, String name, WrapperOptions options) + throws SQLException { + st.setNull( name, SqlTypes.VARCHAR ); + } + }; + } + + @Override + public ValueExtractor getExtractor(JavaType javaType) { + if ( !isNationalized() ) { + return super.getExtractor( javaType ); + } + return new BasicExtractor<>( javaType, this ) { + + @Override + protected X doExtract(ResultSet rs, int paramIndex, WrapperOptions options) throws SQLException { + return getObject( rs.getString( paramIndex ), options ); + } + + @Override + protected X doExtract(CallableStatement statement, int index, WrapperOptions options) + throws SQLException { + return getObject( statement.getString( index ), options ); + } + + @Override + protected X doExtract(CallableStatement statement, String name, WrapperOptions options) + throws SQLException { + return getObject( statement.getString( name ), options ); + } + + private X getObject(String xml, WrapperOptions options) throws SQLException { + return xml == null ? null : getXmlAsStringJdbcType().fromString( xml, getJavaType(), options ); + } + + private SybaseJtdsXmlAsStringJdbcType getXmlAsStringJdbcType() { + return (SybaseJtdsXmlAsStringJdbcType) getJdbcType(); + } + }; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/AggregateSupport.java b/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/AggregateSupport.java index 35cc3d101f35..03649ddbea98 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/AggregateSupport.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/AggregateSupport.java @@ -7,6 +7,7 @@ import java.util.List; import org.hibernate.Incubating; +import org.hibernate.Internal; import org.hibernate.boot.model.relational.AuxiliaryDatabaseObject; import org.hibernate.boot.model.relational.Namespace; import org.hibernate.dialect.Dialect; @@ -71,6 +72,11 @@ default String aggregateComponentCustomReadExpression( ); } + @Internal // TODO: find a better way! + default boolean useLengthsInCasts() { + return false; + } + /** * Returns the custom read expression to use for {@code column}. * Replaces the given {@code placeholder} in the given {@code template} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/DB2AggregateSupport.java b/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/DB2AggregateSupport.java index 348b852e6711..3ad57364c48b 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/DB2AggregateSupport.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/DB2AggregateSupport.java @@ -71,6 +71,11 @@ public DB2AggregateSupport(boolean jsonSupport) { this.jsonSupport = jsonSupport; } + @Override + public boolean useLengthsInCasts() { + return true; + } + @Override public String aggregateComponentCustomReadExpression( String template, @@ -211,7 +216,7 @@ private static String xmlExtractArguments(String aggregateParentReadExpression, if ( aggregateParentReadExpression.startsWith( XML_EXTRACT_START ) && aggregateParentReadExpression.endsWith( XML_EXTRACT_END ) && (separatorIndex = aggregateParentReadExpression.indexOf( XML_EXTRACT_SEPARATOR )) != -1 ) { - final StringBuilder sb = new StringBuilder( aggregateParentReadExpression.length() - XML_EXTRACT_START.length() + xpathFragment.length() ); + final var sb = new StringBuilder( aggregateParentReadExpression.length() - XML_EXTRACT_START.length() + xpathFragment.length() ); sb.append( aggregateParentReadExpression, XML_EXTRACT_START.length(), separatorIndex ); sb.append( '/' ); sb.append( xpathFragment ); @@ -320,7 +325,7 @@ public String aggregateCustomWriteExpression( case XML_ARRAY: return null; case STRUCT: - final StringBuilder sb = new StringBuilder(); + final var sb = new StringBuilder(); appendStructCustomWriteExpression( aggregateColumn, aggregatedColumns, sb ); return sb.toString(); } @@ -1031,7 +1036,7 @@ private void renderParentInserts(SqlAppender sqlAppender, SelectablePath parentP private String columnXPath(SelectablePath selectablePath) { final SelectablePath[] parts = selectablePath.getParts(); - final StringBuilder xpath = new StringBuilder(); + final var xpath = new StringBuilder(); for ( int i = 1; i < parts.length; i++ ) { xpath.append( '/' ); xpath.append( parts[i].getSelectableName() ); @@ -1041,7 +1046,7 @@ private String columnXPath(SelectablePath selectablePath) { private String columnVariable(SelectablePath selectablePath) { final SelectablePath[] parts = selectablePath.getParts(); - final StringBuilder variable = new StringBuilder(); + final var variable = new StringBuilder(); for ( int i = 1; i < parts.length; i++ ) { variable.append( parts[i].getSelectableName() ); variable.append( '-' ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/H2AggregateSupport.java b/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/H2AggregateSupport.java index d984016407ef..3d5fd958635c 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/H2AggregateSupport.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/H2AggregateSupport.java @@ -53,20 +53,20 @@ public String aggregateComponentCustomReadExpression( switch ( aggregateColumnTypeCode ) { case JSON_ARRAY: case JSON: + final String readExpression = aggregateParentReadExpression.isEmpty() ? + columnExpression + : "(" + aggregateParentReadExpression + ").\"" + columnExpression + "\""; switch ( column.getJdbcMapping().getJdbcType().getDefaultSqlTypeCode() ) { case JSON: case JSON_ARRAY: - return template.replace( - placeholder, - "(" + aggregateParentReadExpression + ").\"" + columnExpression + "\"" - ); + return template.replace( placeholder, readExpression ); case BINARY: case VARBINARY: case LONG32VARBINARY: // We encode binary data as hex, so we have to decode here return template.replace( placeholder, - hexDecodeExpression( queryExpression( "(" + aggregateParentReadExpression + ").\"" + columnExpression + "\"" ), column.getColumnDefinition() ) + hexDecodeExpression( queryExpression( readExpression ), column.getColumnDefinition() ) ); case ARRAY: final BasicPluralType pluralType = (BasicPluralType) column.getJdbcMapping(); @@ -78,18 +78,18 @@ public String aggregateComponentCustomReadExpression( // We encode binary data as hex, so we have to decode here return template.replace( placeholder, - "(select array_agg(" + hexDecodeExpression( queryExpression( "(" + aggregateParentReadExpression + ").\"" + columnExpression + "\"[i.x]" ), elementTypeName ) + ") from system_range(1,10000) i where i.x<=coalesce(array_length((" + aggregateParentReadExpression + ").\"" + columnExpression + "\"),0))" + "(select array_agg(" + hexDecodeExpression( queryExpression( readExpression + "[i.x]" ), elementTypeName ) + ") from system_range(1,10000) i where i.x<=coalesce(array_length(" + readExpression + "),0))" ); default: return template.replace( placeholder, - "(select array_agg(" + valueExpression( "(" + aggregateParentReadExpression + ").\"" + columnExpression + "\"[i.x]", elementTypeName ) + ") from system_range(1,10000) i where i.x<=coalesce(array_length((" + aggregateParentReadExpression + ").\"" + columnExpression + "\"),0))" + "(select array_agg(" + valueExpression( readExpression + "[i.x]", elementTypeName ) + ") from system_range(1,10000) i where i.x<=coalesce(array_length(" + readExpression + "),0))" ); } default: return template.replace( placeholder, - columnExpression( aggregateParentReadExpression, columnExpression, column.getColumnDefinition() ) + valueExpression( readExpression, column.getColumnDefinition() ) ); } } @@ -102,10 +102,6 @@ private static String getElementTypeName(String arrayTypeName) { return elementTypeName.equals( "clob" ) ? "varchar" : elementTypeName; } - private static String columnExpression(String aggregateParentReadExpression, String columnExpression, String columnType) { - return valueExpression( "(" + aggregateParentReadExpression + ").\"" + columnExpression + "\"", columnType ); - } - private static String hexDecodeExpression(String valueExpression, String columnType) { return "cast(hextoraw(regexp_replace(" + valueExpression + ",'([0-9a-f][0-9a-f])','00$1')) as " + columnType + ")"; } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/OracleAggregateSupport.java b/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/OracleAggregateSupport.java index 55f290752e0b..159a17e66ffd 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/OracleAggregateSupport.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/OracleAggregateSupport.java @@ -37,7 +37,6 @@ import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.StructuredJdbcType; import org.hibernate.type.descriptor.sql.DdlType; -import org.hibernate.type.descriptor.sql.spi.DdlTypeRegistry; import org.hibernate.type.spi.TypeConfiguration; import java.util.LinkedHashMap; @@ -358,7 +357,7 @@ private static String xmlExtractArguments(String aggregateParentReadExpression, if ( aggregateParentReadExpression.startsWith( XML_EXTRACT_START ) && aggregateParentReadExpression.endsWith( XML_EXTRACT_END ) && (separatorIndex = aggregateParentReadExpression.indexOf( XML_EXTRACT_SEPARATOR )) != -1 ) { - final StringBuilder sb = new StringBuilder( aggregateParentReadExpression.length() - XML_EXTRACT_START.length() + xpathFragment.length() ); + final var sb = new StringBuilder( aggregateParentReadExpression.length() - XML_EXTRACT_START.length() + xpathFragment.length() ); sb.append( aggregateParentReadExpression, XML_EXTRACT_START.length(), separatorIndex ); sb.append( '/' ); sb.append( xpathFragment ); @@ -368,7 +367,7 @@ private static String xmlExtractArguments(String aggregateParentReadExpression, else if ( aggregateParentReadExpression.startsWith( XML_QUERY_START ) && aggregateParentReadExpression.endsWith( XML_QUERY_END ) && (separatorIndex = aggregateParentReadExpression.indexOf( XML_QUERY_SEPARATOR )) != -1 ) { - final StringBuilder sb = new StringBuilder( aggregateParentReadExpression.length() - XML_QUERY_START.length() + xpathFragment.length() ); + final var sb = new StringBuilder( aggregateParentReadExpression.length() - XML_QUERY_START.length() + xpathFragment.length() ); sb.append( aggregateParentReadExpression, XML_QUERY_START.length(), separatorIndex ); sb.append( '/' ); sb.append( xpathFragment ); @@ -511,7 +510,7 @@ private static String determineElementTypeName( Size castTargetSize, BasicPluralType pluralType, TypeConfiguration typeConfiguration) { - final DdlTypeRegistry ddlTypeRegistry = typeConfiguration.getDdlTypeRegistry(); + final var ddlTypeRegistry = typeConfiguration.getDdlTypeRegistry(); final BasicType expressionType = pluralType.getElementType(); DdlType ddlType = ddlTypeRegistry.getDescriptor( expressionType.getJdbcType().getDdlTypeCode() ); if ( ddlType == null ) { diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/PostgreSQLAggregateSupport.java b/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/PostgreSQLAggregateSupport.java index ba2ea0ee7794..e7bc400ab948 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/PostgreSQLAggregateSupport.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/PostgreSQLAggregateSupport.java @@ -170,7 +170,7 @@ private static String xmlExtractArguments(String aggregateParentReadExpression, if ( aggregateParentReadExpression.startsWith( XML_EXTRACT_START ) && aggregateParentReadExpression.endsWith( XML_EXTRACT_END ) && (separatorIndex = aggregateParentReadExpression.indexOf( XML_EXTRACT_SEPARATOR )) != -1 ) { - final StringBuilder sb = new StringBuilder( aggregateParentReadExpression.length() - XML_EXTRACT_START.length() + xpathFragment.length() ); + final var sb = new StringBuilder( aggregateParentReadExpression.length() - XML_EXTRACT_START.length() + xpathFragment.length() ); sb.append( aggregateParentReadExpression, XML_EXTRACT_START.length(), separatorIndex ); sb.append( '/' ); sb.append( xpathFragment ); @@ -180,7 +180,7 @@ private static String xmlExtractArguments(String aggregateParentReadExpression, else if ( aggregateParentReadExpression.startsWith( XML_QUERY_START ) && aggregateParentReadExpression.endsWith( XML_QUERY_END ) && (separatorIndex = aggregateParentReadExpression.indexOf( XML_QUERY_SEPARATOR )) != -1 ) { - final StringBuilder sb = new StringBuilder( aggregateParentReadExpression.length() - XML_QUERY_START.length() + xpathFragment.length() ); + final var sb = new StringBuilder( aggregateParentReadExpression.length() - XML_QUERY_START.length() + xpathFragment.length() ); sb.append( aggregateParentReadExpression, XML_QUERY_START.length(), separatorIndex ); sb.append( '/' ); sb.append( xpathFragment ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/SybaseASEAggregateSupport.java b/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/SybaseASEAggregateSupport.java index a43864a57889..85e992d90199 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/SybaseASEAggregateSupport.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/SybaseASEAggregateSupport.java @@ -167,7 +167,7 @@ private static String xmlExtractArguments(String aggregateParentReadExpression, XML_EXTRACT_READ_INVOCATION_START, 0, XML_EXTRACT_READ_INVOCATION_START.length() )) { final int argumentsStartIndex = patternIdx + XML_EXTRACT_READ_NULL_CHECK.length() + XML_EXTRACT_READ_INVOCATION_START.length(); separatorIndex = aggregateParentReadExpression.indexOf( XML_EXTRACT_SEPARATOR ); - final StringBuilder sb = new StringBuilder( aggregateParentReadExpression.length() - argumentsStartIndex + xpathFragment.length() ); + final var sb = new StringBuilder( aggregateParentReadExpression.length() - argumentsStartIndex + xpathFragment.length() ); sb.append( aggregateParentReadExpression, argumentsStartIndex, separatorIndex ); sb.append( '/' ); sb.append( xpathFragment ); @@ -177,7 +177,7 @@ private static String xmlExtractArguments(String aggregateParentReadExpression, else if ( aggregateParentReadExpression.startsWith( XML_EXTRACT_START ) && aggregateParentReadExpression.endsWith( XML_EXTRACT_END ) && (separatorIndex = aggregateParentReadExpression.indexOf( XML_EXTRACT_SIMPLE_SEPARATOR )) != -1 ) { - final StringBuilder sb = new StringBuilder( aggregateParentReadExpression.length() - XML_EXTRACT_START.length() + xpathFragment.length() ); + final var sb = new StringBuilder( aggregateParentReadExpression.length() - XML_EXTRACT_START.length() + xpathFragment.length() ); final int xpathEnd; if ( aggregateParentReadExpression.regionMatches( separatorIndex - 2, XML_EXTRACT_SEPARATOR, 0, XML_EXTRACT_SEPARATOR.length() ) ) { xpathEnd = separatorIndex - 2; @@ -209,7 +209,7 @@ private static String xmlExtractForConcat(String prefix, String aggregateParentR caseExpression = aggregateParentReadExpression.substring( 0, patternIdx + XML_EXTRACT_READ_NULL_CHECK.length() ); final int argumentsStartIndex = patternIdx + XML_EXTRACT_READ_NULL_CHECK.length() + XML_EXTRACT_READ_INVOCATION_START.length(); final int separatorIndex = aggregateParentReadExpression.indexOf( XML_EXTRACT_SEPARATOR ); - final StringBuilder sb = new StringBuilder( aggregateParentReadExpression.length() - argumentsStartIndex + xpathFragment.length() ); + final var sb = new StringBuilder( aggregateParentReadExpression.length() - argumentsStartIndex + xpathFragment.length() ); sb.append( aggregateParentReadExpression, argumentsStartIndex, separatorIndex ); sb.append( '/' ); sb.append( xpathFragment ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/CommonFunctionFactory.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/CommonFunctionFactory.java index d2f944187055..779b06f07161 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/CommonFunctionFactory.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/CommonFunctionFactory.java @@ -54,7 +54,10 @@ import org.hibernate.type.spi.TypeConfiguration; import static org.hibernate.query.sqm.produce.function.FunctionParameterType.*; +import static org.hibernate.query.sqm.produce.function.StandardFunctionArgumentTypeResolvers.ARGUMENT_OR_IMPLIED_RESULT_TYPE; +import static org.hibernate.query.sqm.produce.function.StandardFunctionArgumentTypeResolvers.IMPLIED_RESULT_TYPE; import static org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers.useArgType; +import static org.hibernate.sql.ast.SqlAstNodeRenderingMode.NO_PLAIN_PARAMETER; /** * Enumeratoes common function template definitions. @@ -62,6 +65,7 @@ * * @author Steve Ebersole * @author Gavin King + * @author Yoobin Yoon */ public class CommonFunctionFactory { @@ -181,6 +185,44 @@ public void log10_log() { .register(); } + /** + * For Spanner + */ + public void position_locate_spanner() { + functionRegistry.registerBinaryTernaryPattern( + "locate", + integerType, + "strpos(?2,?1)", + "strpos(substr(?2,?3),?1)", + FunctionParameterType.STRING, FunctionParameterType.STRING, FunctionParameterType.INTEGER, + typeConfiguration + ) + .setArgumentListSignature( "(STRING pattern, STRING string[, INTEGER start])" ); + functionRegistry.registerAlternateKey( "position", "locate" ); + } + + public void round_spanner() { + functionRegistry.registerUnaryBinaryPattern( + "round", + "round(?1::float8)", + "(floor(?1*power(10,?2)+0.5)/power(10,?2))", + NUMERIC, INTEGER, + typeConfiguration + ).setArgumentListSignature( "(NUMERIC number[, INTEGER places])" ); + } + + public void log_spanner() { + functionRegistry.registerUnaryBinaryPattern( + "log", + doubleType, + "log(cast(?1 as float8))", + "ln(cast(?2 as float8))/ln(cast(?1 as float8))", + NUMERIC, NUMERIC, typeConfiguration + ) + .setArgumentListSignature( "(NUMERIC arg1[, NUMERIC arg2])" ); + functionRegistry.registerAlternateKey( "log10", "log" ); + } + public void log2() { functionRegistry.namedDescriptorBuilder( "log2" ) .setInvariantType(doubleType) @@ -308,20 +350,43 @@ private void trunc( functionRegistry.registerAlternateKey( "truncate", "trunc" ); } - private void trunc(TruncFunction.DatetimeTrunc datetimeTrunc) { - trunc( "trunc(?1)", "trunc(?1,?2)", datetimeTrunc, null ); - } - public void trunc() { - trunc( null ); + trunc( "trunc(?1)", "trunc(?1,?2)", null, null ); } + /** + * H2, DB2 + */ public void trunc_dateTrunc() { - trunc( TruncFunction.DatetimeTrunc.DATE_TRUNC ); + functionRegistry.register( + "trunc", + new TruncFunction( + "trunc(?1)", + "trunc(?1,?2)", + TruncFunction.DatetimeTrunc.DATE_TRUNC, + null, + NO_PLAIN_PARAMETER, + typeConfiguration + ) + ); + functionRegistry.registerAlternateKey( "truncate", "trunc" ); } + /** + * HSQL + */ public void trunc_dateTrunc_trunc() { - trunc( TruncFunction.DatetimeTrunc.TRUNC ); + functionRegistry.register( + "trunc", + new TruncFunction( + "trunc(?1)", + "trunc(?1,?2)", + TruncFunction.DatetimeTrunc.TRUNC, + null, + NO_PLAIN_PARAMETER, + typeConfiguration ) + ); + functionRegistry.registerAlternateKey( "truncate", "trunc" ); } /** @@ -848,7 +913,7 @@ public void md5() { functionRegistry.namedDescriptorBuilder( "md5" ) .setInvariantType(stringType) .setExactArgumentCount( 1 ) - .setArgumentTypeResolver( StandardFunctionArgumentTypeResolvers.IMPLIED_RESULT_TYPE ) + .setArgumentTypeResolver( IMPLIED_RESULT_TYPE ) .register(); } @@ -856,7 +921,7 @@ public void initcap() { functionRegistry.namedDescriptorBuilder( "initcap" ) .setInvariantType(stringType) .setExactArgumentCount( 1 ) - .setArgumentTypeResolver( StandardFunctionArgumentTypeResolvers.IMPLIED_RESULT_TYPE ) + .setArgumentTypeResolver( IMPLIED_RESULT_TYPE ) .register(); } @@ -889,28 +954,28 @@ public void translate() { public void bitand() { functionRegistry.namedDescriptorBuilder( "bitand" ) .setExactArgumentCount( 2 ) - .setArgumentTypeResolver( StandardFunctionArgumentTypeResolvers.ARGUMENT_OR_IMPLIED_RESULT_TYPE ) + .setArgumentTypeResolver( ARGUMENT_OR_IMPLIED_RESULT_TYPE ) .register(); } public void bitor() { functionRegistry.namedDescriptorBuilder( "bitor" ) .setExactArgumentCount( 2 ) - .setArgumentTypeResolver( StandardFunctionArgumentTypeResolvers.ARGUMENT_OR_IMPLIED_RESULT_TYPE ) + .setArgumentTypeResolver( ARGUMENT_OR_IMPLIED_RESULT_TYPE ) .register(); } public void bitxor() { functionRegistry.namedDescriptorBuilder( "bitxor" ) .setExactArgumentCount( 2 ) - .setArgumentTypeResolver( StandardFunctionArgumentTypeResolvers.ARGUMENT_OR_IMPLIED_RESULT_TYPE ) + .setArgumentTypeResolver( ARGUMENT_OR_IMPLIED_RESULT_TYPE ) .register(); } public void bitnot() { functionRegistry.namedDescriptorBuilder( "bitnot" ) .setExactArgumentCount( 1 ) - .setArgumentTypeResolver( StandardFunctionArgumentTypeResolvers.IMPLIED_RESULT_TYPE ) + .setArgumentTypeResolver( IMPLIED_RESULT_TYPE ) .register(); } @@ -920,25 +985,25 @@ public void bitnot() { public void bitandorxornot_bitAndOrXorNot() { functionRegistry.namedDescriptorBuilder( "bit_and" ) .setExactArgumentCount( 2 ) - .setArgumentTypeResolver( StandardFunctionArgumentTypeResolvers.ARGUMENT_OR_IMPLIED_RESULT_TYPE ) + .setArgumentTypeResolver( ARGUMENT_OR_IMPLIED_RESULT_TYPE ) .register(); functionRegistry.registerAlternateKey( "bitand", "bit_and" ); functionRegistry.namedDescriptorBuilder( "bit_or" ) .setExactArgumentCount( 2 ) - .setArgumentTypeResolver( StandardFunctionArgumentTypeResolvers.ARGUMENT_OR_IMPLIED_RESULT_TYPE ) + .setArgumentTypeResolver( ARGUMENT_OR_IMPLIED_RESULT_TYPE ) .register(); functionRegistry.registerAlternateKey( "bitor", "bit_or" ); functionRegistry.namedDescriptorBuilder( "bit_xor" ) .setExactArgumentCount( 2 ) - .setArgumentTypeResolver( StandardFunctionArgumentTypeResolvers.ARGUMENT_OR_IMPLIED_RESULT_TYPE ) + .setArgumentTypeResolver( ARGUMENT_OR_IMPLIED_RESULT_TYPE ) .register(); functionRegistry.registerAlternateKey( "bitxor", "bit_xor" ); functionRegistry.namedDescriptorBuilder( "bit_not" ) .setExactArgumentCount( 1 ) - .setArgumentTypeResolver( StandardFunctionArgumentTypeResolvers.IMPLIED_RESULT_TYPE ) + .setArgumentTypeResolver( IMPLIED_RESULT_TYPE ) .register(); functionRegistry.registerAlternateKey( "bitnot", "bit_not" ); } @@ -949,25 +1014,25 @@ public void bitandorxornot_bitAndOrXorNot() { public void bitandorxornot_binAndOrXorNot() { functionRegistry.namedDescriptorBuilder( "bin_and" ) .setMinArgumentCount( 1 ) - .setArgumentTypeResolver( StandardFunctionArgumentTypeResolvers.ARGUMENT_OR_IMPLIED_RESULT_TYPE ) + .setArgumentTypeResolver( ARGUMENT_OR_IMPLIED_RESULT_TYPE ) .register(); functionRegistry.registerAlternateKey( "bitand", "bin_and" ); functionRegistry.namedDescriptorBuilder( "bin_or" ) .setMinArgumentCount( 1 ) - .setArgumentTypeResolver( StandardFunctionArgumentTypeResolvers.ARGUMENT_OR_IMPLIED_RESULT_TYPE ) + .setArgumentTypeResolver( ARGUMENT_OR_IMPLIED_RESULT_TYPE ) .register(); functionRegistry.registerAlternateKey( "bitor", "bin_or" ); functionRegistry.namedDescriptorBuilder( "bin_xor" ) .setMinArgumentCount( 1 ) - .setArgumentTypeResolver( StandardFunctionArgumentTypeResolvers.ARGUMENT_OR_IMPLIED_RESULT_TYPE ) + .setArgumentTypeResolver( ARGUMENT_OR_IMPLIED_RESULT_TYPE ) .register(); functionRegistry.registerAlternateKey( "bitxor", "bin_xor" ); functionRegistry.namedDescriptorBuilder( "bin_not" ) .setExactArgumentCount( 1 ) - .setArgumentTypeResolver( StandardFunctionArgumentTypeResolvers.IMPLIED_RESULT_TYPE ) + .setArgumentTypeResolver( IMPLIED_RESULT_TYPE ) .register(); functionRegistry.registerAlternateKey( "bitnot", "bin_not" ); } @@ -978,22 +1043,22 @@ public void bitandorxornot_binAndOrXorNot() { public void bitandorxornot_operator() { functionRegistry.patternDescriptorBuilder( "bitand", "(?1&?2)" ) .setExactArgumentCount( 2 ) - .setArgumentTypeResolver( StandardFunctionArgumentTypeResolvers.ARGUMENT_OR_IMPLIED_RESULT_TYPE ) + .setArgumentTypeResolver( ARGUMENT_OR_IMPLIED_RESULT_TYPE ) .register(); functionRegistry.patternDescriptorBuilder( "bitor", "(?1|?2)" ) .setExactArgumentCount( 2 ) - .setArgumentTypeResolver( StandardFunctionArgumentTypeResolvers.ARGUMENT_OR_IMPLIED_RESULT_TYPE ) + .setArgumentTypeResolver( ARGUMENT_OR_IMPLIED_RESULT_TYPE ) .register(); functionRegistry.patternDescriptorBuilder( "bitxor", "(?1^?2)" ) .setExactArgumentCount( 2 ) - .setArgumentTypeResolver( StandardFunctionArgumentTypeResolvers.ARGUMENT_OR_IMPLIED_RESULT_TYPE ) + .setArgumentTypeResolver( ARGUMENT_OR_IMPLIED_RESULT_TYPE ) .register(); functionRegistry.patternDescriptorBuilder( "bitnot", "~?1" ) .setExactArgumentCount( 1 ) - .setArgumentTypeResolver( StandardFunctionArgumentTypeResolvers.IMPLIED_RESULT_TYPE ) + .setArgumentTypeResolver( IMPLIED_RESULT_TYPE ) .register(); } @@ -1003,12 +1068,12 @@ public void bitandorxornot_operator() { public void bitAndOr() { functionRegistry.namedAggregateDescriptorBuilder( "bit_and" ) .setExactArgumentCount( 1 ) - .setArgumentTypeResolver( StandardFunctionArgumentTypeResolvers.ARGUMENT_OR_IMPLIED_RESULT_TYPE ) + .setArgumentTypeResolver( ARGUMENT_OR_IMPLIED_RESULT_TYPE ) .register(); functionRegistry.namedAggregateDescriptorBuilder( "bit_or" ) .setExactArgumentCount( 1 ) - .setArgumentTypeResolver( StandardFunctionArgumentTypeResolvers.ARGUMENT_OR_IMPLIED_RESULT_TYPE ) + .setArgumentTypeResolver( ARGUMENT_OR_IMPLIED_RESULT_TYPE ) .register(); //MySQL has it but how is that even useful? @@ -1558,7 +1623,7 @@ public void atan2_atn2() { public void coalesce() { functionRegistry.namedDescriptorBuilder( "coalesce" ) .setMinArgumentCount( 1 ) - .setArgumentTypeResolver( StandardFunctionArgumentTypeResolvers.ARGUMENT_OR_IMPLIED_RESULT_TYPE ) + .setArgumentTypeResolver( ARGUMENT_OR_IMPLIED_RESULT_TYPE ) .register(); } @@ -1568,7 +1633,7 @@ public void coalesce() { public void coalesce_value() { functionRegistry.namedDescriptorBuilder( "value" ) .setMinArgumentCount( 1 ) - .setArgumentTypeResolver( StandardFunctionArgumentTypeResolvers.ARGUMENT_OR_IMPLIED_RESULT_TYPE ) + .setArgumentTypeResolver( ARGUMENT_OR_IMPLIED_RESULT_TYPE ) .register(); functionRegistry.registerAlternateKey( "coalesce", "value" ); } @@ -1576,7 +1641,7 @@ public void coalesce_value() { public void nullif() { functionRegistry.namedDescriptorBuilder( "nullif" ) .setExactArgumentCount( 2 ) - .setArgumentTypeResolver( StandardFunctionArgumentTypeResolvers.ARGUMENT_OR_IMPLIED_RESULT_TYPE ) + .setArgumentTypeResolver( ARGUMENT_OR_IMPLIED_RESULT_TYPE ) .register(); } @@ -1968,12 +2033,12 @@ public void leastGreatest() { functionRegistry.namedDescriptorBuilder( "least" ) .setMinArgumentCount( 2 ) .setParameterTypes(COMPARABLE, COMPARABLE) - .setArgumentTypeResolver( StandardFunctionArgumentTypeResolvers.ARGUMENT_OR_IMPLIED_RESULT_TYPE ) + .setArgumentTypeResolver( ARGUMENT_OR_IMPLIED_RESULT_TYPE ) .register(); functionRegistry.namedDescriptorBuilder( "greatest" ) .setMinArgumentCount( 2 ) .setParameterTypes(COMPARABLE, COMPARABLE) - .setArgumentTypeResolver( StandardFunctionArgumentTypeResolvers.ARGUMENT_OR_IMPLIED_RESULT_TYPE ) + .setArgumentTypeResolver( ARGUMENT_OR_IMPLIED_RESULT_TYPE ) .register(); } @@ -1981,12 +2046,12 @@ public void leastGreatest_minMax() { functionRegistry.namedDescriptorBuilder( "least", "min" ) .setMinArgumentCount( 2 ) .setParameterTypes(COMPARABLE, COMPARABLE) - .setArgumentTypeResolver( StandardFunctionArgumentTypeResolvers.ARGUMENT_OR_IMPLIED_RESULT_TYPE ) + .setArgumentTypeResolver( ARGUMENT_OR_IMPLIED_RESULT_TYPE ) .register(); functionRegistry.namedDescriptorBuilder( "greatest", "max" ) .setMinArgumentCount( 2 ) .setParameterTypes(COMPARABLE, COMPARABLE) - .setArgumentTypeResolver( StandardFunctionArgumentTypeResolvers.ARGUMENT_OR_IMPLIED_RESULT_TYPE ) + .setArgumentTypeResolver( ARGUMENT_OR_IMPLIED_RESULT_TYPE ) .register(); } @@ -1994,12 +2059,12 @@ public void leastGreatest_minMaxValue() { functionRegistry.namedDescriptorBuilder( "least", "minvalue" ) .setMinArgumentCount( 2 ) .setParameterTypes(COMPARABLE, COMPARABLE) - .setArgumentTypeResolver( StandardFunctionArgumentTypeResolvers.ARGUMENT_OR_IMPLIED_RESULT_TYPE ) + .setArgumentTypeResolver( ARGUMENT_OR_IMPLIED_RESULT_TYPE ) .register(); functionRegistry.namedDescriptorBuilder( "greatest", "maxvalue" ) .setMinArgumentCount( 2 ) .setParameterTypes(COMPARABLE, COMPARABLE) - .setArgumentTypeResolver( StandardFunctionArgumentTypeResolvers.ARGUMENT_OR_IMPLIED_RESULT_TYPE ) + .setArgumentTypeResolver( ARGUMENT_OR_IMPLIED_RESULT_TYPE ) .register(); } @@ -2008,14 +2073,14 @@ public void aggregates(Dialect dialect, SqlAstNodeRenderingMode inferenceArgumen .setArgumentRenderingMode( inferenceArgumentRenderingMode ) .setExactArgumentCount( 1 ) .setParameterTypes(COMPARABLE) - .setArgumentTypeResolver( StandardFunctionArgumentTypeResolvers.IMPLIED_RESULT_TYPE ) + .setArgumentTypeResolver( IMPLIED_RESULT_TYPE ) .register(); functionRegistry.namedAggregateDescriptorBuilder( "min" ) .setArgumentRenderingMode( inferenceArgumentRenderingMode ) .setExactArgumentCount( 1 ) .setParameterTypes(COMPARABLE) - .setArgumentTypeResolver( StandardFunctionArgumentTypeResolvers.IMPLIED_RESULT_TYPE ) + .setArgumentTypeResolver( IMPLIED_RESULT_TYPE ) .register(); functionRegistry.namedAggregateDescriptorBuilder( "sum" ) @@ -2186,19 +2251,19 @@ public void windowFunctions() { functionRegistry.namedWindowDescriptorBuilder( "first_value" ) .setExactArgumentCount( 1 ) .setParameterTypes( ANY ) - .setArgumentTypeResolver( StandardFunctionArgumentTypeResolvers.IMPLIED_RESULT_TYPE ) + .setArgumentTypeResolver( IMPLIED_RESULT_TYPE ) .setArgumentListSignature( "ANY value" ) .register(); functionRegistry.namedWindowDescriptorBuilder( "last_value" ) .setExactArgumentCount( 1 ) .setParameterTypes( ANY ) - .setArgumentTypeResolver( StandardFunctionArgumentTypeResolvers.IMPLIED_RESULT_TYPE ) + .setArgumentTypeResolver( IMPLIED_RESULT_TYPE ) .setArgumentListSignature( "ANY value" ) .register(); functionRegistry.namedWindowDescriptorBuilder( "nth_value" ) .setExactArgumentCount( 2 ) .setParameterTypes( ANY, INTEGER ) - .setArgumentTypeResolver( StandardFunctionArgumentTypeResolvers.IMPLIED_RESULT_TYPE ) + .setArgumentTypeResolver( IMPLIED_RESULT_TYPE ) .setArgumentListSignature( "ANY value, INTEGER nth" ) .register(); } @@ -2280,6 +2345,17 @@ public void power_expLn() { .register(); } + /** + * power() for Spanner + */ + public void power_spanner() { + functionRegistry.patternDescriptorBuilder("power", "power(?1::float8, ?2::float8)") + .setExactArgumentCount(2) + .setParameterTypes(NUMERIC) + .setInvariantType(doubleType) + .register(); + } + public void round() { functionRegistry.namedDescriptorBuilder( "round" ) // To avoid truncating to a specific data type, we default to using the argument type @@ -2290,6 +2366,24 @@ public void round() { .register(); } + private static final String VAR_SAMP_SUM_COUNT_SPANNER_PATTERN = "(sum(power(cast(?1 as float8), cast(2 as float8)))-(power(cast(sum(?1) as float8), cast(2 as float8))/count(?1)))/nullif(count(?1)-1,0)"; + + public void stddevSamp_sumCount_spanner() { + functionRegistry.patternAggregateDescriptorBuilder( "stddev_samp", "sqrt(" + VAR_SAMP_SUM_COUNT_SPANNER_PATTERN + ")" ) + .setInvariantType( doubleType ) + .setExactArgumentCount( 1 ) + .setParameterTypes( NUMERIC ) + .register(); + } + + public void varSamp_sumCount_spanner() { + functionRegistry.patternAggregateDescriptorBuilder( "var_samp", VAR_SAMP_SUM_COUNT_SPANNER_PATTERN ) + .setInvariantType( doubleType ) + .setExactArgumentCount( 1 ) + .setParameterTypes( NUMERIC ) + .register(); + } + /** * SQL Server */ @@ -2353,6 +2447,14 @@ public void crc32() { .register(); } + public void sqrt_spanner() { + functionRegistry.patternDescriptorBuilder("sqrt", "sqrt(?1::float8)") + .setExactArgumentCount(1) + .setParameterTypes(NUMERIC) + .setInvariantType(doubleType) + .register(); + } + public void hex(String pattern) { functionRegistry.patternDescriptorBuilder( "hex", pattern ) .setInvariantType(stringType) @@ -2404,7 +2506,8 @@ public void sha() { .register(); } - public void timestampaddAndDiff(Dialect dialect, SqlAstNodeRenderingMode timestampRenderingMode) { + public void timestampaddAndDiff(Dialect dialect) { + // disallow plain parameter for timestamps argument since databases reject it functionRegistry.register( "timestampadd", new TimestampaddFunction( @@ -2412,7 +2515,7 @@ public void timestampaddAndDiff(Dialect dialect, SqlAstNodeRenderingMode timesta typeConfiguration, SqlAstNodeRenderingMode.DEFAULT, SqlAstNodeRenderingMode.DEFAULT, - timestampRenderingMode + NO_PLAIN_PARAMETER ) ); functionRegistry.register( @@ -2421,8 +2524,8 @@ public void timestampaddAndDiff(Dialect dialect, SqlAstNodeRenderingMode timesta dialect, typeConfiguration, SqlAstNodeRenderingMode.DEFAULT, - timestampRenderingMode, - timestampRenderingMode + NO_PLAIN_PARAMETER, + NO_PLAIN_PARAMETER ) ); } @@ -2985,6 +3088,41 @@ public void arrayLength_cardinality() { functionRegistry.register( "length", new DynamicDispatchFunction( functionRegistry, "character_length", "array_length" ) ); } + /** + * Spanner Postgres array_length() function + */ + public void arrayLength_spannerpg() { + functionRegistry.patternDescriptorBuilder( "array_length", "case when ?1 is null then null else coalesce(array_length(?1, 1), 0) end" ) + .setReturnTypeResolver( StandardFunctionReturnTypeResolvers.invariant( integerType ) ) + .setArgumentsValidator( + StandardArgumentsValidators.composite( + StandardArgumentsValidators.exactly( 1 ), + ArrayArgumentValidator.DEFAULT_INSTANCE + ) + ) + .setArgumentListSignature( "(ARRAY array)" ) + .register(); + functionRegistry.register( "length", new DynamicDispatchFunction( functionRegistry, "character_length", "array_length" ) ); + functionRegistry.registerAlternateKey( "cardinality", "array_length" ); + } + + /** + * Spanner array_length() function + */ + public void arrayLength_spanner() { + functionRegistry.patternDescriptorBuilder( "array_length", "array_length(?1)" ) + .setReturnTypeResolver( StandardFunctionReturnTypeResolvers.invariant( integerType ) ) + .setArgumentsValidator( + StandardArgumentsValidators.composite( + StandardArgumentsValidators.exactly( 1 ), + ArrayArgumentValidator.DEFAULT_INSTANCE + ) + ) + .setArgumentListSignature( "(ARRAY array)" ) + .register(); + functionRegistry.register( "length", new DynamicDispatchFunction( functionRegistry, "character_length", "array_length" ) ); + } + /** * Oracle array_length() function */ @@ -3072,21 +3210,16 @@ public void arrayGet_h2() { .setArgumentListSignature( "(ARRAY array, INTEGER index)" ) .register(); } + + public void arrayGet_bracket() { + arrayGet_bracket( true ); + } + /** * CockroachDB and PostgreSQL array_get() function via bracket syntax */ - public void arrayGet_bracket() { - functionRegistry.patternDescriptorBuilder( "array_get", "?1[?2]" ) - .setReturnTypeResolver( ElementViaArrayArgumentReturnTypeResolver.DEFAULT_INSTANCE ) - .setArgumentsValidator( - StandardArgumentsValidators.composite( - ArrayArgumentValidator.DEFAULT_INSTANCE, - new ArgumentTypesValidator( null, ANY, INTEGER ) - ) - ) - .setArgumentTypeResolver( StandardFunctionArgumentTypeResolvers.invariant( ANY, INTEGER ) ) - .setArgumentListSignature( "(ARRAY array, INTEGER index)" ) - .register(); + public void arrayGet_bracket(boolean supportsJsonBracket) { + functionRegistry.register( "array_get", new ArrayGetBracketFunction( supportsJsonBracket ) ); } /** @@ -3204,7 +3337,7 @@ public void arraySlice() { .setArgumentTypeResolver( StandardFunctionArgumentTypeResolvers.composite( StandardFunctionArgumentTypeResolvers.invariant( ANY, INTEGER, INTEGER ), - StandardFunctionArgumentTypeResolvers.IMPLIED_RESULT_TYPE + IMPLIED_RESULT_TYPE ) ) .setArgumentListSignature( "(ARRAY array, INTEGER start, INTEGER end)" ) @@ -3233,7 +3366,7 @@ public void arraySlice_operator() { .setArgumentTypeResolver( StandardFunctionArgumentTypeResolvers.composite( StandardFunctionArgumentTypeResolvers.invariant( ANY, INTEGER, INTEGER ), - StandardFunctionArgumentTypeResolvers.IMPLIED_RESULT_TYPE + IMPLIED_RESULT_TYPE ) ) .setArgumentListSignature( "(ARRAY array, INTEGER start, INTEGER end)" ) @@ -3300,7 +3433,7 @@ public void arrayTrim_trim_array() { .setArgumentTypeResolver( StandardFunctionArgumentTypeResolvers.composite( StandardFunctionArgumentTypeResolvers.invariant( ANY, INTEGER ), - StandardFunctionArgumentTypeResolvers.IMPLIED_RESULT_TYPE + IMPLIED_RESULT_TYPE ) ) .setArgumentListSignature( "(ARRAY array, INTEGER elementsToRemove)" ) @@ -3321,6 +3454,108 @@ public void arrayTrim_oracle() { functionRegistry.register( "array_trim", new OracleArrayTrimFunction() ); } + /** + * CockroachDB and PostgreSQL array_reverse() function + */ + public void arrayReverse() { + functionRegistry.namedDescriptorBuilder( "array_reverse" ) + .setArgumentsValidator( + StandardArgumentsValidators.composite( + StandardArgumentsValidators.exactly( 1 ), + ArrayArgumentValidator.DEFAULT_INSTANCE + ) ) + .setReturnTypeResolver( ArrayViaArgumentReturnTypeResolver.DEFAULT_INSTANCE ) + .setArgumentTypeResolver( + StandardFunctionArgumentTypeResolvers.composite( + StandardFunctionArgumentTypeResolvers.invariant( ANY ) + ) ) + .setArgumentListSignature( "(ARRAY array)" ) + .register(); + } + + /** + * array_reverse() emulation for PostgreSQL versions before 18 and HSQLDB + */ + public void arrayReverse_unnest() { + functionRegistry.register( "array_reverse", new PostgreSQLArrayReverseEmulation() ); + } + + /** + * Oracle array_reverse() function + */ + public void arrayReverse_oracle() { + functionRegistry.register( "array_reverse", new OracleArrayReverseFunction() ); + } + + /** + * H2 array_reverse() function + */ + public void arrayReverse_h2(int maximumArraySize) { + functionRegistry.register( "array_reverse", new H2ArrayReverseFunction( maximumArraySize ) ); + } + + /** + * CockroachDB and PostgreSQL array_sort() function + */ + public void arraySort() { + functionRegistry.namedDescriptorBuilder( "array_sort" ) + .setArgumentsValidator( + new ArgumentTypesValidator( + StandardArgumentsValidators.composite( + StandardArgumentsValidators.between( 1, 3 ), + ArrayArgumentValidator.DEFAULT_INSTANCE + ), + FunctionParameterType.ANY, + FunctionParameterType.BOOLEAN, + FunctionParameterType.BOOLEAN + ) + ) + .setReturnTypeResolver( ArrayViaArgumentReturnTypeResolver.DEFAULT_INSTANCE ) + .setArgumentTypeResolver( + StandardFunctionArgumentTypeResolvers.composite( + StandardFunctionArgumentTypeResolvers.invariant( ANY ), + StandardFunctionArgumentTypeResolvers.invariant( + typeConfiguration, + FunctionParameterType.BOOLEAN + ), + StandardFunctionArgumentTypeResolvers.invariant( + typeConfiguration, + FunctionParameterType.BOOLEAN + ) + ) + ) + .setArgumentListSignature( "(ARRAY array[, boolean descending[, boolean nulls_first]])" ) + .register(); + } + + /** + * PostgreSQL array_sort() emulation for versions before 18 + */ + public void arraySort_unnest() { + functionRegistry.register( "array_sort", new PostgreSQLArraySortEmulation( typeConfiguration ) ); + } + + /** + * Oracle array_sort() function + */ + public void arraySort_oracle() { + functionRegistry.register( "array_sort", new OracleArraySortFunction( typeConfiguration ) ); + } + + /** + * H2 array_sort() function + */ + public void arraySort_h2(int maximumArraySize) { + functionRegistry.register( "array_sort", new H2ArraySortFunction( maximumArraySize, typeConfiguration ) ); + } + + /** + * HSQL array_sort() function + */ + public void arraySort_hsql() { + functionRegistry.register( "array_sort", new HSQLArraySortFunction( typeConfiguration ) ); + } + /** * H2 array_fill() function */ @@ -4267,7 +4502,7 @@ public void xmlagg_sqlserver() { * Standard unnest() function */ public void unnest(@Nullable String defaultBasicArrayElementColumnName, String defaultIndexSelectionExpression) { - functionRegistry.register( "unnest", new UnnestFunction( defaultBasicArrayElementColumnName, defaultIndexSelectionExpression ) ); + functionRegistry.register( "unnest", new UnnestFunction( defaultBasicArrayElementColumnName, defaultIndexSelectionExpression, false ) ); } /** @@ -4288,10 +4523,18 @@ public void unnest_h2(int maxArraySize) { /** * Oracle unnest() function */ + @Deprecated(forRemoval = true) public void unnest_oracle() { functionRegistry.register( "unnest", new OracleUnnestFunction() ); } + /** + * Oracle unnest() function + */ + public void unnest_oracle(boolean supportsJsonType) { + functionRegistry.register( "unnest", new OracleUnnestFunction( supportsJsonType ) ); + } + /** * PostgreSQL unnest() function */ diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/CountFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/CountFunction.java index f9fb0a665246..cfe20aa577fe 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/CountFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/CountFunction.java @@ -52,6 +52,7 @@ public class CountFunction extends AbstractSqmSelfRenderingFunctionDescriptor { private final String concatArgumentCastType; private final boolean castDistinctStringConcat; private final String distinctArgumentCastType; + private final int separatorAsciiCode; public CountFunction( Dialect dialect, @@ -116,6 +117,29 @@ public CountFunction( String concatArgumentCastType, boolean castDistinctStringConcat, String distinctArgumentCastType) { + this( + dialect, + typeConfiguration, + defaultArgumentRenderingMode, + countFunctionName, + concatOperator, + concatArgumentCastType, + castDistinctStringConcat, + distinctArgumentCastType, + 0 + ); + } + + public CountFunction( + Dialect dialect, + TypeConfiguration typeConfiguration, + SqlAstNodeRenderingMode defaultArgumentRenderingMode, + String countFunctionName, + String concatOperator, + String concatArgumentCastType, + boolean castDistinctStringConcat, + String distinctArgumentCastType, + int separatorAsciiCode) { super( "count", FunctionKind.AGGREGATE, @@ -132,6 +156,7 @@ public CountFunction( this.concatArgumentCastType = concatArgumentCastType; this.castDistinctStringConcat = castDistinctStringConcat; this.distinctArgumentCastType = distinctArgumentCastType; + this.separatorAsciiCode = separatorAsciiCode; } @Override @@ -188,7 +213,7 @@ else if ( !dialect.supportsTupleDistinctCounts() ) { .findFunctionDescriptor( "chr" ); final List chrArguments = List.of( new QueryLiteral<>( - 0, + separatorAsciiCode, translator.getSessionFactory().getTypeConfiguration() .getBasicTypeForJavaType( Integer.class ) ) diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/ExtractFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/ExtractFunction.java index 095917d69b10..15879a2ce2ab 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/ExtractFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/ExtractFunction.java @@ -147,7 +147,7 @@ private SelfRenderingSqmFunction extractWeek( QueryEngine queryEngine) { final NodeBuilder builder = field.nodeBuilder(); - final TypeConfiguration typeConfiguration = queryEngine.getTypeConfiguration(); + final var typeConfiguration = queryEngine.getTypeConfiguration(); final BasicType intType = typeConfiguration.getBasicTypeForJavaType( Integer.class ); final BasicType floatType = typeConfiguration.getBasicTypeForJavaType( Float.class ); @@ -240,7 +240,7 @@ private SelfRenderingSqmFunction extractNanoseconds( QueryEngine queryEngine) { final NodeBuilder builder = expressionToExtract.nodeBuilder(); - final TypeConfiguration typeConfiguration = queryEngine.getTypeConfiguration(); + final var typeConfiguration = queryEngine.getTypeConfiguration(); final BasicType floatType = typeConfiguration.getBasicTypeForJavaType(Float.class); final SqmExtractUnit extractSeconds = new SqmExtractUnit<>( SECOND, floatType, builder ); @@ -266,7 +266,7 @@ private SelfRenderingSqmFunction extractOffsetUsingFormat( QueryEngine queryEngine) { final NodeBuilder builder = expressionToExtract.nodeBuilder(); - final TypeConfiguration typeConfiguration = queryEngine.getTypeConfiguration(); + final var typeConfiguration = queryEngine.getTypeConfiguration(); final BasicType offsetType = typeConfiguration.getBasicTypeForJavaType(ZoneOffset.class); final BasicType stringType = typeConfiguration.getBasicTypeForJavaType(String.class); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/FormatFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/FormatFunction.java index 0d99aa6bf826..c64bea4faacd 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/FormatFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/FormatFunction.java @@ -223,7 +223,7 @@ public Expression convertToSqlAst(SqmToSqlAstConverter walker) { .resolve( StandardBasicTypes.STRING ); final Dialect dialect = walker.getCreationContext().getDialect(); Expression formatExpression = null; - final StringBuilder sb = new StringBuilder(); + final var sb = new StringBuilder(); final StringBuilderSqlAppender sqlAppender = new StringBuilderSqlAppender( sb ); final String delimiter; if ( supportsPatternLiterals ) { diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/NumberSeriesGenerateSeriesFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/NumberSeriesGenerateSeriesFunction.java index 2633d0acbd5e..b2054ee942ac 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/NumberSeriesGenerateSeriesFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/NumberSeriesGenerateSeriesFunction.java @@ -48,7 +48,7 @@ import org.hibernate.sql.ast.tree.predicate.PredicateContainer; import org.hibernate.sql.ast.tree.select.QuerySpec; import org.hibernate.sql.ast.tree.select.SelectStatement; -import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; +import org.hibernate.sql.exec.spi.JdbcSelect; import org.hibernate.sql.results.internal.SqlSelectionImpl; import org.hibernate.type.BasicType; import org.hibernate.type.SqlTypes; @@ -458,10 +458,10 @@ private static String timestampadd(String startExpression, String stepExpression type ) ) ); - final SqlAstTranslator translator = + final SqlAstTranslator translator = creationContext.getDialect().getSqlAstTranslatorFactory() .buildSelectTranslator( creationContext.getSessionFactory(), new SelectStatement( fakeQuery ) ); - final JdbcOperationQuerySelect operation = translator.translate( null, QueryOptions.NONE ); + final JdbcSelect operation = translator.translate( null, QueryOptions.NONE ); final String sqlString = operation.getSqlString(); assert sqlString.startsWith( "select " ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/PostgreSQLTruncFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/PostgreSQLTruncFunction.java index 1a3af951d364..b1a83bb91a57 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/PostgreSQLTruncFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/PostgreSQLTruncFunction.java @@ -23,6 +23,10 @@ public class PostgreSQLTruncFunction extends TruncFunction { private final PostgreSQLTruncRoundFunction postgreSQLTruncRoundFunction; public PostgreSQLTruncFunction(boolean supportsTwoArguments, TypeConfiguration typeConfiguration) { + this( false, supportsTwoArguments, typeConfiguration ); + } + + public PostgreSQLTruncFunction(boolean requiresArgumentCasts, boolean supportsTwoArguments, TypeConfiguration typeConfiguration) { super( "trunc(?1)", null, @@ -30,7 +34,7 @@ public PostgreSQLTruncFunction(boolean supportsTwoArguments, TypeConfiguration t null, typeConfiguration ); - this.postgreSQLTruncRoundFunction = new PostgreSQLTruncRoundFunction( "trunc", supportsTwoArguments ); + this.postgreSQLTruncRoundFunction = new PostgreSQLTruncRoundFunction( "trunc", requiresArgumentCasts, supportsTwoArguments ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/PostgreSQLTruncRoundFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/PostgreSQLTruncRoundFunction.java index f91c3fd50d0e..ca3258639732 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/PostgreSQLTruncRoundFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/PostgreSQLTruncRoundFunction.java @@ -36,8 +36,7 @@ * This custom function falls back to using {@code floor} as a workaround only when necessary, * e.g. when there are 2 arguments to the function and either: *
      - *
    • The first argument is not of type {@code numeric}
    • - * or + *
    • The first argument is not of type {@code numeric}, or
    • *
    • The dialect doesn't support the two-argument {@code trunc} function
    • *
    * @@ -45,15 +44,21 @@ * @see PostgreSQL documentation */ public class PostgreSQLTruncRoundFunction extends AbstractSqmFunctionDescriptor implements FunctionRenderer { + private final boolean requiresArgumentCasts; private final boolean supportsTwoArguments; public PostgreSQLTruncRoundFunction(String name, boolean supportsTwoArguments) { + this( name, false, supportsTwoArguments ); + } + + public PostgreSQLTruncRoundFunction(String name, boolean requiresArgumentCasts, boolean supportsTwoArguments) { super( name, new ArgumentTypesValidator( StandardArgumentsValidators.between( 1, 2 ), NUMERIC, INTEGER ), StandardFunctionReturnTypeResolvers.useArgType( 1 ), StandardFunctionArgumentTypeResolvers.invariant( NUMERIC, INTEGER ) ); + this.requiresArgumentCasts = requiresArgumentCasts; this.supportsTwoArguments = supportsTwoArguments; } @@ -79,23 +84,69 @@ public void render( } else { // workaround using floor + sqlAppender.appendSql( '(' ); + final SqlAstNode secondArg = arguments.get( 1 ); if ( getName().equals( "trunc" ) ) { sqlAppender.appendSql( "sign(" ); + if ( requiresArgumentCasts ) { + sqlAppender.appendSql( "cast(" ); + } firstArg.accept( walker ); + if ( requiresArgumentCasts ) { + sqlAppender.appendSql( " as numeric)" ); + } sqlAppender.appendSql( ")*floor(abs(" ); + if ( requiresArgumentCasts ) { + sqlAppender.appendSql( "cast(" ); + } firstArg.accept( walker ); - sqlAppender.appendSql( ")*1e" ); - arguments.get( 1 ).accept( walker ); + if ( requiresArgumentCasts ) { + sqlAppender.appendSql( " as numeric)" ); + } + sqlAppender.appendSql( ")*" ); + if ( requiresArgumentCasts ) { + sqlAppender.appendSql( "cast(" ); + } + sqlAppender.appendSql( "power(10," ); + secondArg.accept( walker ); + sqlAppender.appendSql( ')' ); + if ( requiresArgumentCasts ) { + sqlAppender.appendSql( " as numeric)" ); + } + sqlAppender.appendSql( ')' ); } else { sqlAppender.appendSql( "floor(" ); + if ( requiresArgumentCasts ) { + sqlAppender.appendSql( "cast(" ); + } firstArg.accept( walker ); - sqlAppender.appendSql( "*1e" ); - arguments.get( 1 ).accept( walker ); - sqlAppender.appendSql( "+0.5" ); + if ( requiresArgumentCasts ) { + sqlAppender.appendSql( " as numeric)" ); + } + sqlAppender.appendSql( "*" ); + if ( requiresArgumentCasts ) { + sqlAppender.appendSql( "cast(" ); + } + sqlAppender.appendSql( "power(10," ); + secondArg.accept( walker ); + sqlAppender.appendSql( ')' ); + if ( requiresArgumentCasts ) { + sqlAppender.appendSql( " as numeric)" ); + } + sqlAppender.appendSql( "+0.5)" ); + } + sqlAppender.appendSql( '/' ); + if ( requiresArgumentCasts ) { + sqlAppender.appendSql( "cast(" ); + } + sqlAppender.appendSql( "power(10," ); + secondArg.accept( walker ); + sqlAppender.appendSql( ')' ); + if ( requiresArgumentCasts ) { + sqlAppender.appendSql( " as numeric)" ); } - sqlAppender.appendSql( ")/1e" ); - arguments.get( 1 ).accept( walker ); + sqlAppender.appendSql( ')' ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/SpannerConcatFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/SpannerConcatFunction.java new file mode 100644 index 000000000000..8b3537789124 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/SpannerConcatFunction.java @@ -0,0 +1,48 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function; + +import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor; +import org.hibernate.query.sqm.function.FunctionKind; +import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators; +import org.hibernate.query.sqm.produce.function.StandardFunctionArgumentTypeResolvers; +import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.type.StandardBasicTypes; +import org.hibernate.type.spi.TypeConfiguration; + +import java.util.List; + +import static org.hibernate.query.sqm.produce.function.FunctionParameterType.STRING; + +public class SpannerConcatFunction extends AbstractSqmSelfRenderingFunctionDescriptor { + + private static final String CAST = "::text"; + + public SpannerConcatFunction(TypeConfiguration typeConfiguration) { + super( "concat", + FunctionKind.NORMAL, + StandardArgumentsValidators.min( 1 ), + StandardFunctionReturnTypeResolvers.invariant( + typeConfiguration.getBasicTypeRegistry().resolve( StandardBasicTypes.STRING ) + ), + StandardFunctionArgumentTypeResolvers.impliedOrInvariant( typeConfiguration, STRING)); + } + + @Override + public void render(SqlAppender sqlAppender, List sqlAstArguments, ReturnableType returnType, SqlAstTranslator walker) { + sqlAppender.append( "concat((" ); + sqlAstArguments.get( 0 ).accept( walker ); + for ( int i = 1; i < sqlAstArguments.size(); i++ ) { + sqlAppender.append( ")::text" ); + sqlAppender.append( ",(" ); + sqlAstArguments.get( i ).accept( walker ); + } + sqlAppender.append( ")::text)" ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/SpannerExtractFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/SpannerExtractFunction.java new file mode 100644 index 000000000000..dcdabcffd492 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/SpannerExtractFunction.java @@ -0,0 +1,53 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function; + +import jakarta.persistence.TemporalType; +import org.hibernate.dialect.Dialect; +import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.query.common.TemporalUnit; +import org.hibernate.query.sqm.produce.function.internal.PatternRenderer; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.ExtractUnit; +import org.hibernate.type.spi.TypeConfiguration; + +import java.util.List; + +import static org.hibernate.type.spi.TypeConfiguration.getSqlTemporalType; + +/** + * uses unix_seconds function for EPOCH unit + */ +public class SpannerExtractFunction extends ExtractFunction { + public SpannerExtractFunction(Dialect dialect, TypeConfiguration typeConfiguration) { + super( dialect, typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + new PatternRenderer( extractPattern( sqlAstArguments ) ).render( sqlAppender, sqlAstArguments, walker ); + } + + @SuppressWarnings("deprecation") + private String extractPattern(List sqlAstArguments) { + var field = (ExtractUnit) sqlAstArguments.get( 0 ); + if ( field.getUnit() == TemporalUnit.EPOCH ) { + var expression = (Expression) sqlAstArguments.get( 1 ); + var type = expression.getExpressionType(); + var temporalType = type != null ? getSqlTemporalType( type ) : null; + return temporalType == TemporalType.DATE + ? "unix_seconds(timestamp(?2))" + : "unix_seconds(?2)"; + } + return dialect.extractPattern( field.getUnit() ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/SpannerFormatFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/SpannerFormatFunction.java new file mode 100644 index 000000000000..161c76dd85c6 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/SpannerFormatFunction.java @@ -0,0 +1,46 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function; + +import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.type.spi.TypeConfiguration; + +import java.util.List; + +import static org.hibernate.type.spi.TypeConfiguration.getSqlTemporalType; + +/** + * The format function for Spanner. + * It uses FORMAT_DATE for temporal type date and FORMAT_TIMESTAMP for temporal type time and timestamp. + */ +public class SpannerFormatFunction extends FormatFunction { + public SpannerFormatFunction(TypeConfiguration typeConfiguration) { + super("format_timestamp", true, true, false, typeConfiguration); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + var datetime = (Expression) sqlAstArguments.get( 0 ); + var format = sqlAstArguments.get( 1 ); + var temporalType = getSqlTemporalType( datetime.getExpressionType() ); + switch ( temporalType ) { + case DATE -> sqlAppender.appendSql( "format_date(" ); + case TIME, TIMESTAMP -> sqlAppender.appendSql( "format_timestamp(" ); + default -> throw new IllegalArgumentException( "Unsupported temporal type: " + temporalType ); + } + format.accept( walker ); + sqlAppender.append( ',' ); + datetime.accept( walker ); + sqlAppender.append( ')' ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/SpannerPostgreSQLRegexpLikeFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/SpannerPostgreSQLRegexpLikeFunction.java new file mode 100644 index 000000000000..42d785f79cd3 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/SpannerPostgreSQLRegexpLikeFunction.java @@ -0,0 +1,34 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function; + +import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.type.spi.TypeConfiguration; + +import java.util.List; + + +public class SpannerPostgreSQLRegexpLikeFunction extends AbstractRegexpLikeFunction { + + public SpannerPostgreSQLRegexpLikeFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + public void render(SqlAppender sqlAppender, List sqlAstArguments, ReturnableType returnType, SqlAstTranslator walker) { + sqlAppender.append( "regexp_match(" ); + sqlAstArguments.get( 0 ).accept( walker ); + sqlAppender.append( "," ); + sqlAstArguments.get( 1 ).accept( walker ); + if (sqlAstArguments.size() > 2) { + sqlAppender.append( "," ); + sqlAstArguments.get( 2 ).accept( walker ); + } + sqlAppender.append( ") IS NOT NULL" ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/SpannerPostgreSQLTruncFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/SpannerPostgreSQLTruncFunction.java new file mode 100644 index 000000000000..3fbaacbc746d --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/SpannerPostgreSQLTruncFunction.java @@ -0,0 +1,47 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function; + +import java.util.ArrayList; +import java.util.List; + +import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.query.spi.QueryEngine; +import org.hibernate.query.sqm.function.SelfRenderingSqmFunction; +import org.hibernate.query.sqm.tree.SqmTypedNode; +import org.hibernate.query.sqm.tree.expression.SqmExtractUnit; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * Spanner-specific TruncFunction that extends PostgreSQLTruncFunction to handle + * both numeric and datetime truncation, reusing PostgreSQL logic where + * applicable. + */ +public class SpannerPostgreSQLTruncFunction extends PostgreSQLTruncFunction { + private final SpannerPostgreSQLTruncRoundFunction spannerPostgreSQLTruncRoundFunction; + + public SpannerPostgreSQLTruncFunction(TypeConfiguration typeConfiguration) { + super(false, typeConfiguration); + this.spannerPostgreSQLTruncRoundFunction = new SpannerPostgreSQLTruncRoundFunction(); + } + + @Override + protected SelfRenderingSqmFunction generateSqmFunctionExpression( + List> arguments, + ReturnableType impliedResultType, + QueryEngine queryEngine) { + final List> args = new ArrayList<>(arguments); + if (arguments.size() != 2 || !(arguments.get(1) instanceof SqmExtractUnit)) { + // numeric truncation - delegate to Spanner-specific implementation + return spannerPostgreSQLTruncRoundFunction.generateSqmFunctionExpression( + arguments, + impliedResultType, + queryEngine); + } + // datetime truncation - delegate to parent (PostgreSQLTruncFunction) which + // handles it correctly + return super.generateSqmFunctionExpression(arguments, impliedResultType, queryEngine); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/SpannerPostgreSQLTruncRoundFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/SpannerPostgreSQLTruncRoundFunction.java new file mode 100644 index 000000000000..b705d57a238d --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/SpannerPostgreSQLTruncRoundFunction.java @@ -0,0 +1,41 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function; + +import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; + +import java.util.List; + +public class SpannerPostgreSQLTruncRoundFunction extends PostgreSQLTruncRoundFunction { + + public SpannerPostgreSQLTruncRoundFunction() { + super( "trunc", false ); + } + + @Override + public void render(SqlAppender sqlAppender, List arguments, ReturnableType returnType, SqlAstTranslator walker) { + final int numberOfArguments = arguments.size(); + final Expression firstArg = (Expression) arguments.get( 0 ); + if ( numberOfArguments == 1 ) { + sqlAppender.appendSql( getName() ); + sqlAppender.appendSql( "(" ); + firstArg.accept( walker ); + sqlAppender.appendSql( "::float8" ); + sqlAppender.appendSql( ")" ); + } + else { + final SqlAstNode secondArg = arguments.get( 1 ); + sqlAppender.appendSql( "trunc(cast((" ); + firstArg.accept( walker ); + sqlAppender.appendSql( ") as numeric), cast(" ); + secondArg.accept( walker ); + sqlAppender.appendSql( " as integer))"); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/SpannerTruncFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/SpannerTruncFunction.java new file mode 100644 index 000000000000..8e4cb5e3f421 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/SpannerTruncFunction.java @@ -0,0 +1,78 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function; + +import java.util.List; + +import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor; +import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers; +import org.hibernate.query.sqm.produce.function.StandardFunctionArgumentTypeResolvers; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; + + +import static org.hibernate.type.spi.TypeConfiguration.getSqlTemporalType; + +/** + * The trunc function for Spanner. + * It renders DATE_TRUNC for dates, TIMESTAMP_TRUNC for timestamps, and TRUNC for numeric types. + */ +public class SpannerTruncFunction extends AbstractSqmSelfRenderingFunctionDescriptor { + + public SpannerTruncFunction() { + super( + "trunc", + new TruncFunction.TruncArgumentsValidator(), + StandardFunctionReturnTypeResolvers.useArgType( 1 ), + StandardFunctionArgumentTypeResolvers.byArgument( + StandardFunctionArgumentTypeResolvers.IMPLIED_RESULT_TYPE, + StandardFunctionArgumentTypeResolvers.NULL + ) + ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + final Expression expression = (Expression) sqlAstArguments.get( 0 ); + final var type = expression.getExpressionType(); + final var temporalType = type != null ? getSqlTemporalType( type ) : null; + + if ( temporalType != null ) { + switch ( temporalType ) { + case DATE -> sqlAppender.appendSql( "DATE_TRUNC" ); + case TIMESTAMP, TIME -> sqlAppender.appendSql( "TIMESTAMP_TRUNC" ); + default -> throw new IllegalArgumentException( "Unsupported temporal type: " + temporalType ); + } + sqlAppender.appendSql( "(" ); + expression.accept( walker ); + sqlAppender.appendSql( ", " ); + sqlAstArguments.get( 1 ).accept( walker ); + sqlAppender.appendSql( ")" ); + } + else { + renderNumericTrunc( sqlAppender, sqlAstArguments, walker ); + } + } + + private void renderNumericTrunc( + SqlAppender sqlAppender, + List args, + SqlAstTranslator walker) { + sqlAppender.appendSql( "TRUNC(" ); + args.get( 0 ).accept( walker ); + if ( args.size() > 1 ) { + sqlAppender.appendSql( ", " ); + args.get( 1 ).accept( walker ); + } + sqlAppender.appendSql( ")" ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/TruncFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/TruncFunction.java index 3b963edfe7d8..9d75f601e484 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/TruncFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/TruncFunction.java @@ -8,6 +8,7 @@ import java.util.List; import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.sql.ast.SqlAstNodeRenderingMode; import org.hibernate.type.BindingContext; import org.hibernate.query.spi.QueryEngine; import org.hibernate.query.sqm.function.AbstractSqmFunctionDescriptor; @@ -65,6 +66,16 @@ public TruncFunction( DatetimeTrunc datetimeTrunc, String toDateFunction, TypeConfiguration typeConfiguration) { + this( truncPattern, twoArgTruncPattern, datetimeTrunc, toDateFunction, SqlAstNodeRenderingMode.DEFAULT, typeConfiguration ); + } + + public TruncFunction( + String truncPattern, + String twoArgTruncPattern, + DatetimeTrunc datetimeTrunc, + String toDateFunction, + SqlAstNodeRenderingMode argumentRenderingMode, + TypeConfiguration typeConfiguration) { super( "trunc", new TruncArgumentsValidator(), @@ -76,8 +87,8 @@ public TruncFunction( ); this.datetimeTrunc = datetimeTrunc; numericRenderingSupport = - new TruncRenderingSupport( new PatternRenderer( truncPattern ), - twoArgTruncPattern == null ? null : new PatternRenderer( twoArgTruncPattern ) ); + new TruncRenderingSupport( new PatternRenderer( truncPattern, argumentRenderingMode ), + twoArgTruncPattern == null ? null : new PatternRenderer( twoArgTruncPattern, argumentRenderingMode ) ); if ( datetimeTrunc == null ) { dateTruncEmulation = null; datetimeRenderingSupport = null; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/UnnestSetReturningFunctionTypeResolver.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/UnnestSetReturningFunctionTypeResolver.java index d751ff7d1220..0c670ffe743f 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/UnnestSetReturningFunctionTypeResolver.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/UnnestSetReturningFunctionTypeResolver.java @@ -8,6 +8,7 @@ import java.util.List; import org.checkerframework.checker.nullness.qual.Nullable; +import org.hibernate.engine.jdbc.Size; import org.hibernate.metamodel.mapping.AttributeMapping; import org.hibernate.metamodel.mapping.CollectionPart; import org.hibernate.metamodel.mapping.EmbeddableMappingType; @@ -28,6 +29,7 @@ import org.hibernate.type.BasicPluralType; import org.hibernate.type.BasicType; import org.hibernate.type.descriptor.jdbc.AggregateJdbcType; +import org.hibernate.type.descriptor.sql.DdlType; import org.hibernate.type.spi.TypeConfiguration; /** @@ -158,15 +160,27 @@ public SelectableMapping[] resolveFunctionReturnType( final String elementSelectionExpression = defaultBasicArrayColumnName == null ? tableIdentifierVariable : defaultBasicArrayColumnName; + final TypeConfiguration typeConfiguration = converter.getCreationContext().getTypeConfiguration(); + final DdlType ddlType = typeConfiguration.getDdlTypeRegistry() + .getDescriptor( elementType.getJdbcType().getDefaultSqlTypeCode() ); final SelectableMapping elementMapping; if ( expressionType instanceof SqlTypedMapping typedMapping ) { + final String columnTypeName = ddlType.getTypeName( + new Size( + typedMapping.getPrecision(), + typedMapping.getScale(), + typedMapping.getLength() + ), + elementType, + typeConfiguration.getDdlTypeRegistry() + ); elementMapping = new SelectableMappingImpl( "", elementSelectionExpression, new SelectablePath( CollectionPart.Nature.ELEMENT.getName() ), null, null, - typedMapping.getColumnDefinition(), + columnTypeName, typedMapping.getLength(), typedMapping.getArrayLength(), typedMapping.getPrecision(), @@ -182,13 +196,18 @@ public SelectableMapping[] resolveFunctionReturnType( ); } else { + final String columnTypeName = ddlType.getTypeName( + Size.nil(), + elementType, + typeConfiguration.getDdlTypeRegistry() + ); elementMapping = new SelectableMappingImpl( "", elementSelectionExpression, new SelectablePath( CollectionPart.Nature.ELEMENT.getName() ), null, null, - null, + columnTypeName, null, null, null, diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/AbstractArrayContainsFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/AbstractArrayContainsFunction.java index 5b2964e0ead8..53e28f37a919 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/AbstractArrayContainsFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/AbstractArrayContainsFunction.java @@ -13,6 +13,7 @@ import org.jboss.logging.Logger; import java.lang.invoke.MethodHandles; +import java.util.Locale; /** * Encapsulates the validator, return type and argument type resolvers for the array_contains function. @@ -20,7 +21,7 @@ */ public abstract class AbstractArrayContainsFunction extends AbstractSqmSelfRenderingFunctionDescriptor { - protected static final DeprecationLogger LOG = Logger.getMessageLogger( MethodHandles.lookup(), DeprecationLogger.class, AbstractArrayContainsFunction.class.getName() ); + protected static final DeprecationLogger LOG = Logger.getMessageLogger( MethodHandles.lookup(), DeprecationLogger.class, AbstractArrayContainsFunction.class.getName(), Locale.ROOT ); protected final boolean nullable; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/AbstractArrayGetFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/AbstractArrayGetFunction.java new file mode 100644 index 000000000000..cba044891f8d --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/AbstractArrayGetFunction.java @@ -0,0 +1,31 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.array; + +import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor; +import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator; +import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators; +import org.hibernate.query.sqm.produce.function.StandardFunctionArgumentTypeResolvers; + +import static org.hibernate.query.sqm.produce.function.FunctionParameterType.ANY; +import static org.hibernate.query.sqm.produce.function.FunctionParameterType.INTEGER; + +/** + * Basic array get function configuration. + */ +public abstract class AbstractArrayGetFunction extends AbstractSqmSelfRenderingFunctionDescriptor { + + public AbstractArrayGetFunction() { + super( + "array_get", + StandardArgumentsValidators.composite( + ArrayArgumentValidator.DEFAULT_INSTANCE, + new ArgumentTypesValidator( null, ANY, INTEGER ) + ), + ElementViaArrayArgumentReturnTypeResolver.DEFAULT_INSTANCE, + StandardFunctionArgumentTypeResolvers.invariant( ANY, INTEGER ) + ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/AbstractArrayReverseFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/AbstractArrayReverseFunction.java new file mode 100644 index 000000000000..7e759ed59fb8 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/AbstractArrayReverseFunction.java @@ -0,0 +1,32 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.array; + +import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor; +import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators; +import org.hibernate.query.sqm.produce.function.StandardFunctionArgumentTypeResolvers; + +import static org.hibernate.query.sqm.produce.function.FunctionParameterType.ANY; + +/** + * Encapsulates the validator, return type and argument type resolvers for the array_reverse functions. + * Subclasses only have to implement the rendering. + */ +public abstract class AbstractArrayReverseFunction extends AbstractSqmSelfRenderingFunctionDescriptor { + + public AbstractArrayReverseFunction() { + super( + "array_reverse", + StandardArgumentsValidators.composite( + StandardArgumentsValidators.exactly( 1 ), + ArrayArgumentValidator.DEFAULT_INSTANCE + ), + ArrayViaArgumentReturnTypeResolver.DEFAULT_INSTANCE, + StandardFunctionArgumentTypeResolvers.composite( + StandardFunctionArgumentTypeResolvers.invariant( ANY ) + ) + ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/AbstractArraySortFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/AbstractArraySortFunction.java new file mode 100644 index 000000000000..ca45dad8288b --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/AbstractArraySortFunction.java @@ -0,0 +1,49 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.array; + +import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor; +import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator; +import org.hibernate.query.sqm.produce.function.FunctionParameterType; +import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators; +import org.hibernate.query.sqm.produce.function.StandardFunctionArgumentTypeResolvers; +import org.hibernate.type.spi.TypeConfiguration; + +import static org.hibernate.query.sqm.produce.function.FunctionParameterType.ANY; + +/** + * Encapsulates the validator, return type and argument type resolvers for the array_sort functions. + * Subclasses only have to implement the rendering. + */ +public abstract class AbstractArraySortFunction extends AbstractSqmSelfRenderingFunctionDescriptor { + + public AbstractArraySortFunction(TypeConfiguration typeConfiguration) { + super( + "array_sort", + new ArgumentTypesValidator( + StandardArgumentsValidators.composite( + StandardArgumentsValidators.between( 1, 3 ), + ArrayArgumentValidator.DEFAULT_INSTANCE + ), + FunctionParameterType.ANY, + FunctionParameterType.BOOLEAN, + FunctionParameterType.BOOLEAN + ), + ArrayViaArgumentReturnTypeResolver.DEFAULT_INSTANCE, + StandardFunctionArgumentTypeResolvers.composite( + StandardFunctionArgumentTypeResolvers.invariant( ANY ), + StandardFunctionArgumentTypeResolvers.invariant( + typeConfiguration, + FunctionParameterType.BOOLEAN + ), + StandardFunctionArgumentTypeResolvers.invariant( + typeConfiguration, + FunctionParameterType.BOOLEAN + ) + ) + ); + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayGetBracketFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayGetBracketFunction.java new file mode 100644 index 000000000000..09aaa449183b --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayGetBracketFunction.java @@ -0,0 +1,59 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.array; + +import org.hibernate.metamodel.mapping.JdbcMapping; +import org.hibernate.metamodel.mapping.JdbcMappingContainer; +import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.type.BasicPluralType; + +import java.util.List; + +/** + * Implement the array get function by using {@code []} (bracket) syntax. + */ +public class ArrayGetBracketFunction extends AbstractArrayGetFunction { + + private final boolean supportsJsonBracket; + + public ArrayGetBracketFunction(boolean supportsJsonBracket) { + this.supportsJsonBracket = supportsJsonBracket; + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + final Expression arrayExpression = (Expression) sqlAstArguments.get( 0 ); + final Expression indexExpression = (Expression) sqlAstArguments.get( 1 ); + final JdbcMappingContainer arrayTypeContainer = arrayExpression.getExpressionType(); + final JdbcMapping arrayType = arrayTypeContainer == null ? null : arrayTypeContainer.getSingleJdbcMapping(); + final boolean isJson = arrayType instanceof BasicPluralType && arrayType.getJdbcType().isJson(); + if ( isJson && !supportsJsonBracket ) { + // JSON arrays have 0-based indexed, so we have to adapt the 1-based array_get index + sqlAppender.append( "(select jsonb_path_query(" ); + arrayExpression.accept( walker ); + sqlAppender.append( ",'$[$i]',('{\"i\":'||((" ); + indexExpression.accept( walker ); + sqlAppender.append( ")-1)||'}')::jsonb))" ); + } + else { + arrayExpression.accept( walker ); + sqlAppender.append( '[' ); + indexExpression.accept( walker ); + if ( isJson ) { + // JSON arrays have 0-based indexed, so we have to adapt the 1-based array_get index + sqlAppender.append( "-1" ); + } + sqlAppender.append( ']' ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayGetUnnestFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayGetUnnestFunction.java index 093fc440d558..bb9c00985627 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayGetUnnestFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayGetUnnestFunction.java @@ -7,34 +7,15 @@ import java.util.List; import org.hibernate.metamodel.model.domain.ReturnableType; -import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor; -import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator; -import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators; -import org.hibernate.query.sqm.produce.function.StandardFunctionArgumentTypeResolvers; import org.hibernate.sql.ast.SqlAstTranslator; import org.hibernate.sql.ast.spi.SqlAppender; import org.hibernate.sql.ast.tree.SqlAstNode; import org.hibernate.sql.ast.tree.expression.Expression; -import static org.hibernate.query.sqm.produce.function.FunctionParameterType.ANY; -import static org.hibernate.query.sqm.produce.function.FunctionParameterType.INTEGER; - /** * Implement the array get function by using {@code unnest}. */ -public class ArrayGetUnnestFunction extends AbstractSqmSelfRenderingFunctionDescriptor { - - public ArrayGetUnnestFunction() { - super( - "array_get", - StandardArgumentsValidators.composite( - ArrayArgumentValidator.DEFAULT_INSTANCE, - new ArgumentTypesValidator( null, ANY, INTEGER ) - ), - ElementViaArrayArgumentReturnTypeResolver.DEFAULT_INSTANCE, - StandardFunctionArgumentTypeResolvers.invariant( ANY, INTEGER ) - ); - } +public class ArrayGetUnnestFunction extends AbstractArrayGetFunction { @Override public void render( diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayRemoveIndexUnnestFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayRemoveIndexUnnestFunction.java index 74d88eab2f87..9e3723c3fd55 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayRemoveIndexUnnestFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayRemoveIndexUnnestFunction.java @@ -52,7 +52,7 @@ public void render( final Expression indexExpression = (Expression) sqlAstArguments.get( 1 ); sqlAppender.append( "case when "); arrayExpression.accept( walker ); - sqlAppender.append( " is not null then coalesce((select array_agg(t.val) from unnest(" ); + sqlAppender.append( " is not null then coalesce((select array_agg(t.val order by t.idx) from unnest(" ); arrayExpression.accept( walker ); sqlAppender.append( ") with ordinality t(val,idx) where t.idx is distinct from " ); indexExpression.accept( walker ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArraySliceUnnestFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArraySliceUnnestFunction.java index 91870da7fd21..b7cc8e928bbc 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArraySliceUnnestFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArraySliceUnnestFunction.java @@ -57,7 +57,7 @@ public void render( startIndexExpression.accept( walker ); sqlAppender.append( " is not null and "); endIndexExpression.accept( walker ); - sqlAppender.append( " is not null then coalesce((select array_agg(t.val) from unnest(" ); + sqlAppender.append( " is not null then coalesce((select array_agg(t.val order by t.idx) from unnest(" ); arrayExpression.accept( walker ); sqlAppender.append( ") with ordinality t(val,idx) where t.idx between " ); startIndexExpression.accept( walker ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/DB2UnnestFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/DB2UnnestFunction.java index c8fd2ec6bdcf..e72c764affe5 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/DB2UnnestFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/DB2UnnestFunction.java @@ -43,7 +43,7 @@ public class DB2UnnestFunction extends UnnestFunction { private final int maximumArraySize; public DB2UnnestFunction(int maximumArraySize) { - super( "v", "i" ); + super( "v", "i", true ); this.maximumArraySize = maximumArraySize; } @@ -138,6 +138,9 @@ protected void renderJsonTable( sqlAppender.append( selectableMapping.getSelectionExpression() ); sqlAppender.append( ' ' ); sqlAppender.append( getDdlType( selectableMapping, SqlTypes.JSON_ARRAY, walker ) ); + if ( selectableMapping.getJdbcMapping().getJdbcType().isJson() ) { + sqlAppender.append( " format json" ); + } sqlAppender.appendSql( " path '$." ); sqlAppender.append( selectableMapping.getSelectableName() ); sqlAppender.appendSql( "' error on error" ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/DdlTypeHelper.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/DdlTypeHelper.java index 1e5be6ab186c..17230c3b79b6 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/DdlTypeHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/DdlTypeHelper.java @@ -7,15 +7,13 @@ import java.lang.reflect.Type; import java.util.List; -import org.hibernate.dialect.Dialect; import org.hibernate.engine.jdbc.Size; -import org.hibernate.internal.build.AllowReflection; import org.hibernate.metamodel.mapping.JdbcMappingContainer; import org.hibernate.metamodel.mapping.SqlTypedMapping; import org.hibernate.metamodel.model.domain.DomainType; import org.hibernate.metamodel.model.domain.ReturnableType; -import org.hibernate.sql.ast.spi.AbstractSqlAstTranslator; import org.hibernate.type.BasicType; +import org.hibernate.type.SqlTypes; import org.hibernate.type.descriptor.java.BasicPluralJavaType; import org.hibernate.type.descriptor.sql.DdlType; import org.hibernate.type.descriptor.sql.spi.DdlTypeRegistry; @@ -24,17 +22,15 @@ public class DdlTypeHelper { @SuppressWarnings("unchecked") - @AllowReflection +// @AllowReflection public static BasicType resolveArrayType(DomainType elementType, TypeConfiguration typeConfiguration) { - @SuppressWarnings("unchecked") final var arrayJavaType = (BasicPluralJavaType) typeConfiguration.getJavaTypeRegistry() .resolveArrayDescriptor( elementType.getJavaType() ); - final Dialect dialect = typeConfiguration.getCurrentBaseSqlTypeIndicators().getDialect(); return arrayJavaType.resolveType( typeConfiguration, - dialect, + typeConfiguration.getCurrentBaseSqlTypeIndicators().getDialect(), (BasicType) elementType, null, typeConfiguration.getCurrentBaseSqlTypeIndicators() @@ -43,19 +39,17 @@ public static BasicType resolveArrayType(DomainType elementType, TypeConfi @SuppressWarnings("unchecked") public static BasicType resolveListType(DomainType elementType, TypeConfiguration typeConfiguration) { - @SuppressWarnings("unchecked") - final BasicPluralJavaType arrayJavaType = + final var arrayJavaType = (BasicPluralJavaType) typeConfiguration.getJavaTypeRegistry() - .getDescriptor( List.class ) + .resolveDescriptor( List.class ) .createJavaType( - new ParameterizedTypeImpl( List.class, new Type[]{ elementType.getJavaType() }, null ), + new ParameterizedTypeImpl( List.class, new Type[] { elementType.getJavaType() }, null ), typeConfiguration ); - final Dialect dialect = typeConfiguration.getCurrentBaseSqlTypeIndicators().getDialect(); return arrayJavaType.resolveType( typeConfiguration, - dialect, + typeConfiguration.getCurrentBaseSqlTypeIndicators().getDialect(), (BasicType) elementType, null, typeConfiguration.getCurrentBaseSqlTypeIndicators() @@ -76,14 +70,12 @@ public static String getTypeName(JdbcMappingContainer type, TypeConfiguration ty public static String getTypeName(JdbcMappingContainer type, Size size, TypeConfiguration typeConfiguration) { if ( type instanceof SqlTypedMapping sqlTypedMapping ) { - return AbstractSqlAstTranslator.getSqlTypeName( sqlTypedMapping, typeConfiguration ); + return getSqlTypeName( sqlTypedMapping, typeConfiguration ); } else { - final BasicType basicType = (BasicType) type.getSingleJdbcMapping(); - final DdlTypeRegistry ddlTypeRegistry = typeConfiguration.getDdlTypeRegistry(); - final DdlType ddlType = ddlTypeRegistry.getDescriptor( - basicType.getJdbcType().getDdlTypeCode() - ); + final var basicType = (BasicType) type.getSingleJdbcMapping(); + final var ddlTypeRegistry = typeConfiguration.getDdlTypeRegistry(); + final var ddlType = ddlTypeRegistry.getDescriptor( basicType.getJdbcType().getDdlTypeCode() ); return ddlType.getTypeName( size, basicType, ddlTypeRegistry ); } } @@ -94,14 +86,12 @@ public static String getTypeName(ReturnableType type, TypeConfiguration typeC public static String getTypeName(ReturnableType type, Size size, TypeConfiguration typeConfiguration) { if ( type instanceof SqlTypedMapping sqlTypedMapping ) { - return AbstractSqlAstTranslator.getSqlTypeName( sqlTypedMapping, typeConfiguration ); + return getSqlTypeName( sqlTypedMapping, typeConfiguration ); } else { - final BasicType basicType = (BasicType) ( (JdbcMappingContainer) type ).getSingleJdbcMapping(); - final DdlTypeRegistry ddlTypeRegistry = typeConfiguration.getDdlTypeRegistry(); - final DdlType ddlType = ddlTypeRegistry.getDescriptor( - basicType.getJdbcType().getDdlTypeCode() - ); + final var basicType = (BasicType) ( (JdbcMappingContainer) type ).getSingleJdbcMapping(); + final var ddlTypeRegistry = typeConfiguration.getDdlTypeRegistry(); + final var ddlType = ddlTypeRegistry.getDescriptor( basicType.getJdbcType().getDdlTypeCode() ); return ddlType.getTypeName( size, basicType, ddlTypeRegistry ); } } @@ -120,14 +110,12 @@ public static String getCastTypeName(JdbcMappingContainer type, TypeConfiguratio public static String getCastTypeName(JdbcMappingContainer type, Size size, TypeConfiguration typeConfiguration) { if ( type instanceof SqlTypedMapping sqlTypedMapping ) { - return AbstractSqlAstTranslator.getCastTypeName( sqlTypedMapping, typeConfiguration ); + return getCastTypeName( sqlTypedMapping, typeConfiguration ); } else { - final BasicType basicType = (BasicType) type.getSingleJdbcMapping(); - final DdlTypeRegistry ddlTypeRegistry = typeConfiguration.getDdlTypeRegistry(); - final DdlType ddlType = ddlTypeRegistry.getDescriptor( - basicType.getJdbcType().getDdlTypeCode() - ); + final var basicType = (BasicType) type.getSingleJdbcMapping(); + final var ddlTypeRegistry = typeConfiguration.getDdlTypeRegistry(); + final var ddlType = ddlTypeRegistry.getDescriptor( basicType.getJdbcType().getDdlTypeCode() ); return ddlType.getCastTypeName( size, basicType, ddlTypeRegistry ); } } @@ -138,16 +126,34 @@ public static String getCastTypeName(ReturnableType type, TypeConfiguration t public static String getCastTypeName(ReturnableType type, Size size, TypeConfiguration typeConfiguration) { if ( type instanceof SqlTypedMapping sqlTypedMapping ) { - return AbstractSqlAstTranslator.getCastTypeName( sqlTypedMapping, typeConfiguration ); + return getCastTypeName( sqlTypedMapping, typeConfiguration ); } else { - final BasicType basicType = (BasicType) ( (JdbcMappingContainer) type ).getSingleJdbcMapping(); - final DdlTypeRegistry ddlTypeRegistry = typeConfiguration.getDdlTypeRegistry(); - final DdlType ddlType = ddlTypeRegistry.getDescriptor( - basicType.getJdbcType().getDdlTypeCode() - ); + final var basicType = (BasicType) ( (JdbcMappingContainer) type ).getSingleJdbcMapping(); + final var ddlTypeRegistry = typeConfiguration.getDdlTypeRegistry(); + final var ddlType = ddlTypeRegistry.getDescriptor( basicType.getJdbcType().getDdlTypeCode() ); return ddlType.getCastTypeName( size, basicType, ddlTypeRegistry ); } } + private static DdlType getDdlType(DdlTypeRegistry ddlTypeRegistry, BasicType expressionType) { + var ddlType = ddlTypeRegistry.getDescriptor( expressionType.getJdbcType().getDdlTypeCode() ); + // this may happen when selecting a null value like `SELECT null from ...` + // some dbs need the value to be cast, so not knowing the real type we fall back to INTEGER + return ddlType == null ? ddlTypeRegistry.getDescriptor( SqlTypes.INTEGER ) : ddlType; + } + + private static String getSqlTypeName(SqlTypedMapping castTarget, TypeConfiguration typeConfiguration) { + final var expressionType = (BasicType) castTarget.getJdbcMapping(); + final var ddlTypeRegistry = typeConfiguration.getDdlTypeRegistry(); + return getDdlType( ddlTypeRegistry, expressionType ) + .getTypeName( castTarget.toSize(), expressionType, ddlTypeRegistry ); + } + + public static String getCastTypeName(SqlTypedMapping castTarget, TypeConfiguration typeConfiguration) { + final var expressionType = (BasicType) castTarget.getJdbcMapping(); + final var ddlTypeRegistry = typeConfiguration.getDdlTypeRegistry(); + return getDdlType( ddlTypeRegistry, expressionType ) + .getCastTypeName( castTarget.toSize(), expressionType, ddlTypeRegistry ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/H2ArrayReverseFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/H2ArrayReverseFunction.java new file mode 100644 index 000000000000..0572556a0f3a --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/H2ArrayReverseFunction.java @@ -0,0 +1,47 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.array; + +import java.util.List; + +import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; + +/** + * H2 requires a very special emulation, because {@code unnest} is pretty much useless, + * due to https://github.com/h2database/h2database/issues/1815. + * This emulation uses {@code array_get}, {@code cardinality} and {@code system_range} + * functions to achieve array reversal. + */ +public class H2ArrayReverseFunction extends AbstractArrayReverseFunction { + + private final int maximumArraySize; + + public H2ArrayReverseFunction(int maximumArraySize) { + this.maximumArraySize = maximumArraySize; + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + final Expression arrayExpression = (Expression) sqlAstArguments.get( 0 ); + + sqlAppender.append( "case when " ); + arrayExpression.accept( walker ); + sqlAppender.append( " is not null then coalesce((select array_agg(array_get(" ); + arrayExpression.accept( walker ); + sqlAppender.append( ",i.idx) order by i.idx desc) from system_range(1," ); + sqlAppender.append( Integer.toString( maximumArraySize ) ); + sqlAppender.append( ") i(idx) where i.idx<=coalesce(cardinality(" ); + arrayExpression.accept( walker ); + sqlAppender.append( "),0)),array[]) end" ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/H2ArraySortFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/H2ArraySortFunction.java new file mode 100644 index 000000000000..e462c9a52664 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/H2ArraySortFunction.java @@ -0,0 +1,132 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.array; + +import java.util.List; + +import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.Literal; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * H2 requires a very special emulation, because {@code unnest} is pretty much useless, + * due to https://github.com/h2database/h2database/issues/1815. + * This emulation uses {@code array_get}, {@code cardinality} and {@code system_range} + * functions to achieve array sorting. + */ +public class H2ArraySortFunction extends AbstractArraySortFunction { + + private final int maximumArraySize; + + public H2ArraySortFunction(int maximumArraySize, TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + this.maximumArraySize = maximumArraySize; + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + + final boolean areArgumentsLiterals = + ( sqlAstArguments.size() <= 1 || sqlAstArguments.get( 1 ) instanceof Literal ) && + ( sqlAstArguments.size() <= 2 || sqlAstArguments.get( 2 ) instanceof Literal ); + + if ( areArgumentsLiterals ) { + renderWithLiteralArguments( sqlAppender, sqlAstArguments, walker ); + } + else { + renderWithExpressionArguments( sqlAppender, sqlAstArguments, walker ); + } + } + + private void renderWithLiteralArguments( + SqlAppender sqlAppender, + List sqlAstArguments, + SqlAstTranslator walker) { + + final Expression arrayExpression = (Expression) sqlAstArguments.get( 0 ); + + final boolean descending = sqlAstArguments.size() > 1 + && sqlAstArguments.get( 1 ) instanceof Literal literal + && literal.getLiteralValue() instanceof Boolean boolValue + ? boolValue : false; + + final Boolean nullsFirst = sqlAstArguments.size() > 2 + && sqlAstArguments.get( 2 ) instanceof Literal literal + && literal.getLiteralValue() instanceof Boolean boolValue + ? boolValue : null; + + final boolean actualNullsFirst = nullsFirst != null ? nullsFirst : descending; + + sqlAppender.append( "case when " ); + arrayExpression.accept( walker ); + sqlAppender.append( " is not null then coalesce((select array_agg(array_get(" ); + arrayExpression.accept( walker ); + sqlAppender.append( ",i.idx) order by array_get(" ); + arrayExpression.accept( walker ); + sqlAppender.append( ",i.idx)" ); + + sqlAppender.append( descending + ? ( actualNullsFirst ? " desc nulls first" : " desc nulls last" ) + : ( actualNullsFirst ? " asc nulls first" : " asc nulls last" ) ); + + sqlAppender.append( ") from system_range(1," ); + sqlAppender.append( Integer.toString( maximumArraySize ) ); + sqlAppender.append( ") i(idx) where i.idx<=coalesce(cardinality(" ); + arrayExpression.accept( walker ); + sqlAppender.append( "),0)),array[]) end" ); + } + + private void renderWithExpressionArguments( + SqlAppender sqlAppender, + List sqlAstArguments, + SqlAstTranslator walker) { + + assert sqlAstArguments.size() >= 2; + + final Expression arrayExpression = (Expression) sqlAstArguments.get( 0 ); + final SqlAstNode descendingNode = sqlAstArguments.get( 1 ); + final SqlAstNode nullsFirstNode = sqlAstArguments.size() > 2 + ? sqlAstArguments.get( 2 ) + : descendingNode; + + sqlAppender.append( "case when " ); + arrayExpression.accept( walker ); + sqlAppender.append( " is not null then coalesce((select array_agg(array_get(" ); + arrayExpression.accept( walker ); + sqlAppender.append( ",i.idx) order by " ); + + sqlAppender.append( '(' ); + nullsFirstNode.accept( walker ); + sqlAppender.append( "=(array_get(" ); + arrayExpression.accept( walker ); + sqlAppender.append( ",i.idx) is null)) desc," ); + + sqlAppender.append( "case when " ); + descendingNode.accept( walker ); + sqlAppender.append( " then array_get(" ); + arrayExpression.accept( walker ); + sqlAppender.append( ",i.idx) end desc," ); + + sqlAppender.append( "case when not " ); + descendingNode.accept( walker ); + sqlAppender.append( " then array_get(" ); + arrayExpression.accept( walker ); + sqlAppender.append( ",i.idx) end" ); + + sqlAppender.append( ") from system_range(1," ); + sqlAppender.append( Integer.toString( maximumArraySize ) ); + sqlAppender.append( ") i(idx) where i.idx<=coalesce(cardinality(" ); + arrayExpression.accept( walker ); + sqlAppender.append( "),0)),array[]) end" ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/H2UnnestFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/H2UnnestFunction.java index 3c0dea0ee1a9..5982815fd868 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/H2UnnestFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/H2UnnestFunction.java @@ -6,9 +6,10 @@ import java.util.List; +import org.hibernate.dialect.aggregate.AggregateSupport; import org.hibernate.dialect.function.UnnestSetReturningFunctionTypeResolver; +import org.hibernate.engine.jdbc.Size; import org.hibernate.engine.spi.SessionFactoryImplementor; -import org.hibernate.internal.util.NullnessUtil; import org.hibernate.metamodel.mapping.CollectionPart; import org.hibernate.metamodel.mapping.EmbeddableMappingType; import org.hibernate.metamodel.mapping.JdbcMappingContainer; @@ -16,6 +17,7 @@ import org.hibernate.metamodel.mapping.SelectablePath; import org.hibernate.metamodel.mapping.SqlTypedMapping; import org.hibernate.metamodel.mapping.internal.SelectableMappingImpl; +import org.hibernate.metamodel.mapping.internal.SqlTypedMappingImpl; import org.hibernate.query.sqm.tuple.internal.AnonymousTupleTableGroupProducer; import org.hibernate.query.spi.QueryEngine; import org.hibernate.query.sqm.ComparisonOperator; @@ -37,9 +39,12 @@ import org.hibernate.sql.ast.tree.predicate.ComparisonPredicate; import org.hibernate.type.BasicPluralType; import org.hibernate.type.BasicType; +import org.hibernate.type.SqlTypes; import org.hibernate.type.descriptor.jdbc.AggregateJdbcType; import org.checkerframework.checker.nullness.qual.Nullable; +import org.hibernate.type.descriptor.sql.DdlType; +import org.hibernate.type.spi.TypeConfiguration; /** * H2 unnest function. @@ -55,7 +60,7 @@ public class H2UnnestFunction extends UnnestFunction { private final int maximumArraySize; public H2UnnestFunction(int maximumArraySize) { - super( new H2UnnestSetReturningFunctionTypeResolver() ); + super( new H2UnnestSetReturningFunctionTypeResolver(), false ); this.maximumArraySize = maximumArraySize; } @@ -232,7 +237,6 @@ public SelectableMapping[] resolveFunctionReturnType( // For column references we render an emulation through system_range(), // so we need to render an array access to get to the element final String elementReadExpression = "array_get(" + arrayColumnReference.getExpressionText() + "," + Template.TEMPLATE + ".x)"; - final String arrayReadExpression = NullnessUtil.castNonNull( arrayColumnReference.getReadExpression() ); final EmbeddableMappingType embeddableMappingType = aggregateJdbcType.getEmbeddableMappingType(); final int jdbcValueCount = embeddableMappingType.getJdbcValueCount(); returnType = new SelectableMapping[jdbcValueCount + (indexMapping == null ? 0 : 1)]; @@ -240,7 +244,7 @@ public SelectableMapping[] resolveFunctionReturnType( final SelectableMapping selectableMapping = embeddableMappingType.getJdbcValueSelectable( i ); // The array expression has to be replaced with the actual array_get read expression in this emulation final String customReadExpression = selectableMapping.getCustomReadExpression() - .replace( arrayReadExpression, elementReadExpression ); + .replace( Template.TEMPLATE, elementReadExpression ); returnType[i] = new SelectableMappingImpl( selectableMapping.getContainingTableExpression(), selectableMapping.getSelectablePath().getSelectableName(), @@ -280,15 +284,53 @@ public SelectableMapping[] resolveFunctionReturnType( elementSelectionExpression = defaultBasicArrayColumnName; elementReadExpression = null; } + final TypeConfiguration typeConfiguration = converter.getCreationContext().getTypeConfiguration(); + final DdlType ddlType = typeConfiguration.getDdlTypeRegistry() + .getDescriptor( elementType.getJdbcType().getDefaultSqlTypeCode() ); + final AggregateSupport aggregateSupport = + converter.getCreationContext().getDialect().getAggregateSupport(); final SelectableMapping elementMapping; + final int pluralSqlTypeCode = pluralType.getJdbcType().getDefaultSqlTypeCode(); if ( expressionType instanceof SqlTypedMapping typedMapping ) { + final String columnTypeName = ddlType.getTypeName( + new Size( + typedMapping.getPrecision(), + typedMapping.getScale(), + typedMapping.getLength() + ), + elementType, + typeConfiguration.getDdlTypeRegistry() + ); + final String readExpression; + if ( pluralSqlTypeCode == SqlTypes.JSON_ARRAY || pluralSqlTypeCode == SqlTypes.XML_ARRAY ) { + readExpression = aggregateSupport.aggregateComponentCustomReadExpression( + "", + "", + "", + elementReadExpression, + pluralSqlTypeCode, + new SqlTypedMappingImpl( + columnTypeName, + typedMapping.getLength(), + null, + typedMapping.getPrecision(), + typedMapping.getScale(), + typedMapping.getTemporalPrecision(), + elementType + ), + typeConfiguration + ); + } + else { + readExpression = elementReadExpression; + } elementMapping = new SelectableMappingImpl( "", elementSelectionExpression, new SelectablePath( CollectionPart.Nature.ELEMENT.getName() ), - elementReadExpression, + readExpression, null, - typedMapping.getColumnDefinition(), + columnTypeName, typedMapping.getLength(), typedMapping.getArrayLength(), typedMapping.getPrecision(), @@ -304,11 +346,39 @@ public SelectableMapping[] resolveFunctionReturnType( ); } else { + final String columnTypeName = ddlType.getTypeName( + Size.nil(), + elementType, + typeConfiguration.getDdlTypeRegistry() + ); + final String readExpression; + if ( pluralSqlTypeCode == SqlTypes.JSON_ARRAY || pluralSqlTypeCode == SqlTypes.XML_ARRAY ) { + readExpression = aggregateSupport.aggregateComponentCustomReadExpression( + "", + "", + "", + elementReadExpression, + pluralSqlTypeCode, + new SqlTypedMappingImpl( + columnTypeName, + null, + null, + null, + null, + null, + elementType + ), + typeConfiguration + ); + } + else { + readExpression = elementReadExpression; + } elementMapping = new SelectableMappingImpl( "", elementSelectionExpression, new SelectablePath( CollectionPart.Nature.ELEMENT.getName() ), - elementReadExpression, + readExpression, null, null, null, diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/HANAUnnestFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/HANAUnnestFunction.java index 67c7fafe8383..7de0a610e517 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/HANAUnnestFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/HANAUnnestFunction.java @@ -67,7 +67,7 @@ public class HANAUnnestFunction extends UnnestFunction { public HANAUnnestFunction() { - super( "v", "i" ); + super( "v", "i", true ); } @Override @@ -233,7 +233,7 @@ private void addIdColumns(EmbeddableValuedModelPart embeddableModelPart, List idColumns) { - final DdlTypeRegistry ddlTypeRegistry = pluralAttributeMapping.getCollectionDescriptor() + final var ddlTypeRegistry = pluralAttributeMapping.getCollectionDescriptor() .getFactory() .getTypeConfiguration() .getDdlTypeRegistry(); @@ -241,7 +241,7 @@ private void addIdColumns(PluralAttributeMapping pluralAttributeMapping, List idColumns) { - final DdlTypeRegistry ddlTypeRegistry = entityMappingType.getEntityPersister() + final var ddlTypeRegistry = entityMappingType.getEntityPersister() .getFactory() .getTypeConfiguration() .getDdlTypeRegistry(); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/HSQLArrayRemoveFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/HSQLArrayRemoveFunction.java index bb7a91f23d17..6526e5fda3e9 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/HSQLArrayRemoveFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/HSQLArrayRemoveFunction.java @@ -30,7 +30,7 @@ public void render( final Expression elementExpression = (Expression) sqlAstArguments.get( 1 ); sqlAppender.append( "case when "); arrayExpression.accept( walker ); - sqlAppender.append( " is not null then coalesce((select array_agg(t.val) from unnest(" ); + sqlAppender.append( " is not null then coalesce((select array_agg(t.val order by t.idx) from unnest(" ); arrayExpression.accept( walker ); sqlAppender.append( ") with ordinality t(val,idx) where t.val is distinct from " ); elementExpression.accept( walker ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/HSQLArraySortFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/HSQLArraySortFunction.java new file mode 100644 index 000000000000..f344c0e5326f --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/HSQLArraySortFunction.java @@ -0,0 +1,111 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.array; + +import java.util.List; + +import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.Literal; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * HSQLDB sort_array function. + */ +public class HSQLArraySortFunction extends AbstractArraySortFunction { + + public HSQLArraySortFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + + final boolean areArgumentsLiterals = + ( sqlAstArguments.size() <= 1 || sqlAstArguments.get( 1 ) instanceof Literal ) && + ( sqlAstArguments.size() <= 2 || sqlAstArguments.get( 2 ) instanceof Literal ); + + if ( areArgumentsLiterals ) { + renderWithLiteralArguments( sqlAppender, sqlAstArguments, walker ); + } + else { + renderWithExpressionArguments( sqlAppender, sqlAstArguments, walker ); + } + } + + private void renderWithLiteralArguments( + SqlAppender sqlAppender, + List sqlAstArguments, + SqlAstTranslator walker) { + + final Expression arrayExpression = (Expression) sqlAstArguments.get( 0 ); + + final boolean descending = sqlAstArguments.size() > 1 + && sqlAstArguments.get( 1 ) instanceof Literal literal + && literal.getLiteralValue() instanceof Boolean boolValue + ? boolValue : false; + + final Boolean nullsFirst = sqlAstArguments.size() > 2 + && sqlAstArguments.get( 2 ) instanceof Literal literal + && literal.getLiteralValue() instanceof Boolean boolValue + ? boolValue : null; + + final boolean actualNullsFirst = nullsFirst != null ? nullsFirst : descending; + + sqlAppender.append( "sort_array(" ); + arrayExpression.accept( walker ); + + sqlAppender.append( descending + ? ( actualNullsFirst ? " desc nulls first" : " desc nulls last" ) + : ( actualNullsFirst ? " asc nulls first" : " asc nulls last" ) ); + + sqlAppender.append( ')' ); + } + + private void renderWithExpressionArguments( + SqlAppender sqlAppender, + List sqlAstArguments, + SqlAstTranslator walker) { + + assert sqlAstArguments.size() >= 2; + + final SqlAstNode arrayExpression = sqlAstArguments.get( 0 ); + final SqlAstNode descendingNode = sqlAstArguments.get( 1 ); + final SqlAstNode nullsFirstNode = sqlAstArguments.size() > 2 + ? sqlAstArguments.get( 2 ) + : descendingNode; + + sqlAppender.append( "case when " ); + arrayExpression.accept( walker ); + sqlAppender.append( " is not null then " ); + + sqlAppender.append( "coalesce((select array_agg(t.val order by " ); + + sqlAppender.append( '(' ); + nullsFirstNode.accept( walker ); + sqlAppender.append( "=(t.val is null)) desc," ); + + sqlAppender.append( "case when " ); + descendingNode.accept( walker ); + sqlAppender.append( " then t.val end desc," ); + + sqlAppender.append( "case when not " ); + descendingNode.accept( walker ); + sqlAppender.append( " then t.val end" ); + + sqlAppender.append( ") from unnest(" ); + arrayExpression.accept( walker ); + sqlAppender.append( ") t(val))" ); + + sqlAppender.append( ",array[]) end" ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/JsonArrayViaElementArgumentReturnTypeResolver.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/JsonArrayViaElementArgumentReturnTypeResolver.java index ce7ed772650b..4ddb37bf909e 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/JsonArrayViaElementArgumentReturnTypeResolver.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/JsonArrayViaElementArgumentReturnTypeResolver.java @@ -7,9 +7,7 @@ import java.util.List; import java.util.function.Supplier; -import org.hibernate.internal.build.AllowReflection; import org.hibernate.metamodel.mapping.BasicValuedMapping; -import org.hibernate.metamodel.mapping.MappingModelExpressible; import org.hibernate.metamodel.model.domain.DomainType; import org.hibernate.metamodel.model.domain.ReturnableType; import org.hibernate.query.sqm.produce.function.FunctionReturnTypeResolver; @@ -20,7 +18,6 @@ import org.hibernate.type.SqlTypes; import org.hibernate.type.descriptor.java.BasicPluralJavaType; import org.hibernate.type.descriptor.jdbc.DelegatingJdbcTypeIndicators; -import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators; import org.hibernate.type.spi.TypeConfiguration; import org.checkerframework.checker.nullness.qual.Nullable; @@ -44,12 +41,12 @@ public ReturnableType resolveFunctionReturnType( TypeConfiguration typeConfiguration) { if ( converter != null ) { if ( converter.isInTypeInference() ) { - // Don't default to a Json array when in type inference mode. + // Don't default to a JSON array when in type inference mode. // Comparing e.g. `array() = (select array_agg() ...)` will trigger this resolver // while inferring the type for `array()`, which we want to avoid. return null; } - final MappingModelExpressible inferredType = converter.resolveFunctionImpliedReturnType(); + final var inferredType = converter.resolveFunctionImpliedReturnType(); if ( inferredType != null ) { if ( inferredType instanceof ReturnableType returnableType ) { return returnableType; @@ -62,8 +59,8 @@ else if ( inferredType instanceof BasicValuedMapping basicValuedMapping ) { if ( impliedType != null ) { return impliedType; } - for ( SqmTypedNode argument : arguments ) { - final DomainType sqmType = argument.getExpressible().getSqmType(); + for ( var argument : arguments ) { + final var sqmType = argument.getExpressible().getSqmType(); if ( sqmType instanceof ReturnableType ) { return resolveJsonArrayType( sqmType, typeConfiguration ); } @@ -78,14 +75,14 @@ public BasicValuedMapping resolveFunctionReturnType( return null; } - @AllowReflection +// @AllowReflection public static BasicType resolveJsonArrayType(DomainType elementType, TypeConfiguration typeConfiguration) { @SuppressWarnings("unchecked") final var arrayJavaType = (BasicPluralJavaType) typeConfiguration.getJavaTypeRegistry() .resolveArrayDescriptor( elementType.getJavaType() ); - final JdbcTypeIndicators currentBaseSqlTypeIndicators = typeConfiguration.getCurrentBaseSqlTypeIndicators(); + final var currentBaseSqlTypeIndicators = typeConfiguration.getCurrentBaseSqlTypeIndicators(); return arrayJavaType.resolveType( typeConfiguration, currentBaseSqlTypeIndicators.getDialect(), diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/OracleArrayGetFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/OracleArrayGetFunction.java index 9044a16492f2..bd8607e77ca8 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/OracleArrayGetFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/OracleArrayGetFunction.java @@ -15,10 +15,7 @@ /** * Oracle array_get function. */ -public class OracleArrayGetFunction extends ArrayGetUnnestFunction { - - public OracleArrayGetFunction() { - } +public class OracleArrayGetFunction extends AbstractArrayGetFunction { @Override public void render( diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/OracleArrayReverseFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/OracleArrayReverseFunction.java new file mode 100644 index 000000000000..c00fa75aea00 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/OracleArrayReverseFunction.java @@ -0,0 +1,36 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.array; + +import java.util.List; + +import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; + +/** + * Oracle array_reverse function. + */ +public class OracleArrayReverseFunction extends AbstractArrayReverseFunction { + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + final Expression arrayExpression = (Expression) sqlAstArguments.get( 0 ); + final String arrayTypeName = DdlTypeHelper.getTypeName( + arrayExpression.getExpressionType(), + walker.getSessionFactory().getTypeConfiguration() + ); + sqlAppender.append( arrayTypeName ); + sqlAppender.append( "_reverse(" ); + arrayExpression.accept( walker ); + sqlAppender.append( ')' ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/OracleArraySortFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/OracleArraySortFunction.java new file mode 100644 index 000000000000..e6f9bf7aba99 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/OracleArraySortFunction.java @@ -0,0 +1,75 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.array; + +import java.util.List; + +import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.type.spi.TypeConfiguration; + +import static org.hibernate.internal.util.NullnessUtil.castNonNull; + +/** + * Oracle array_sort function. + */ +public class OracleArraySortFunction extends AbstractArraySortFunction { + + public OracleArraySortFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + final Expression arrayExpression = (Expression) sqlAstArguments.get( 0 ); + final String arrayTypeName = DdlTypeHelper.getTypeName( + arrayExpression.getExpressionType(), + walker.getSessionFactory().getTypeConfiguration() + ); + + sqlAppender.append( arrayTypeName ); + sqlAppender.append( "_sort(" ); + arrayExpression.accept( walker ); + + if ( sqlAstArguments.size() > 1 ) { + sqlAppender.append( ',' ); + final Expression descNode = (Expression) sqlAstArguments.get( 1 ); + sqlAppender.append( "case when " ); + descNode.accept( walker ); + sqlAppender.append( '=' ); + var sessionFactory = walker.getSessionFactory(); + castNonNull( descNode.getExpressionType() ).getSingleJdbcMapping().getJdbcLiteralFormatter() + .appendJdbcLiteral( + sqlAppender, + true, + sessionFactory.getJdbcServices().getDialect(), + sessionFactory.getWrapperOptions() + ); + sqlAppender.append( " then 1 else 0 end" ); + if ( sqlAstArguments.size() > 2 ) { + sqlAppender.append( ",case when " ); + final Expression nullsNode = (Expression) sqlAstArguments.get( 2 ); + nullsNode.accept( walker ); + sqlAppender.append( '=' ); + castNonNull( nullsNode.getExpressionType() ).getSingleJdbcMapping().getJdbcLiteralFormatter() + .appendJdbcLiteral( + sqlAppender, + true, + sessionFactory.getJdbcServices().getDialect(), + sessionFactory.getWrapperOptions() + ); + sqlAppender.append( " then 1 else 0 end" ); + } + } + sqlAppender.append( ')' ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/OracleUnnestFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/OracleUnnestFunction.java index 9fcb44dd9bd5..c2c26f8f5820 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/OracleUnnestFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/OracleUnnestFunction.java @@ -20,8 +20,13 @@ */ public class OracleUnnestFunction extends UnnestFunction { + @Deprecated(forRemoval = true) public OracleUnnestFunction() { - super( "column_value", "i" ); + this( false ); + } + + public OracleUnnestFunction(boolean supportsJsonType) { + super( "column_value", "i", !supportsJsonType ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/PostgreSQLArrayReverseEmulation.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/PostgreSQLArrayReverseEmulation.java new file mode 100644 index 000000000000..5c0e4adf7581 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/PostgreSQLArrayReverseEmulation.java @@ -0,0 +1,60 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.array; + +import java.util.List; + +import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.type.BasicPluralType; +import org.hibernate.type.BasicType; + +/** + * PostgreSQL array_reverse emulation for versions before 18. + * HSQLDB uses the same approach. + */ +public class PostgreSQLArrayReverseEmulation extends AbstractArrayReverseFunction { + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + final SqlAstNode arrayExpression = sqlAstArguments.get( 0 ); + + sqlAppender.append( "case when " ); + arrayExpression.accept( walker ); + sqlAppender.append( " is not null then " ); + sqlAppender.append( "coalesce((select array_agg(t.val order by t.idx desc) from unnest(" ); + arrayExpression.accept( walker ); + sqlAppender.append( ") with ordinality t(val,idx)" ); + + String arrayTypeName = null; + if ( returnType instanceof BasicPluralType pluralType ) { + if ( needsArrayCasting( pluralType.getElementType() ) ) { + arrayTypeName = DdlTypeHelper.getCastTypeName( + returnType, + walker.getSessionFactory().getTypeConfiguration() + ); + } + } + if ( arrayTypeName != null ) { + sqlAppender.append( "),cast(array[] as " ); + sqlAppender.appendSql( arrayTypeName ); + sqlAppender.appendSql( "))" ); + } + else { + sqlAppender.append( "),array[])" ); + } + sqlAppender.append( " end" ); + } + + private static boolean needsArrayCasting(BasicType elementType) { + return elementType.getJdbcType().isString(); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/PostgreSQLArraySortEmulation.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/PostgreSQLArraySortEmulation.java new file mode 100644 index 000000000000..b7cfa927563e --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/PostgreSQLArraySortEmulation.java @@ -0,0 +1,154 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.array; + +import java.util.List; + +import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Literal; +import org.hibernate.type.BasicPluralType; +import org.hibernate.type.BasicType; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * PostgreSQL array_sort emulation for versions before 18. + */ +public class PostgreSQLArraySortEmulation extends AbstractArraySortFunction { + + public PostgreSQLArraySortEmulation(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + + final boolean areArgumentsLiterals = + ( sqlAstArguments.size() <= 1 || sqlAstArguments.get( 1 ) instanceof Literal ) && + ( sqlAstArguments.size() <= 2 || sqlAstArguments.get( 2 ) instanceof Literal ); + + if ( areArgumentsLiterals ) { + renderWithLiteralArguments( sqlAppender, sqlAstArguments, returnType, walker ); + } + else { + renderWithExpressionArguments( sqlAppender, sqlAstArguments, returnType, walker ); + } + } + + private void renderWithLiteralArguments( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + + final SqlAstNode arrayExpression = sqlAstArguments.get( 0 ); + + final boolean descending = sqlAstArguments.size() > 1 + && sqlAstArguments.get( 1 ) instanceof Literal literal + && literal.getLiteralValue() instanceof Boolean + ? (Boolean) literal.getLiteralValue() + : false; + + final Boolean nullsFirst = sqlAstArguments.size() > 2 + && sqlAstArguments.get( 2 ) instanceof Literal literal + && literal.getLiteralValue() instanceof Boolean + ? (Boolean) literal.getLiteralValue() + : null; + + final boolean actualNullsFirst = nullsFirst != null ? nullsFirst : descending; + + sqlAppender.append( "case when " ); + arrayExpression.accept( walker ); + sqlAppender.append( " is not null then " ); + + sqlAppender.append( "coalesce((select array_agg(t.val order by t.val" ); + + sqlAppender.append( descending + ? ( actualNullsFirst ? " desc nulls first" : " desc nulls last" ) + : ( actualNullsFirst ? " asc nulls first" : " asc nulls last" ) ); + + sqlAppender.append( ") from unnest(" ); + arrayExpression.accept( walker ); + sqlAppender.append( ") t(val))" ); + + appendEmptyArrayWithCasting( sqlAppender, returnType, walker ); + sqlAppender.append( " end" ); + } + + private void renderWithExpressionArguments( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + + assert sqlAstArguments.size() >= 2; + + final SqlAstNode arrayExpression = sqlAstArguments.get( 0 ); + final SqlAstNode descendingNode = sqlAstArguments.get( 1 ); + final SqlAstNode nullsFirstNode = sqlAstArguments.size() > 2 + ? sqlAstArguments.get( 2 ) + : descendingNode; + + sqlAppender.append( "case when " ); + arrayExpression.accept( walker ); + sqlAppender.append( " is not null then " ); + + sqlAppender.append( "coalesce((select array_agg(t.val order by " ); + + sqlAppender.append( '(' ); + nullsFirstNode.accept( walker ); + sqlAppender.append( "=(t.val is null)) desc," ); + + sqlAppender.append( "case when " ); + descendingNode.accept( walker ); + sqlAppender.append( " then t.val end desc," ); + + sqlAppender.append( "case when not " ); + descendingNode.accept( walker ); + sqlAppender.append( " then t.val end" ); + + sqlAppender.append( ") from unnest(" ); + arrayExpression.accept( walker ); + sqlAppender.append( ") t(val))" ); + + appendEmptyArrayWithCasting( sqlAppender, returnType, walker ); + sqlAppender.append( " end" ); + } + + private void appendEmptyArrayWithCasting( + SqlAppender sqlAppender, + ReturnableType returnType, + SqlAstTranslator walker) { + + String arrayTypeName = null; + if ( returnType instanceof BasicPluralType pluralType ) { + if ( needsArrayCasting( pluralType.getElementType() ) ) { + arrayTypeName = DdlTypeHelper.getCastTypeName( + returnType, + walker.getSessionFactory().getTypeConfiguration() + ); + } + } + + if ( arrayTypeName != null ) { + sqlAppender.append( ",cast(array[] as " ); + sqlAppender.appendSql( arrayTypeName ); + sqlAppender.appendSql( "))" ); + } + else { + sqlAppender.append( ",array[])" ); + } + } + + private static boolean needsArrayCasting(BasicType elementType) { + return elementType.getJdbcType().isString(); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/PostgreSQLUnnestFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/PostgreSQLUnnestFunction.java index de9712049da9..899cfb331e53 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/PostgreSQLUnnestFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/PostgreSQLUnnestFunction.java @@ -5,6 +5,7 @@ package org.hibernate.dialect.function.array; import org.checkerframework.checker.nullness.qual.Nullable; +import org.hibernate.metamodel.mapping.ModelPart; import org.hibernate.type.descriptor.jdbc.XmlHelper; import org.hibernate.dialect.aggregate.AggregateSupport; import org.hibernate.metamodel.mapping.CollectionPart; @@ -26,7 +27,7 @@ public class PostgreSQLUnnestFunction extends UnnestFunction { private final boolean supportsJsonTable; public PostgreSQLUnnestFunction(boolean supportsJsonTable) { - super( null, "ordinality" ); + super( null, "ordinality", false ); this.supportsJsonTable = supportsJsonTable; } @@ -62,7 +63,10 @@ protected void renderJsonTable( sqlAppender.append( ',' ); } if ( CollectionPart.Nature.INDEX.getName().equals( selectableMapping.getSelectableName() ) ) { - sqlAppender.appendSql( "t.i" ); + sqlAppender.append( "t.i" ); + } + else if ( CollectionPart.Nature.ELEMENT.getName().equals( selectableMapping.getSelectableName() ) ) { + sqlAppender.append( "t.v" ); } else { sqlAppender.append( aggregateSupport.aggregateComponentCustomReadExpression( @@ -78,7 +82,13 @@ protected void renderJsonTable( sqlAppender.append( " as " ); sqlAppender.append( selectableMapping.getSelectionExpression() ); } ); - sqlAppender.appendSql( " from jsonb_array_elements(" ); + final ModelPart elementPart = tupleType.findSubPart( CollectionPart.Nature.ELEMENT.getName(), null ); + if ( elementPart != null && elementPart.getSingleJdbcMapping().getJdbcType().isStringLike() ) { + sqlAppender.appendSql( " from jsonb_array_elements_text(" ); + } + else { + sqlAppender.appendSql( " from jsonb_array_elements(" ); + } array.accept( walker ); sqlAppender.appendSql( ')' ); if ( tupleType.findSubPart( CollectionPart.Nature.INDEX.getName(), null ) != null ) { diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/SQLServerUnnestFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/SQLServerUnnestFunction.java index eed68fb15aff..a9d2bd2e8ec1 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/SQLServerUnnestFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/SQLServerUnnestFunction.java @@ -24,7 +24,7 @@ public class SQLServerUnnestFunction extends UnnestFunction { public SQLServerUnnestFunction() { - super( "v", "i" ); + super( "v", "i", false ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/SpannerArrayConcatElementFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/SpannerArrayConcatElementFunction.java new file mode 100644 index 000000000000..760546ef0601 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/SpannerArrayConcatElementFunction.java @@ -0,0 +1,74 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.array; + +import java.util.List; + +import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.sql.ast.SqlAstNodeRenderingMode; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.Literal; +import org.hibernate.type.BasicPluralType; + +/** + * Uses typed array literals for {@code null} arrays like {@code ARRAY[null]}, to resolve ambiguity. + */ +public class SpannerArrayConcatElementFunction extends ArrayConcatElementFunction { + + public SpannerArrayConcatElementFunction(boolean prepend) { + super( "", "||", "", prepend ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + final Expression firstArgument = (Expression) sqlAstArguments.get( 0 ); + final Expression secondArgument = (Expression) sqlAstArguments.get( 1 ); + final Expression elementArgument = prepend ? firstArgument : secondArgument; + + if ( needsTypedLiteral( elementArgument ) ) { + String typeName = null; + if ( returnType instanceof BasicPluralType pluralType ) { + typeName = DdlTypeHelper.getCastTypeName( pluralType.getElementType(), walker.getSessionFactory().getTypeConfiguration() ); + } + + if ( typeName != null && !typeName.isEmpty() ) { + final Expression arrayArgument = prepend ? secondArgument : firstArgument; + if ( prepend ) { + sqlAppender.append( "ARRAY<" ); + sqlAppender.append( typeName ); + sqlAppender.append( ">[" ); + walker.render( elementArgument, SqlAstNodeRenderingMode.DEFAULT ); + sqlAppender.append( "]||" ); + walker.render( arrayArgument, SqlAstNodeRenderingMode.DEFAULT ); + } + else { + walker.render( arrayArgument, SqlAstNodeRenderingMode.DEFAULT ); + sqlAppender.append( "||ARRAY<" ); + sqlAppender.append( typeName ); + sqlAppender.append( ">[" ); + walker.render( elementArgument, SqlAstNodeRenderingMode.DEFAULT ); + sqlAppender.append( "]" ); + } + } + } + else { + super.render( sqlAppender, sqlAstArguments, returnType, walker ); + } + } + + private static boolean needsTypedLiteral(Expression elementExpression) { + if ( elementExpression instanceof Literal literal ) { + return literal.getLiteralValue() == null; + } + return false; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/SpannerPostgreSQLArrayConcatElementFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/SpannerPostgreSQLArrayConcatElementFunction.java new file mode 100644 index 000000000000..33ab2c450b62 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/SpannerPostgreSQLArrayConcatElementFunction.java @@ -0,0 +1,71 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.array; + +import java.util.List; + +import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.sql.ast.SqlAstNodeRenderingMode; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; + +/** + * Spanner PostgreSQL variant of the function to properly return {@code null} when the array argument is null + * without generating casts which Spanner doesn't support. + */ +public class SpannerPostgreSQLArrayConcatElementFunction extends ArrayConcatElementFunction { + + public SpannerPostgreSQLArrayConcatElementFunction(boolean prepend) { + super( "", "||", "", prepend ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + final Expression firstArgument = (Expression) sqlAstArguments.get( 0 ); + final Expression secondArgument = (Expression) sqlAstArguments.get( 1 ); + final Expression arrayArgument; + final Expression elementArgument; + if ( prepend ) { + elementArgument = firstArgument; + arrayArgument = secondArgument; + } + else { + arrayArgument = firstArgument; + elementArgument = secondArgument; + } + + sqlAppender.append( "case when " ); + walker.render( arrayArgument, SqlAstNodeRenderingMode.DEFAULT ); + sqlAppender.append( " is not null then " ); + + if ( prepend ) { + sqlAppender.append( "array[" ); + walker.render( elementArgument, SqlAstNodeRenderingMode.DEFAULT ); + sqlAppender.append( "]" ); + } + else { + walker.render( arrayArgument, SqlAstNodeRenderingMode.DEFAULT ); + } + + sqlAppender.append( "||" ); + + if ( prepend ) { + walker.render( arrayArgument, SqlAstNodeRenderingMode.DEFAULT ); + } + else { + sqlAppender.append( "array[" ); + walker.render( elementArgument, SqlAstNodeRenderingMode.DEFAULT ); + sqlAppender.append( "]" ); + } + + sqlAppender.append( " end" ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/SpannerPostgreSQLArrayRemoveFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/SpannerPostgreSQLArrayRemoveFunction.java new file mode 100644 index 000000000000..958aa129c6c6 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/SpannerPostgreSQLArrayRemoveFunction.java @@ -0,0 +1,66 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.array; + +import java.util.List; + +import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.type.BasicPluralType; +import org.hibernate.type.BasicType; + +/** + * Emulation of PostgreSQL's array_remove function for Spanner PostgreSQL dialect. + */ +public class SpannerPostgreSQLArrayRemoveFunction extends AbstractArrayRemoveFunction { + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + final Expression arrayExpression = (Expression) sqlAstArguments.get( 0 ); + final Expression elementExpression = (Expression) sqlAstArguments.get( 1 ); + + sqlAppender.append( "case when " ); + arrayExpression.accept( walker ); + sqlAppender.append( " is not null then coalesce((select array_agg(t.val order by t.idx) from unnest(" ); + arrayExpression.accept( walker ); + sqlAppender.append( ") with ordinality as t(val, idx) where t.val != " ); + elementExpression.accept( walker ); + sqlAppender.append( " or (t.val is null and " ); + elementExpression.accept( walker ); + sqlAppender.append( " is not null) or (t.val is not null and " ); + elementExpression.accept( walker ); + sqlAppender.append( " is null))" ); + + String arrayTypeName = null; + if ( returnType instanceof BasicPluralType pluralType ) { + if ( needsArrayCasting( pluralType.getElementType() ) ) { + arrayTypeName = org.hibernate.dialect.function.array.DdlTypeHelper.getCastTypeName( + returnType, + walker.getSessionFactory().getTypeConfiguration() + ); + } + } + if ( arrayTypeName != null ) { + sqlAppender.append( ",cast(array[] as " ); + sqlAppender.appendSql( arrayTypeName ); + sqlAppender.appendSql( ")) end" ); + } + else { + sqlAppender.append( ",array[]) end" ); + } + } + + private static boolean needsArrayCasting(BasicType elementType) { + // PostgreSQL doesn't do implicit conversion between text[] and varchar[], so we need casting + return elementType.getJdbcType().isString(); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/SpannerPostgreSQLArrayRemoveIndexFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/SpannerPostgreSQLArrayRemoveIndexFunction.java new file mode 100644 index 000000000000..9da088feaf99 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/SpannerPostgreSQLArrayRemoveIndexFunction.java @@ -0,0 +1,78 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.array; + +import java.util.List; + +import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor; +import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator; +import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators; +import org.hibernate.query.sqm.produce.function.StandardFunctionArgumentTypeResolvers; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; + +import static org.hibernate.query.sqm.produce.function.FunctionParameterType.ANY; +import static org.hibernate.query.sqm.produce.function.FunctionParameterType.INTEGER; + +/** + * Spanner PostgreSQL specific emulation of array_remove_index. + * + * Spanner does not support the {@code IS DISTINCT FROM} operator, so this + * emulation uses a null-safe inequality check using {@code coalesce}. + */ +public class SpannerPostgreSQLArrayRemoveIndexFunction extends AbstractSqmSelfRenderingFunctionDescriptor { + + private final boolean castEmptyArrayLiteral; + + public SpannerPostgreSQLArrayRemoveIndexFunction(boolean castEmptyArrayLiteral) { + super( + "array_remove_index", + StandardArgumentsValidators.composite( + new ArgumentTypesValidator( null, ANY, INTEGER ), + ArrayArgumentValidator.DEFAULT_INSTANCE + ), + ArrayViaArgumentReturnTypeResolver.DEFAULT_INSTANCE, + StandardFunctionArgumentTypeResolvers.composite( + StandardFunctionArgumentTypeResolvers.invariant( ANY, INTEGER ), + StandardFunctionArgumentTypeResolvers.IMPLIED_RESULT_TYPE + ) + ); + this.castEmptyArrayLiteral = castEmptyArrayLiteral; + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + final Expression arrayExpression = (Expression) sqlAstArguments.get( 0 ); + final Expression indexExpression = (Expression) sqlAstArguments.get( 1 ); + + sqlAppender.append( "case when "); + arrayExpression.accept( walker ); + sqlAppender.append( " is not null then coalesce((select array_agg(t.val order by t.idx) from unnest(" ); + arrayExpression.accept( walker ); + sqlAppender.append( ") with ordinality t(val,idx) where coalesce(t.idx != " ); + indexExpression.accept( walker ); + sqlAppender.append( ", true)),"); + + if ( castEmptyArrayLiteral ) { + sqlAppender.append( "cast(array[] as " ); + sqlAppender.append( DdlTypeHelper.getCastTypeName( + returnType, + walker.getSessionFactory().getTypeConfiguration() + ) ); + sqlAppender.append( ')' ); + } + else { + sqlAppender.append( "array[]" ); + } + sqlAppender.append(") end" ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/SpannerPostgreSQLArrayReplaceFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/SpannerPostgreSQLArrayReplaceFunction.java new file mode 100644 index 000000000000..7ce90f317ff5 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/SpannerPostgreSQLArrayReplaceFunction.java @@ -0,0 +1,81 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.array; + +import java.util.List; + +import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor; +import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.type.BasicPluralType; +import org.hibernate.type.BasicType; + +/** + * Emulation of PostgreSQL's array_replace function for Spanner PostgreSQL dialect. + */ +public class SpannerPostgreSQLArrayReplaceFunction extends AbstractSqmSelfRenderingFunctionDescriptor { + + public SpannerPostgreSQLArrayReplaceFunction() { + super( + "array_replace", + StandardArgumentsValidators.composite( + StandardArgumentsValidators.exactly( 3 ), + new ArrayAndElementArgumentValidator( 0, 1, 2 ) + ), + ArrayViaArgumentReturnTypeResolver.DEFAULT_INSTANCE, + new ArrayAndElementArgumentTypeResolver( 0, 1, 2 ) + ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + final Expression arrayExpression = (Expression) sqlAstArguments.get( 0 ); + final Expression oldExpression = (Expression) sqlAstArguments.get( 1 ); + final Expression newExpression = (Expression) sqlAstArguments.get( 2 ); + + sqlAppender.append( "case when " ); + arrayExpression.accept( walker ); + sqlAppender.append( " is not null then coalesce((select array_agg(case when t.val = " ); + oldExpression.accept( walker ); + sqlAppender.append( " or (t.val is null and " ); + oldExpression.accept( walker ); + sqlAppender.append( " is null) then " ); + newExpression.accept( walker ); + sqlAppender.append( " else t.val end order by t.idx) from unnest(" ); + arrayExpression.accept( walker ); + sqlAppender.append( ") with ordinality as t(val, idx))" ); + + String arrayTypeName = null; + if ( returnType instanceof BasicPluralType pluralType ) { + if ( needsArrayCasting( pluralType.getElementType() ) ) { + arrayTypeName = org.hibernate.dialect.function.array.DdlTypeHelper.getCastTypeName( + returnType, + walker.getSessionFactory().getTypeConfiguration() + ); + } + } + if ( arrayTypeName != null ) { + sqlAppender.append( ",cast(array[] as " ); + sqlAppender.appendSql( arrayTypeName ); + sqlAppender.appendSql( ")) end" ); + } + else { + sqlAppender.append( ",array[]) end" ); + } + } + + private static boolean needsArrayCasting(BasicType elementType) { + // PostgreSQL doesn't do implicit conversion between text[] and varchar[], so we need casting + return elementType.getJdbcType().isString(); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/SpannerPostgreSQLArrayTrimEmulation.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/SpannerPostgreSQLArrayTrimEmulation.java new file mode 100644 index 000000000000..22be08b15c49 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/SpannerPostgreSQLArrayTrimEmulation.java @@ -0,0 +1,59 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.array; + +import java.util.List; + +import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.type.BasicPluralType; +import org.hibernate.type.BasicType; + +/** + * Spanner PostgreSQL emulation for array_trim. + */ +public class SpannerPostgreSQLArrayTrimEmulation extends AbstractArrayTrimFunction { + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + final SqlAstNode arrayExpression = sqlAstArguments.get( 0 ); + final SqlAstNode elementCountExpression = sqlAstArguments.get( 1 ); + sqlAppender.append( "coalesce((select array_agg(t.val order by t.idx) from unnest(" ); + arrayExpression.accept( walker ); + sqlAppender.append( ") with ordinality t(val,idx) where t.idx<=coalesce(array_length(" ); + arrayExpression.accept( walker ); + sqlAppender.append( ", 1), 0)-" ); + elementCountExpression.accept( walker ); + + String arrayTypeName = null; + if ( returnType instanceof BasicPluralType pluralType ) { + if ( needsArrayCasting( pluralType.getElementType() ) ) { + arrayTypeName = org.hibernate.dialect.function.array.DdlTypeHelper.getCastTypeName( + returnType, + walker.getSessionFactory().getTypeConfiguration() + ); + } + } + if ( arrayTypeName != null ) { + sqlAppender.append( "),cast(array[] as " ); + sqlAppender.appendSql( arrayTypeName ); + sqlAppender.appendSql( "))" ); + } + else { + sqlAppender.append( "),array[])" ); + } + } + + private static boolean needsArrayCasting(BasicType elementType) { + // PostgreSQL doesn't do implicit conversion between text[] and varchar[], so we need casting + return elementType.getJdbcType().isString(); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/SybaseASEUnnestFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/SybaseASEUnnestFunction.java index 80db0e89ebf2..88fd509120e9 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/SybaseASEUnnestFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/SybaseASEUnnestFunction.java @@ -23,7 +23,7 @@ public class SybaseASEUnnestFunction extends UnnestFunction { public SybaseASEUnnestFunction() { - super( "v", "i" ); + super( "v", "i", false ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/UnnestFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/UnnestFunction.java index 3c2eedb3e62d..6b9d2136d0eb 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/UnnestFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/UnnestFunction.java @@ -29,17 +29,28 @@ */ public class UnnestFunction extends AbstractSqmSelfRenderingSetReturningFunctionDescriptor { + private final boolean needsFormatJson; + public UnnestFunction(@Nullable String defaultBasicArrayColumnName, String defaultIndexSelectionExpression) { - this( new UnnestSetReturningFunctionTypeResolver( defaultBasicArrayColumnName, defaultIndexSelectionExpression ) ); + this( defaultBasicArrayColumnName, defaultIndexSelectionExpression, false ); + } + + public UnnestFunction(@Nullable String defaultBasicArrayColumnName, String defaultIndexSelectionExpression, boolean needsFormatJson) { + this( new UnnestSetReturningFunctionTypeResolver( defaultBasicArrayColumnName, defaultIndexSelectionExpression ), needsFormatJson ); } protected UnnestFunction(SetReturningFunctionTypeResolver setReturningFunctionTypeResolver) { + this( setReturningFunctionTypeResolver, false ); + } + + protected UnnestFunction(SetReturningFunctionTypeResolver setReturningFunctionTypeResolver, boolean needsFormatJson) { super( "unnest", ArrayArgumentValidator.DEFAULT_INSTANCE, setReturningFunctionTypeResolver, null ); + this.needsFormatJson = needsFormatJson; } @Override @@ -108,6 +119,9 @@ protected void renderJsonTableColumns(SqlAppender sqlAppender, AnonymousTupleTab } else { sqlAppender.append( getDdlType( selectableMapping, SqlTypes.JSON_ARRAY, walker ) ); + if ( needsFormatJson && selectableMapping.getJdbcMapping().getJdbcType().isJson() ) { + sqlAppender.append( " format json" ); + } sqlAppender.appendSql( " path '$." ); sqlAppender.append( selectableMapping.getSelectableName() ); sqlAppender.appendSql( '\'' ); @@ -132,6 +146,9 @@ protected void renderJsonTableColumns(SqlAppender sqlAppender, AnonymousTupleTab else { sqlAppender.append( ' ' ); sqlAppender.append( getDdlType( selectableMapping, SqlTypes.JSON_ARRAY, walker ) ); + if ( needsFormatJson && selectableMapping.getJdbcMapping().getJdbcType().isJson() ) { + sqlAppender.append( " format json" ); + } sqlAppender.appendSql( " path '$'" ); if ( errorOnError ) { sqlAppender.appendSql( " error on error" ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonTableFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonTableFunction.java index 3f4b68655851..1a7fb4426045 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonTableFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonTableFunction.java @@ -726,7 +726,7 @@ protected void addSelectableMappings(List selectableMappings, } private String castValueExpression(String baseReadExpression, CastTarget castTarget, @Nullable Literal defaultExpression, SqmToSqlAstConverter converter) { - final StringBuilder sb = new StringBuilder( baseReadExpression.length() + 200 ); + final var sb = new StringBuilder( baseReadExpression.length() + 200 ); if ( defaultExpression != null ) { sb.append( "coalesce(" ); } @@ -773,7 +773,7 @@ protected void addSelectableMappings(List selectableMappings, } private String castQueryExpression(String baseReadExpression, JsonQueryEmptyBehavior emptyBehavior, JsonQueryWrapMode wrapMode, SqmToSqlAstConverter converter) { - final StringBuilder sb = new StringBuilder( baseReadExpression.length() + 200 ); + final var sb = new StringBuilder( baseReadExpression.length() + 200 ); if ( emptyBehavior == JsonQueryEmptyBehavior.EMPTY_ARRAY || emptyBehavior == JsonQueryEmptyBehavior.EMPTY_OBJECT ) { sb.append( "coalesce(" ); } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonValueFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonValueFunction.java index d5926101f6cc..e28edb2a1f45 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonValueFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonValueFunction.java @@ -161,7 +161,7 @@ static String applyJsonPath(String parentPath, boolean isColumn, boolean isJson, if ( "$".equals( jsonPath ) ) { return parentPath; } - final StringBuilder sb = new StringBuilder( parentPath.length() + jsonPath.length() ); + final var sb = new StringBuilder( parentPath.length() + jsonPath.length() ); final List jsonPathElements = JsonPathHelper.parseJsonPathElements( jsonPath ); final boolean needsWrapping = jsonPathElements.get( 0 ) instanceof JsonPathHelper.JsonAttribute && isColumn diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/HANAJsonTableFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/HANAJsonTableFunction.java index b2f0b1c30a79..4141b0f4a592 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/HANAJsonTableFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/HANAJsonTableFunction.java @@ -346,7 +346,7 @@ private void addIdColumns(EmbeddableValuedModelPart embeddableModelPart, List idColumns) { - final DdlTypeRegistry ddlTypeRegistry = pluralAttributeMapping.getCollectionDescriptor() + final var ddlTypeRegistry = pluralAttributeMapping.getCollectionDescriptor() .getFactory() .getTypeConfiguration() .getDdlTypeRegistry(); @@ -354,7 +354,7 @@ private void addIdColumns(PluralAttributeMapping pluralAttributeMapping, List idColumns) { - final DdlTypeRegistry ddlTypeRegistry = entityMappingType.getEntityPersister() + final var ddlTypeRegistry = entityMappingType.getEntityPersister() .getFactory() .getTypeConfiguration() .getDdlTypeRegistry(); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/HANAJsonValueFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/HANAJsonValueFunction.java index c1508963a2fb..46f3aa263810 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/HANAJsonValueFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/HANAJsonValueFunction.java @@ -15,7 +15,7 @@ import org.hibernate.type.descriptor.jdbc.JdbcLiteralFormatter; import org.hibernate.type.spi.TypeConfiguration; -import static org.hibernate.sql.ast.spi.AbstractSqlAstTranslator.getCastTypeName; +import static org.hibernate.dialect.function.array.DdlTypeHelper.getCastTypeName; /** * HANA json_value function. diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonPathHelper.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonPathHelper.java index 60b44caad8f0..5b21c46fb708 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonPathHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonPathHelper.java @@ -248,7 +248,7 @@ public static String toJsonPath(List pathElements) { } public static String toJsonPath(List pathElements, int start, int end) { - final StringBuilder jsonPath = new StringBuilder(); + final var jsonPath = new StringBuilder(); jsonPath.append( "$" ); for ( int i = start; i < end; i++ ) { final JsonPathElement jsonPathElement = pathElements.get( i ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonTableFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonTableFunction.java index 6ab41692351a..38b9e6c6d6fb 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonTableFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonTableFunction.java @@ -93,7 +93,7 @@ private static class OracleJsonTableSetReturningFunctionTypeResolver extends Jso @Override protected void addSelectableMappings(List selectableMappings, JsonTableQueryColumnDefinition definition, SqmToSqlAstConverter converter) { // - final TypeConfiguration typeConfiguration = converter.getCreationContext().getTypeConfiguration(); + final var typeConfiguration = converter.getCreationContext().getTypeConfiguration(); final JdbcType jsonType = typeConfiguration.getJdbcTypeRegistry().getDescriptor( SqlTypes.JSON ); if ( jsonType.getDdlTypeCode() == SqlTypes.BLOB ) { // Blob is not supported on all DB versions as return type for json_table(), so we have to use clob diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonArrayFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonArrayFunction.java index 8eaa5c72a273..a9695f7480e7 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonArrayFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonArrayFunction.java @@ -36,7 +36,7 @@ public void render( } else { if ( sqlAstArguments.isEmpty() ) { - sqlAppender.appendSql( "'[]'" ); + sqlAppender.appendSql( "N'[]'" ); } else { final SqlAstNode lastArgument = sqlAstArguments.get( sqlAstArguments.size() - 1 ); @@ -51,7 +51,7 @@ public void render( argumentsCount = sqlAstArguments.size(); } if ( nullBehavior == JsonNullBehavior.ABSENT ) { - sqlAppender.appendSql( "(select '['+string_agg(substring(t.d,2,len(t.d)-2),',')" ); + sqlAppender.appendSql( "(select N'['+string_agg(substring(t.d,2,len(t.d)-2),',')" ); sqlAppender.appendSql( "within group (order by t.k)+']' from (values" ); char separator = ' '; for ( int i = 0; i < argumentsCount; i++ ) { diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SpannerJsonQueryFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SpannerJsonQueryFunction.java new file mode 100644 index 000000000000..60008de9e382 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SpannerJsonQueryFunction.java @@ -0,0 +1,39 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.json; + +import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.expression.JsonQueryWrapMode; +import org.hibernate.type.spi.TypeConfiguration; + +public class SpannerJsonQueryFunction extends JsonQueryFunction { + + public SpannerJsonQueryFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration, false, false ); + } + + @Override + protected void render(SqlAppender sqlAppender, JsonQueryArguments arguments, ReturnableType returnType, SqlAstTranslator walker) { + if ( arguments.wrapMode() == JsonQueryWrapMode.WITH_WRAPPER ) { + sqlAppender.appendSql( "parse_json(concat('[', to_json_string(" ); + JsonQueryArguments noWrapArgs = new JsonQueryArguments( + arguments.jsonDocument(), + arguments.jsonPath(), + arguments.isJsonType(), + arguments.passingClause(), + null, + arguments.errorBehavior(), + arguments.emptyBehavior() + ); + super.render( sqlAppender, noWrapArgs, returnType, walker ); + sqlAppender.appendSql( "), ']'))" ); + } + else { + super.render( sqlAppender, arguments, returnType, walker ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SpannerJsonValueFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SpannerJsonValueFunction.java new file mode 100644 index 000000000000..fc149d790045 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SpannerJsonValueFunction.java @@ -0,0 +1,48 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.json; + + +import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.expression.CastTarget; +import org.hibernate.type.spi.TypeConfiguration; + +public class SpannerJsonValueFunction extends JsonValueFunction { + + public SpannerJsonValueFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration, false, false ); + } + + @Override + protected void render( + SqlAppender sqlAppender, + JsonValueArguments arguments, + ReturnableType returnType, + SqlAstTranslator walker) { + final CastTarget returningType = arguments.returningType(); + if ( returningType != null ) { + sqlAppender.appendSql( "cast(" ); + } + sqlAppender.appendSql( "json_value(" ); + arguments.jsonDocument().accept( walker ); + sqlAppender.appendSql( ',' ); + // Spanner requires literal JSONPath expressions; named parameters (e.g. $idx) are not parsed and must be manually inlined. + JsonPathHelper.appendInlinedJsonPathIncludingPassingClause( + sqlAppender, + "", + arguments.jsonPath(), + arguments.passingClause(), + walker + ); + sqlAppender.appendSql( ')' ); + if ( returningType != null ) { + sqlAppender.appendSql( " as " ); + returningType.accept( walker ); + sqlAppender.appendSql( ')' ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SpannerPostgreSQLJsonArrayFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SpannerPostgreSQLJsonArrayFunction.java new file mode 100644 index 000000000000..4fcd6d023b68 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SpannerPostgreSQLJsonArrayFunction.java @@ -0,0 +1,65 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.json; + +import java.util.List; + +import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.JsonNullBehavior; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * Spanner PostgreSQL json_array function. + */ +public class SpannerPostgreSQLJsonArrayFunction extends JsonArrayFunction { + + public SpannerPostgreSQLJsonArrayFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + if ( sqlAstArguments.isEmpty() ) { + sqlAppender.appendSql( "jsonb_build_array()" ); + } + else { + final SqlAstNode lastArgument = sqlAstArguments.get( sqlAstArguments.size() - 1 ); + final JsonNullBehavior nullBehavior; + final int argumentsCount; + if ( lastArgument instanceof JsonNullBehavior jsonNullBehavior ) { + nullBehavior = jsonNullBehavior; + argumentsCount = sqlAstArguments.size() - 1; + } + else { + nullBehavior = null; + argumentsCount = sqlAstArguments.size(); + } + + if ( nullBehavior == JsonNullBehavior.ABSENT ) { + sqlAppender.appendSql( "(select cast('['||coalesce(string_agg(cast(e.v as varchar),','),'')||']' as jsonb) from jsonb_array_elements(jsonb_build_array" ); + } + else { + sqlAppender.appendSql( "jsonb_build_array" ); + } + char separator = '('; + for ( int i = 0; i < argumentsCount; i++ ) { + sqlAppender.appendSql( separator ); + sqlAstArguments.get( i ).accept( walker ); + separator = ','; + } + sqlAppender.appendSql( ')' ); + if ( nullBehavior == JsonNullBehavior.ABSENT ) { + sqlAppender.appendSql( ") e(v) where jsonb_typeof(e.v) != 'null')" ); + } + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SpannerPostgreSQLJsonObjectFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SpannerPostgreSQLJsonObjectFunction.java new file mode 100644 index 000000000000..fe8d3d5552f5 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SpannerPostgreSQLJsonObjectFunction.java @@ -0,0 +1,67 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.json; + +import java.util.List; + +import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.JsonNullBehavior; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * Spanner PostgreSQL json_object function. + */ +public class SpannerPostgreSQLJsonObjectFunction extends JsonObjectFunction { + + public SpannerPostgreSQLJsonObjectFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration, false ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + if ( sqlAstArguments.isEmpty() ) { + sqlAppender.appendSql( "jsonb_build_object()" ); + } + else { + final SqlAstNode lastArgument = sqlAstArguments.get( sqlAstArguments.size() - 1 ); + final JsonNullBehavior nullBehavior; + final int argumentsCount; + if ( lastArgument instanceof JsonNullBehavior jsonNullBehavior ) { + nullBehavior = jsonNullBehavior; + argumentsCount = sqlAstArguments.size() - 1; + } + else { + nullBehavior = JsonNullBehavior.NULL; + argumentsCount = sqlAstArguments.size(); + } + + if ( nullBehavior == JsonNullBehavior.ABSENT ) { + sqlAppender.appendSql( "jsonb_strip_nulls(jsonb_build_object" ); + } + else { + sqlAppender.appendSql( "jsonb_build_object" ); + } + + char separator = '('; + for ( int i = 0; i < argumentsCount; i++ ) { + sqlAppender.appendSql( separator ); + sqlAstArguments.get( i ).accept( walker ); + separator = ','; + } + sqlAppender.appendSql( ')' ); + + if ( nullBehavior == JsonNullBehavior.ABSENT ) { + sqlAppender.appendSql( ')' ); + } + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/HANAXmlTableFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/HANAXmlTableFunction.java index dbcd1fb66619..74304e850291 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/HANAXmlTableFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/HANAXmlTableFunction.java @@ -235,7 +235,7 @@ private void addIdColumns(EmbeddableValuedModelPart embeddableModelPart, List idColumns) { - final DdlTypeRegistry ddlTypeRegistry = pluralAttributeMapping.getCollectionDescriptor() + final var ddlTypeRegistry = pluralAttributeMapping.getCollectionDescriptor() .getFactory() .getTypeConfiguration() .getDdlTypeRegistry(); @@ -243,7 +243,7 @@ private void addIdColumns(PluralAttributeMapping pluralAttributeMapping, List idColumns) { - final DdlTypeRegistry ddlTypeRegistry = entityMappingType.getEntityPersister() + final var ddlTypeRegistry = entityMappingType.getEntityPersister() .getFactory() .getTypeConfiguration() .getDdlTypeRegistry(); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/identity/SpannerIdentityColumnSupport.java b/hibernate-core/src/main/java/org/hibernate/dialect/identity/SpannerIdentityColumnSupport.java new file mode 100644 index 000000000000..09387dbb7605 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/identity/SpannerIdentityColumnSupport.java @@ -0,0 +1,32 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.identity; + +import org.hibernate.MappingException; + +/** + * Identity column support for Spanner. + */ +public class SpannerIdentityColumnSupport extends IdentityColumnSupportImpl { + public static final SpannerIdentityColumnSupport INSTANCE = new SpannerIdentityColumnSupport(); + + private SpannerIdentityColumnSupport() {} + + @Override + public boolean supportsIdentityColumns() { + return true; + } + + @Override + public String getIdentitySelectString(String table, String column, int type) throws MappingException { + throw new MappingException( + getClass().getName() + " does not support selecting the last generated identity value"); + } + + @Override + public String getIdentityColumnString(int type) { + return "not null generated by default as identity (bit_reversed_positive)"; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/lock/AbstractPessimisticUpdateLockingStrategy.java b/hibernate-core/src/main/java/org/hibernate/dialect/lock/AbstractPessimisticUpdateLockingStrategy.java index de3ebdbda91e..af1b1c4709d8 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/lock/AbstractPessimisticUpdateLockingStrategy.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/lock/AbstractPessimisticUpdateLockingStrategy.java @@ -10,12 +10,11 @@ import org.hibernate.StaleObjectStateException; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.persister.entity.EntityPersister; -import org.hibernate.pretty.MessageHelper; import org.hibernate.sql.Update; import java.sql.SQLException; -import static org.hibernate.internal.CoreMessageLogger.CORE_LOGGER; +import static org.hibernate.pretty.MessageHelper.infoString; /** * Common implementation of {@link PessimisticReadUpdateLockingStrategy} @@ -39,13 +38,15 @@ public abstract class AbstractPessimisticUpdateLockingStrategy implements Lockin public AbstractPessimisticUpdateLockingStrategy(EntityPersister lockable, LockMode lockMode) { this.lockable = lockable; this.lockMode = lockMode; - if ( !lockable.isVersioned() ) { - CORE_LOGGER.writeLocksNotSupported( lockable.getEntityName() ); - this.sql = null; + if ( lockMode.lessThan( LockMode.PESSIMISTIC_READ ) ) { + throw new HibernateException( "Lock mode " + lockMode + + " not valid for locking via 'update' statement" ); } - else { - this.sql = generateLockString(); + if ( !lockable.isVersioned() ) { + throw new HibernateException( "Entity '" + lockable.getEntityName() + + "' has no version and may not be locked via 'update' statement" ); } + this.sql = generateLockString(); } @Override @@ -54,14 +55,11 @@ public void lock(Object id, Object version, Object object, int timeout, SharedSe doLock( id, version, session ); } catch (JDBCException e) { - throw new PessimisticEntityLockException( object, "could not obtain pessimistic lock", e ); + throw new PessimisticEntityLockException( object, "Could not obtain pessimistic lock", e ); } } void doLock(Object id, Object version, SharedSessionContractImplementor session) { - if ( !lockable.isVersioned() ) { - throw new HibernateException( "write locks via update not supported for non-versioned entities [" + lockable.getEntityName() + "]" ); - } try { final var factory = session.getFactory(); final var jdbcCoordinator = session.getJdbcCoordinator(); @@ -100,7 +98,7 @@ void doLock(Object id, Object version, SharedSessionContractImplementor session) catch ( SQLException e ) { throw session.getJdbcServices().getSqlExceptionHelper().convert( e, - "could not lock: " + MessageHelper.infoString( lockable, id, session.getFactory() ), + "could not lock: " + infoString( lockable, id, session.getFactory() ), sql ); } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/lock/OptimisticForceIncrementLockingStrategy.java b/hibernate-core/src/main/java/org/hibernate/dialect/lock/OptimisticForceIncrementLockingStrategy.java index 5cf5ab687068..5276bd42a9d6 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/lock/OptimisticForceIncrementLockingStrategy.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/lock/OptimisticForceIncrementLockingStrategy.java @@ -34,15 +34,17 @@ public OptimisticForceIncrementLockingStrategy(EntityPersister lockable, LockMod this.lockable = lockable; this.lockMode = lockMode; if ( lockMode.lessThan( LockMode.OPTIMISTIC_FORCE_INCREMENT ) ) { - throw new HibernateException( "[" + lockMode + "] not valid for [" + lockable.getEntityName() + "]" ); + throw new HibernateException( "Entity '" + lockable.getEntityName() + + "' may not be locked at level " + lockMode ); + } + if ( !lockable.isVersioned() ) { + throw new HibernateException( "Entity '" + lockable.getEntityName() + + "' has no version and may not be locked at level " + lockMode); } } @Override public void lock(Object id, Object version, Object object, int timeout, EventSource session) { - if ( !lockable.isVersioned() ) { - throw new HibernateException( "[" + lockMode + "] not supported for non-versioned entities [" + lockable.getEntityName() + "]" ); - } // final EntityEntry entry = session.getPersistenceContextInternal().getEntry( object ); // Register the EntityIncrementVersionProcess action to run just prior to transaction commit. session.getActionQueue().registerCallback( new EntityIncrementVersionProcess( object ) ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/lock/OptimisticLockingStrategy.java b/hibernate-core/src/main/java/org/hibernate/dialect/lock/OptimisticLockingStrategy.java index 79337885d354..8c8c28260760 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/lock/OptimisticLockingStrategy.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/lock/OptimisticLockingStrategy.java @@ -33,15 +33,17 @@ public OptimisticLockingStrategy(EntityPersister lockable, LockMode lockMode) { this.lockable = lockable; this.lockMode = lockMode; if ( lockMode.lessThan( LockMode.OPTIMISTIC ) ) { - throw new HibernateException( "[" + lockMode + "] not valid for [" + lockable.getEntityName() + "]" ); + throw new HibernateException( "Entity '" + lockable.getEntityName() + + "' may not be locked at level " + lockMode ); + } + if ( !lockable.isVersioned() ) { + throw new HibernateException( "Entity '" + lockable.getEntityName() + + "' has no version and may not be locked at level " + lockMode); } } @Override public void lock(Object id, Object version, Object object, int timeout, EventSource session) { - if ( !lockable.isVersioned() ) { - throw new HibernateException( "[" + lockMode + "] not supported for non-versioned entities [" + lockable.getEntityName() + "]" ); - } // Register the EntityVerifyVersionProcess action to run just prior to transaction commit. session.getActionQueue().registerCallback( new EntityVerifyVersionProcess( object ) ); } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/lock/PessimisticEntityLockException.java b/hibernate-core/src/main/java/org/hibernate/dialect/lock/PessimisticEntityLockException.java index b7469e0e7b5e..3a2e1e9e8fcb 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/lock/PessimisticEntityLockException.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/lock/PessimisticEntityLockException.java @@ -24,4 +24,9 @@ public class PessimisticEntityLockException extends LockingStrategyException { public PessimisticEntityLockException(Object entity, String message, JDBCException cause) { super( entity, message, cause ); } + + @Override + public JDBCException getCause() { + return (JDBCException) super.getCause(); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/lock/PessimisticForceIncrementLockingStrategy.java b/hibernate-core/src/main/java/org/hibernate/dialect/lock/PessimisticForceIncrementLockingStrategy.java index cc2df7827e9d..b5e1345455cc 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/lock/PessimisticForceIncrementLockingStrategy.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/lock/PessimisticForceIncrementLockingStrategy.java @@ -34,15 +34,17 @@ public PessimisticForceIncrementLockingStrategy(EntityPersister lockable, LockMo this.lockMode = lockMode; // ForceIncrement can be used for PESSIMISTIC_READ, PESSIMISTIC_WRITE or PESSIMISTIC_FORCE_INCREMENT if ( lockMode.lessThan( LockMode.PESSIMISTIC_READ ) ) { - throw new HibernateException( "[" + lockMode + "] not valid for [" + lockable.getEntityName() + "]" ); + throw new HibernateException( "Entity '" + lockable.getEntityName() + + "' may not be locked at level " + lockMode ); + } + if ( !lockable.isVersioned() ) { + throw new HibernateException( "Entity '" + lockable.getEntityName() + + "' has no version and may not be locked at level " + lockMode); } } @Override public void lock(Object id, Object version, Object object, int timeout, SharedSessionContractImplementor session) { - if ( !lockable.isVersioned() ) { - throw new HibernateException( "[" + lockMode + "] not supported for non-versioned entities [" + lockable.getEntityName() + "]" ); - } final var entry = session.getPersistenceContextInternal().getEntry( object ); OptimisticLockHelper.forceVersionIncrement( object, entry, session ); } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/lock/PessimisticReadUpdateLockingStrategy.java b/hibernate-core/src/main/java/org/hibernate/dialect/lock/PessimisticReadUpdateLockingStrategy.java index 717fe17b9b08..51d222654d43 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/lock/PessimisticReadUpdateLockingStrategy.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/lock/PessimisticReadUpdateLockingStrategy.java @@ -4,7 +4,6 @@ */ package org.hibernate.dialect.lock; -import org.hibernate.HibernateException; import org.hibernate.LockMode; import org.hibernate.persister.entity.EntityPersister; @@ -30,8 +29,5 @@ public class PessimisticReadUpdateLockingStrategy extends AbstractPessimisticUpd */ public PessimisticReadUpdateLockingStrategy(EntityPersister lockable, LockMode lockMode) { super( lockable, lockMode ); - if ( lockMode.lessThan( LockMode.PESSIMISTIC_READ ) ) { - throw new HibernateException( "[" + lockMode + "] not valid for update statement" ); - } } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/lock/PessimisticWriteUpdateLockingStrategy.java b/hibernate-core/src/main/java/org/hibernate/dialect/lock/PessimisticWriteUpdateLockingStrategy.java index b809344f1d78..0bfab56ad135 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/lock/PessimisticWriteUpdateLockingStrategy.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/lock/PessimisticWriteUpdateLockingStrategy.java @@ -4,7 +4,6 @@ */ package org.hibernate.dialect.lock; -import org.hibernate.HibernateException; import org.hibernate.LockMode; import org.hibernate.persister.entity.EntityPersister; @@ -29,8 +28,5 @@ public class PessimisticWriteUpdateLockingStrategy extends AbstractPessimisticUp */ public PessimisticWriteUpdateLockingStrategy(EntityPersister lockable, LockMode lockMode) { super( lockable, lockMode ); - if ( lockMode.lessThan( LockMode.PESSIMISTIC_READ ) ) { - throw new HibernateException( "[" + lockMode + "] not valid for update statement" ); - } } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/lock/UpdateLockingStrategy.java b/hibernate-core/src/main/java/org/hibernate/dialect/lock/UpdateLockingStrategy.java index cc0e93d1ba20..1e7235629d82 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/lock/UpdateLockingStrategy.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/lock/UpdateLockingStrategy.java @@ -35,7 +35,7 @@ public class UpdateLockingStrategy extends AbstractPessimisticUpdateLockingStrat public UpdateLockingStrategy(EntityPersister lockable, LockMode lockMode) { super( lockable, lockMode ); if ( lockMode.lessThan( LockMode.WRITE ) ) { - throw new HibernateException( "[" + lockMode + "] not valid for update statement" ); + throw new HibernateException( "Lock mode " + lockMode + " not valid for locking via 'update' statement" ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/lock/internal/Helper.java b/hibernate-core/src/main/java/org/hibernate/dialect/lock/internal/Helper.java index 2f8924011b27..28bd8283d291 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/lock/internal/Helper.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/lock/internal/Helper.java @@ -6,7 +6,6 @@ import jakarta.persistence.Timeout; import org.hibernate.HibernateException; -import org.hibernate.engine.jdbc.spi.SqlExceptionHelper; import org.hibernate.engine.spi.SessionFactoryImplementor; import java.sql.Connection; @@ -29,17 +28,17 @@ public static Timeout getLockTimeout( TimeoutExtractor extractor, Connection connection, SessionFactoryImplementor factory) { - try (final java.sql.Statement statement = connection.createStatement()) { + try ( final var statement = connection.createStatement() ) { factory.getJdbcServices().getSqlStatementLogger().logStatement( sql ); - final ResultSet results = statement.executeQuery( sql ); + final var results = statement.executeQuery( sql ); if ( !results.next() ) { throw new HibernateException( "Unable to query JDBC Connection for current lock-timeout setting (no result)" ); } return extractor.extractFrom( results ); } catch (SQLException sqle) { - final SqlExceptionHelper sqlExceptionHelper = factory.getJdbcServices().getJdbcEnvironment().getSqlExceptionHelper(); - throw sqlExceptionHelper.convert( sqle, "Unable to query JDBC Connection for current lock-timeout setting" ); + throw factory.getJdbcServices().getJdbcEnvironment().getSqlExceptionHelper() + .convert( sqle, "Unable to query JDBC Connection for current lock-timeout setting" ); } } @@ -50,13 +49,13 @@ public static void setLockTimeout( String sql, Connection connection, SessionFactoryImplementor factory) { - try (final java.sql.Statement statement = connection.createStatement()) { + try ( final var statement = connection.createStatement() ) { factory.getJdbcServices().getSqlStatementLogger().logStatement( sql ); statement.execute( sql ); } catch (SQLException sqle) { - final SqlExceptionHelper sqlExceptionHelper = factory.getJdbcServices().getJdbcEnvironment().getSqlExceptionHelper(); - throw sqlExceptionHelper.convert( sqle, "Unable to set lock-timeout setting on JDBC connection" ); + throw factory.getJdbcServices().getJdbcEnvironment().getSqlExceptionHelper() + .convert( sqle, "Unable to set lock-timeout setting on JDBC connection" ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/lock/internal/MySQLLockingSupport.java b/hibernate-core/src/main/java/org/hibernate/dialect/lock/internal/MySQLLockingSupport.java index 739007513db2..3ceb9658490d 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/lock/internal/MySQLLockingSupport.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/lock/internal/MySQLLockingSupport.java @@ -90,9 +90,23 @@ public ConnectionLockTimeoutStrategy getConnectionLockTimeoutStrategy() { } public static class ConnectionLockTimeoutStrategyImpl implements ConnectionLockTimeoutStrategy { + // Making this configurable so TiDBDialect can re-use this, but with a lower value + // TiDB v8.5.5 limits innodb_lock_wait_timeout to 3600s + private final int foreverValue; + + public ConnectionLockTimeoutStrategyImpl() { + // see https://dev.mysql.com/doc/refman/8.4/en/innodb-parameters.html#sysvar_innodb_lock_wait_timeout + // unit: seconds, allowed values: [1, 1073741824] + this( 100_000 ); + } + + public ConnectionLockTimeoutStrategyImpl(int foreverValue) { + this.foreverValue = foreverValue; + } + @Override public Level getSupportedLevel() { - return ConnectionLockTimeoutStrategy.Level.EXTENDED; + return Level.SUPPORTED; } @Override @@ -101,12 +115,9 @@ public Timeout getLockTimeout(Connection connection, SessionFactoryImplementor f "SELECT @@SESSION.innodb_lock_wait_timeout", (resultSet) -> { // see https://dev.mysql.com/doc/refman/8.4/en/innodb-parameters.html#sysvar_innodb_lock_wait_timeout - final int millis = resultSet.getInt( 1 ); - return switch ( millis ) { - case 0 -> Timeouts.NO_WAIT; - case 100000000 -> Timeouts.WAIT_FOREVER; - default -> Timeout.milliseconds( millis ); - }; + // unit: seconds, allowed values: [1, 1073741824] + final int seconds = resultSet.getInt( 1 ); + return seconds == foreverValue ? Timeouts.WAIT_FOREVER : Timeout.seconds( seconds ); }, connection, factory @@ -119,14 +130,18 @@ public void setLockTimeout(Timeout timeout, Connection connection, SessionFactor timeout, (t) -> { // see https://dev.mysql.com/doc/refman/8.4/en/innodb-parameters.html#sysvar_innodb_lock_wait_timeout + // unit: seconds, allowed values: [1, 1073741824] final int milliseconds = timeout.milliseconds(); if ( milliseconds == SKIP_LOCKED_MILLI ) { throw new HibernateException( "Connection lock-timeout does not accept skip-locked" ); } + if ( milliseconds == NO_WAIT_MILLI ) { + throw new HibernateException( "Connection lock-timeout does not accept no-wait" ); + } if ( milliseconds == WAIT_FOREVER_MILLI ) { - return 100000000; + return foreverValue; } - return milliseconds; + return (int) Math.ceil( (double) milliseconds / 1000); }, "SET @@SESSION.innodb_lock_wait_timeout = %s", connection, diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/lock/internal/PostgreSQLLockingSupport.java b/hibernate-core/src/main/java/org/hibernate/dialect/lock/internal/PostgreSQLLockingSupport.java index 89dc933e087e..23f2ef4b5d9d 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/lock/internal/PostgreSQLLockingSupport.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/lock/internal/PostgreSQLLockingSupport.java @@ -79,19 +79,31 @@ public Timeout getLockTimeout(Connection connection, SessionFactoryImplementor f return Helper.getLockTimeout( "select current_setting('lock_timeout', true)", (resultSet) -> { - // even though lock_timeout is "in milliseconds", `current_setting` - // returns a String form which unfortunately varies depending on - // the actual value: - // * for zero (no timeout), "0" is returned - // * for non-zero, `{timeout-in-seconds}s` is returned (e.g. "4s") - // so we need to "parse" that form here - final String value = resultSet.getString( 1 ); + // Although lock_timeout is stored internally in milliseconds, + // `current_setting` returns a String in a canonical, human-readable form + // that varies depending on the value: + // * "0" is returned for no timeout (WAIT_FOREVER) + // * Non-zero values may be returned with units such as: + // - milliseconds: "500ms" + // - seconds: "3s" + // - minutes: "1min" + // - hours: "1h" + // Therefore, we need to parse this String carefully to reconstruct the correct Timeout. + String value = resultSet.getString( 1 ); if ( "0".equals( value ) ) { return Timeouts.WAIT_FOREVER; } - assert value.endsWith( "s" ); - final int secondsValue = Integer.parseInt( value.substring( 0, value.length() - 1 ) ); - return Timeout.seconds( secondsValue ); + final var unitStartIndex = findUnitStartIndex( value ); + final var amount = Integer.parseInt( value, 0, unitStartIndex, 10 ); + return switch ( unitStartIndex == -1 ? "ms" : value.substring( unitStartIndex ) ) { + case "ms" -> Timeout.milliseconds( amount ); + case "s" -> Timeout.seconds( amount ); + case "min" -> Timeout.seconds( amount * 60 ); + case "h" -> Timeout.seconds( amount * 3600 ); + case "d" -> Timeout.seconds( amount * 3600 * 24 ); + default -> throw new IllegalArgumentException( + "Unexpected PostgreSQL lock_timeout format: " + value ); + }; }, connection, factory @@ -120,4 +132,13 @@ public void setLockTimeout(Timeout timeout, Connection connection, SessionFactor factory ); } + + private static int findUnitStartIndex(String value) { + for ( int i = value.length() - 1; i >= 0; i-- ) { + if ( Character.isDigit( value.charAt( i ) ) ) { + return i + 1; + } + } + return -1; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/lock/internal/TransactSQLLockingSupport.java b/hibernate-core/src/main/java/org/hibernate/dialect/lock/internal/TransactSQLLockingSupport.java index 95714c02456d..a2636d1a6420 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/lock/internal/TransactSQLLockingSupport.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/lock/internal/TransactSQLLockingSupport.java @@ -47,8 +47,8 @@ public class TransactSQLLockingSupport extends LockingSupportParameterized { public static final LockingSupport SYBASE_ASE = new TransactSQLLockingSupport( PessimisticLockStyle.TABLE_HINT, LockTimeoutType.CONNECTION, - LockTimeoutType.NONE, - LockTimeoutType.NONE, + LockTimeoutType.CONNECTION, + LockTimeoutType.QUERY, RowLockStrategy.TABLE, OuterJoinLockingType.IDENTIFIED, SybaseImpl.IMPL @@ -154,7 +154,7 @@ public static class SybaseImpl implements ConnectionLockTimeoutStrategy { @Override public Level getSupportedLevel() { - return Level.SUPPORTED; + return Level.EXTENDED; } @Override @@ -162,11 +162,11 @@ public Timeout getLockTimeout(Connection connection, SessionFactoryImplementor f return Helper.getLockTimeout( "select @@lock_timeout", (resultSet) -> { - final int timeoutInMilliseconds = resultSet.getInt( 1 ); - return switch ( timeoutInMilliseconds ) { + final int timeoutInSeconds = resultSet.getInt( 1 ); + return switch ( timeoutInSeconds ) { case -1 -> Timeouts.WAIT_FOREVER; case 0 -> Timeouts.NO_WAIT; - default -> Timeout.milliseconds( timeoutInMilliseconds ); + default -> Timeout.seconds( timeoutInSeconds ); }; }, connection, @@ -176,27 +176,30 @@ public Timeout getLockTimeout(Connection connection, SessionFactoryImplementor f @Override public void setLockTimeout(Timeout timeout, Connection connection, SessionFactoryImplementor factory) { + // see https://infocenter.sybase.com/help/index.jsp?topic=/com.sybase.infocenter.dc31654.1600/doc/html/san1360629104549.html + // SAP Adaptive Server Enterprise 16.0 + // > System Administration Guide 16.0: Volume 1 + // > Setting Configuration Parameters + // > Configuration Parameters + // > Alphabetical Listing of Configuration Parameters + // > lock wait period + // + // range: 0 – 2147483647 + // default: 2147483647 + // unit: seconds final int milliseconds = timeout.milliseconds(); if ( milliseconds == Timeouts.SKIP_LOCKED_MILLI ) { throw new HibernateException( "Sybase does not accept skip-locked for lock-timeout" ); } - // Sybase needs a special syntax for NO_WAIT rather than a number - if ( milliseconds == Timeouts.NO_WAIT_MILLI ) { - // NOTE: The docs say this is supported, and it does not fail when used, - // but immediately after the setting value is still -1. So it seems to - // allow the call but ignore it. Might just be jTDS. - Helper.setLockTimeout( "set lock nowait", connection, factory ); - } - else if ( milliseconds == Timeouts.WAIT_FOREVER_MILLI ) { + if ( milliseconds == Timeouts.WAIT_FOREVER_MILLI ) { // Even though Sybase's wait-forever (and default) value is -1, it won't accept - // -1 as a value because, well, of course it won't. Need to set max value instead - // because, well, of course you do. - Helper.setLockTimeout( 2147483647, "set lock wait %s", connection, factory ); + // -1 as a value because, well, of course it won't. Need to omit the argument to reset it + Helper.setLockTimeout( "set lock wait", connection, factory ); } else { - Helper.setLockTimeout( milliseconds, "set lock wait %s", connection, factory ); + Helper.setLockTimeout( (int) Math.ceil( (double) milliseconds / 1000), "set lock wait %s", connection, factory ); } } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/lock/spi/LockingSupport.java b/hibernate-core/src/main/java/org/hibernate/dialect/lock/spi/LockingSupport.java index 71434d4ab2ba..49f061e410b9 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/lock/spi/LockingSupport.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/lock/spi/LockingSupport.java @@ -63,7 +63,7 @@ default LockTimeoutType getLockTimeoutType(Timeout timeout) { /** * The {@linkplain RowLockStrategy strategy} for indicating which rows * to lock as part of a {@code for share of} style clause. - *

    + *

    * By default, simply uses {@linkplain #getWriteRowLockStrategy()}. */ default RowLockStrategy getReadRowLockStrategy() { diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/pagination/AbstractLimitHandler.java b/hibernate-core/src/main/java/org/hibernate/dialect/pagination/AbstractLimitHandler.java index e0562ab87015..580fb8e638f1 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/pagination/AbstractLimitHandler.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/pagination/AbstractLimitHandler.java @@ -248,8 +248,7 @@ public static boolean hasMaxRows(Limit limit) { */ public static boolean hasFirstRow(Limit limit) { return limit != null - && limit.getFirstRow() != null - && limit.getFirstRow() > 0; + && limit.getFirstRow() != null; } /** diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/sequence/SpannerSequenceSupport.java b/hibernate-core/src/main/java/org/hibernate/dialect/sequence/SpannerSequenceSupport.java new file mode 100644 index 000000000000..b8d188d9894d --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/sequence/SpannerSequenceSupport.java @@ -0,0 +1,89 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.sequence; + + +import org.hibernate.MappingException; + +import static org.hibernate.internal.util.StringHelper.isNotEmpty; + +import org.hibernate.dialect.SpannerDialect; + +/** + * Sequence support for Spanner. + */ +public class SpannerSequenceSupport implements SequenceSupport { + + private final SpannerDialect dialect; + + public SpannerSequenceSupport(SpannerDialect dialect) { + this.dialect = dialect; + } + + @Override + public String getCreateSequenceString(String sequenceName) throws MappingException { + return getCreateSequenceString(sequenceName, 1, 1); + } + + @Override + public String getCreateSequenceString(String sequenceName, int initialValue, int incrementSize) + throws MappingException { + if (incrementSize == 1) { + return getCreateSequenceString(sequenceName, initialValue, ""); + } + throw new MappingException("Cloud Spanner does not support sequences with an increment size != 1"); + } + + @Override + public String[] getCreateSequenceStrings(String sequenceName, int initialValue, int incrementSize, String options) + throws MappingException { + if (incrementSize == 1) { + return new String[] { getCreateSequenceString(sequenceName, initialValue, options) }; + } + throw new MappingException("Cloud Spanner does not support sequences with an increment size != 1"); + } + + protected String getCreateSequenceString(String sequenceName, int initialValue, String additionalOptions) { + final var builder = new StringBuilder("create sequence if not exists "); + builder.append(sequenceName).append(" options(sequence_kind=\"bit_reversed_positive\""); + if (initialValue != 1) { + builder.append(", start_with_counter=").append(initialValue); + } + if (isNotEmpty(additionalOptions)) { + builder.append(", ").append(additionalOptions); + } + builder.append(")"); + return builder.toString(); + } + + @Override + public String getDropSequenceString(String sequenceName) { + return "drop sequence if exists " + sequenceName; + } + + @Override + public String getRestartSequenceString(String sequenceName, long startWith) { + return "alter sequence " + sequenceName + " set options (start_with_counter = " + startWith + ")"; + } + + @Override + public String getSequenceNextValString(String sequenceName) { + return "select " + getSelectSequenceNextValString(sequenceName); + } + + @Override + public String getSelectSequenceNextValString(String sequenceName) { + var nextValString = "get_next_sequence_value(sequence " + sequenceName + ")"; + if ( dialect != null && dialect.useIntegerForPrimaryKey() ) { + return "bit_reverse(" + nextValString + ", true)"; + } + return nextValString; + } + + @Override + public boolean supportsPooledSequences() { + return false; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/CockroachSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/CockroachSqlAstTranslator.java index 85bdacbc40ab..43bb87f4e509 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/CockroachSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/CockroachSqlAstTranslator.java @@ -27,6 +27,8 @@ import org.hibernate.sql.exec.internal.JdbcOperationQueryInsertImpl; import org.hibernate.sql.exec.spi.JdbcOperation; import org.hibernate.sql.exec.spi.JdbcOperationQueryInsert; +import org.hibernate.sql.model.internal.OptionalTableInsert; +import org.hibernate.sql.model.internal.TableInsertStandard; /** * A SQL AST translator for Cockroach. @@ -47,6 +49,39 @@ public void visitBinaryArithmeticExpression(BinaryArithmeticExpression arithmeti super.visitBinaryArithmeticExpression(arithmeticExpression); } + @Override + public void visitStandardTableInsert(TableInsertStandard tableInsert) { + getCurrentClauseStack().push( Clause.INSERT ); + try { + renderInsertInto( tableInsert ); + if ( tableInsert instanceof OptionalTableInsert optionalTableInsert ) { + appendSql( " on conflict " ); + final String constraintName = optionalTableInsert.getConstraintName(); + if ( constraintName != null ) { + appendSql( " on constraint " ); + appendSql( constraintName ); + } + else { + char separator = '('; + for ( String constraintColumnName : optionalTableInsert.getConstraintColumnNames() ) { + appendSql( separator ); + appendSql( constraintColumnName ); + separator = ','; + } + appendSql( ')' ); + } + appendSql( " do nothing" ); + } + + if ( tableInsert.getNumberOfReturningColumns() > 0 ) { + visitReturningColumns( tableInsert::getReturningColumns ); + } + } + finally { + getCurrentClauseStack().pop(); + } + } + @Override protected JdbcOperationQueryInsert translateInsert(InsertSelectStatement sqlAst) { visitInsertStatement( sqlAst ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/DB2iSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/DB2iSqlAstTranslator.java index 1c23fd495dc6..38826e01eccf 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/DB2iSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/DB2iSqlAstTranslator.java @@ -9,8 +9,11 @@ import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.sql.ast.tree.Statement; import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.SqlTupleContainer; import org.hibernate.sql.exec.spi.JdbcOperation; +import java.util.List; + import static org.hibernate.dialect.DB2iDialect.DB2_LUW_VERSION; /** @@ -32,6 +35,20 @@ protected void renderComparison(Expression lhs, ComparisonOperator operator, Exp renderComparisonStandard( lhs, operator, rhs ); } + @Override + protected void renderExpressionsAsValuesSubquery(int tupleSize, List listExpressions) { + // DB2 for i supports type-inference in this special VALUES expression, but not if it's wrapped as SELECT + appendSql( "values" ); + char separator = ' '; + for ( Expression expression : listExpressions ) { + appendSql( separator ); + appendSql( OPEN_PARENTHESIS ); + renderCommaSeparated( SqlTupleContainer.getSqlTuple( expression ).getExpressions() ); + appendSql( CLOSE_PARENTHESIS ); + separator = ','; + } + } + @Override public DatabaseVersion getDB2Version() { return DB2_LUW_VERSION; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/DB2zSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/DB2zSqlAstTranslator.java index aafa7174ebf4..7c4eaf5e42db 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/DB2zSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/DB2zSqlAstTranslator.java @@ -55,6 +55,12 @@ protected String getNewTableChangeModifier() { return "final"; } + @Override + protected boolean preferUnionQueryForTupleInListPredicate() { + // DB2 z/OS can't use an index when rendering a union query + return false; + } + @Override public DatabaseVersion getDB2Version() { return DB2_LUW_VERSION; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/H2SqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/H2SqlAstTranslator.java index 2b4c24a35156..b10e3189a5a0 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/H2SqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/H2SqlAstTranslator.java @@ -4,6 +4,7 @@ */ package org.hibernate.dialect.sql.ast; +import java.util.ArrayDeque; import java.util.List; import org.hibernate.LockMode; @@ -14,6 +15,7 @@ import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.sql.ast.Clause; import org.hibernate.sql.ast.SqlAstNodeRenderingMode; +import org.hibernate.sql.ast.spi.FullJoinEmulation; import org.hibernate.sql.ast.spi.SqlAstTranslatorWithMerge; import org.hibernate.sql.ast.spi.SqlSelection; import org.hibernate.sql.ast.tree.Statement; @@ -27,8 +29,8 @@ import org.hibernate.sql.ast.tree.expression.SqlTuple; import org.hibernate.sql.ast.tree.expression.Summarization; import org.hibernate.sql.ast.tree.from.NamedTableReference; -import org.hibernate.sql.ast.tree.from.QueryPartTableReference; import org.hibernate.sql.ast.tree.from.TableGroup; +import org.hibernate.sql.ast.tree.from.QueryPartTableReference; import org.hibernate.sql.ast.tree.from.TableReference; import org.hibernate.sql.ast.tree.insert.ConflictClause; import org.hibernate.sql.ast.tree.insert.InsertSelectStatement; @@ -36,6 +38,8 @@ import org.hibernate.sql.ast.tree.predicate.LikePredicate; import org.hibernate.sql.ast.tree.select.QueryPart; import org.hibernate.sql.ast.tree.select.SelectClause; +import org.hibernate.sql.ast.tree.select.SortSpecification; +import org.hibernate.sql.ast.tree.select.QuerySpec; import org.hibernate.sql.ast.tree.update.UpdateStatement; import org.hibernate.sql.exec.spi.JdbcOperation; import org.hibernate.sql.model.internal.TableInsertStandard; @@ -51,9 +55,45 @@ public class H2SqlAstTranslator extends SqlAstTranslatorWithMerge { private boolean renderAsArray; + private final ArrayDeque fullJoinEmulations = new ArrayDeque<>(); public H2SqlAstTranslator(SessionFactoryImplementor sessionFactory, Statement statement) { super( sessionFactory, statement ); + this.fullJoinEmulations.push( new FullJoinEmulation( this ) ); + } + + private FullJoinEmulation currentFullJoinEmulationHelper() { + return fullJoinEmulations.getFirst(); + } + + @Override + public void visitQuerySpec(QuerySpec querySpec) { + final var helper = currentFullJoinEmulationHelper(); + final boolean needsNestedHelper = + helper.hasActiveFullJoinEmulation() + && !helper.isFullJoinEmulationQueryPart( querySpec ); + if ( needsNestedHelper ) { + fullJoinEmulations.push( new FullJoinEmulation( this ) ); + } + try { + final var currentHelper = currentFullJoinEmulationHelper(); + if ( !currentHelper.renderFullJoinEmulationBranchIfNeeded( querySpec, super::visitQuerySpec ) + && !currentHelper.emulateFullJoinWithUnionIfNeeded( querySpec ) ) { + super.visitQuerySpec( querySpec ); + } + } + finally { + if ( needsNestedHelper ) { + fullJoinEmulations.pop(); + } + } + } + + @Override + public void visitSelectClause(SelectClause selectClause) { + if ( !currentFullJoinEmulationHelper().renderSelectClauseIfNeeded( selectClause ) ) { + super.visitSelectClause( selectClause ); + } } @Override @@ -170,10 +210,11 @@ protected void renderDmlTargetTableExpression(NamedTableReference tableReference @Override protected void visitConflictClause(ConflictClause conflictClause) { - if ( conflictClause != null ) { - if ( conflictClause.isDoUpdate() && conflictClause.getConstraintName() != null ) { - throw new IllegalQueryOperationException( "Insert conflict 'do update' clause with constraint name is not supported" ); - } + if ( conflictClause != null + && conflictClause.isDoUpdate() + && conflictClause.getConstraintName() != null ) { + throw new IllegalQueryOperationException( + "Insert conflict 'do update' clause with constraint name is not supported" ); } } @@ -220,23 +261,31 @@ public void visitBooleanExpressionPredicate(BooleanExpressionPredicate booleanEx } } + @Override + protected void visitOrderBy(List sortSpecifications) { + currentFullJoinEmulationHelper().renderOrderByIfNeeded( getCurrentQueryPart(), sortSpecifications, super::visitOrderBy ); + } + @Override public void visitOffsetFetchClause(QueryPart queryPart) { - if ( isRowsOnlyFetchClauseType( queryPart ) ) { - if ( supportsOffsetFetchClause() ) { - renderOffsetFetchClause( queryPart, true ); - } - else { - renderLimitOffsetClause( queryPart ); - } - } - else { - if ( supportsOffsetFetchClausePercentWithTies() ) { - renderOffsetFetchClause( queryPart, true ); + if ( !currentFullJoinEmulationHelper().isFullJoinEmulationQueryPart( queryPart ) ) { + if ( isRowsOnlyFetchClauseType( queryPart ) ) { + if ( supportsOffsetFetchClause() ) { + renderOffsetFetchClause( queryPart, true ); + } + else { + renderLimitOffsetClause( queryPart ); + } } else { - // FETCH PERCENT and WITH TIES were introduced along with window functions - throw new IllegalArgumentException( "Can't emulate fetch clause type: " + queryPart.getFetchClauseType() ); + if ( supportsOffsetFetchClausePercentWithTies() ) { + renderOffsetFetchClause( queryPart, true ); + } + else { + // FETCH PERCENT and WITH TIES were introduced along with window functions + throw new IllegalArgumentException( + "Can't emulate fetch clause type: " + queryPart.getFetchClauseType() ); + } } } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/MariaDBSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/MariaDBSqlAstTranslator.java index 30958f0fe4eb..5b5e270510fa 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/MariaDBSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/MariaDBSqlAstTranslator.java @@ -4,6 +4,7 @@ */ package org.hibernate.dialect.sql.ast; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.List; @@ -14,10 +15,12 @@ import org.hibernate.metamodel.mapping.JdbcMappingContainer; import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.sql.ast.Clause; +import org.hibernate.sql.ast.spi.NestedOrTargetTableCorrelationVisitor; +import org.hibernate.sql.ast.tree.AbstractUpdateOrDeleteStatement; +import org.hibernate.sql.ast.tree.MutationStatement; import org.hibernate.sql.ast.tree.Statement; import org.hibernate.sql.ast.tree.delete.DeleteStatement; import org.hibernate.sql.ast.tree.expression.BinaryArithmeticExpression; -import org.hibernate.sql.ast.tree.expression.CastTarget; import org.hibernate.sql.ast.tree.expression.ColumnReference; import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.Literal; @@ -32,25 +35,35 @@ import org.hibernate.sql.ast.tree.select.QueryGroup; import org.hibernate.sql.ast.tree.select.QueryPart; import org.hibernate.sql.ast.tree.select.QuerySpec; +import org.hibernate.sql.ast.tree.select.SelectClause; import org.hibernate.sql.ast.tree.select.SelectStatement; +import org.hibernate.sql.ast.tree.select.SortSpecification; import org.hibernate.sql.ast.tree.update.UpdateStatement; import org.hibernate.sql.exec.internal.JdbcOperationQueryInsertImpl; import org.hibernate.sql.exec.spi.JdbcOperation; import org.hibernate.sql.exec.spi.JdbcOperationQueryInsert; import org.hibernate.sql.model.ast.ColumnValueBinding; +import org.hibernate.sql.ast.spi.FullJoinEmulation; /** * A SQL AST translator for MariaDB. * * @author Christian Beikov + * @author Yoobin Yoon */ public class MariaDBSqlAstTranslator extends SqlAstTranslatorWithOnDuplicateKeyUpdate { private final MariaDBDialect dialect; + private final ArrayDeque fullJoinEmulations = new ArrayDeque<>(); public MariaDBSqlAstTranslator(SessionFactoryImplementor sessionFactory, Statement statement, MariaDBDialect dialect) { super( sessionFactory, statement ); this.dialect = dialect; + this.fullJoinEmulations.push( new FullJoinEmulation( this ) ); + } + + private FullJoinEmulation currentFullJoinEmulationHelper() { + return fullJoinEmulations.getFirst(); } @Override @@ -113,17 +126,24 @@ && getStatementStack().getCurrent() instanceof InsertSelectStatement insertSelec @Override protected void renderDeleteClause(DeleteStatement statement) { - appendSql( "delete" ); final Stack clauseStack = getClauseStack(); try { clauseStack.push( Clause.DELETE ); - renderTableReferenceIdentificationVariable( statement.getTargetTable() ); - if ( statement.getFromClause().getRoots().isEmpty() ) { - appendSql( " from " ); - renderDmlTargetTableExpression( statement.getTargetTable() ); + if ( usesSingleTableDml( statement ) ) { + appendSql( "delete from " ); + appendSql( statement.getTargetTable().getTableExpression() ); + registerAffectedTable( statement.getTargetTable() ); } else { - visitFromClause( statement.getFromClause() ); + appendSql( "delete" ); + renderTableReferenceIdentificationVariable( statement.getTargetTable() ); + if ( statement.getFromClause().getRoots().isEmpty() ) { + appendSql( " from " ); + renderDmlTargetTableExpression( statement.getTargetTable() ); + } + else { + visitFromClause( statement.getFromClause() ); + } } } finally { @@ -133,7 +153,12 @@ protected void renderDeleteClause(DeleteStatement statement) { @Override protected void renderUpdateClause(UpdateStatement updateStatement) { - if ( updateStatement.getFromClause().getRoots().isEmpty() ) { + if ( usesSingleTableDml( updateStatement ) ) { + appendSql( "update " ); + appendSql( updateStatement.getTargetTable().getTableExpression() ); + registerAffectedTable( updateStatement.getTargetTable() ); + } + else if ( updateStatement.getFromClause().getRoots().isEmpty() ) { super.renderUpdateClause( updateStatement ); } else { @@ -145,7 +170,7 @@ protected void renderUpdateClause(UpdateStatement updateStatement) { @Override protected void renderDmlTargetTableExpression(NamedTableReference tableReference) { super.renderDmlTargetTableExpression( tableReference ); - if ( getClauseStack().getCurrent() != Clause.INSERT ) { + if ( getClauseStack().getCurrent() != Clause.INSERT && !usesSingleTableDml( getCurrentDmlStatement() ) ) { renderTableReferenceIdentificationVariable( tableReference ); } } @@ -178,6 +203,16 @@ protected String determineColumnReferenceQualifier(ColumnReference columnReferen || !( getCurrentDmlStatement() instanceof InsertSelectStatement insertSelectStatement ) || ( dmlAlias = insertSelectStatement.getTargetTable().getIdentificationVariable() ) == null || !dmlAlias.equals( columnReference.getQualifier() ) ) { + final MutationStatement currentStatement = getCurrentDmlStatement(); + if ( currentStatement != null && usesSingleTableDml( currentStatement ) && columnReference.getQualifier() != null ) { + final NamedTableReference targetTable = currentStatement.getTargetTable(); + final String targetTableName = targetTable.getTableExpression(); + final String qualifier = columnReference.getQualifier(); + final String targetAlias = targetTable.getIdentificationVariable(); + if ( ( targetAlias != null && qualifier.equals( targetAlias ) ) || qualifier.equals( targetTableName ) ) { + return !getQueryPartStack().isEmpty() ? targetTableName : null; + } + } return columnReference.getQualifier(); } // Qualify the column reference with the table expression also when in subqueries @@ -247,11 +282,37 @@ public void visitQueryGroup(QueryGroup queryGroup) { @Override public void visitQuerySpec(QuerySpec querySpec) { - if ( shouldEmulateFetchClause( querySpec ) ) { - emulateFetchOffsetWithWindowFunctions( querySpec, true ); + final var helper = currentFullJoinEmulationHelper(); + final boolean needsNestedHelper = + helper.hasActiveFullJoinEmulation() + && !helper.isFullJoinEmulationQueryPart( querySpec ); + if ( needsNestedHelper ) { + fullJoinEmulations.push( new FullJoinEmulation( this ) ); } - else { - super.visitQuerySpec( querySpec ); + try { + final var currentHelper = currentFullJoinEmulationHelper(); + if ( !currentHelper.renderFullJoinEmulationBranchIfNeeded( querySpec, super::visitQuerySpec ) ) { + if ( !currentHelper.emulateFullJoinWithUnionIfNeeded( querySpec ) ) { + if ( shouldEmulateFetchClause( querySpec ) ) { + emulateFetchOffsetWithWindowFunctions( querySpec, true ); + } + else { + super.visitQuerySpec( querySpec ); + } + } + } + } + finally { + if ( needsNestedHelper ) { + fullJoinEmulations.pop(); + } + } + } + + @Override + public void visitSelectClause(SelectClause selectClause) { + if ( !currentFullJoinEmulationHelper().renderSelectClauseIfNeeded( selectClause ) ) { + super.visitSelectClause( selectClause ); } } @@ -267,11 +328,17 @@ protected void renderDerivedTableReferenceIdentificationVariable(DerivedTableRef @Override public void visitOffsetFetchClause(QueryPart queryPart) { - if ( !isRowNumberingCurrentQueryPart() ) { + if ( !currentFullJoinEmulationHelper().isFullJoinEmulationQueryPart( queryPart ) + && !isRowNumberingCurrentQueryPart() ) { renderCombinedLimitClause( queryPart ); } } + @Override + protected void visitOrderBy(List sortSpecifications) { + currentFullJoinEmulationHelper().renderOrderByIfNeeded( getCurrentQueryPart(), sortSpecifications, super::visitOrderBy ); + } + @Override protected void renderComparison(Expression lhs, ComparisonOperator operator, Expression rhs) { final JdbcMappingContainer lhsExpressionType = lhs.getExpressionType(); @@ -387,17 +454,6 @@ private boolean supportsWindowFunctions() { return true; } - @Override - public void visitCastTarget(CastTarget castTarget) { - String sqlType = MySQLSqlAstTranslator.getSqlType( castTarget, getSessionFactory() ); - if ( sqlType != null ) { - appendSql( sqlType ); - } - else { - super.visitCastTarget( castTarget ); - } - } - @Override protected void renderStringContainsExactlyPredicate(Expression haystack, Expression needle) { // MariaDB can't cope with NUL characters in the position function, so we use a like predicate instead @@ -416,7 +472,7 @@ INSERT INTO employees (id, name, salary, version) salary = values(salary) */ @Override - protected void renderUpdatevalue(ColumnValueBinding columnValueBinding) { + protected void renderUpdateValue(ColumnValueBinding columnValueBinding) { appendSql( "values(" ); appendSql( columnValueBinding.getColumnReference().getColumnExpression() ); appendSql( ")" ); @@ -430,4 +486,72 @@ protected void appendAssignmentColumn(ColumnReference column) { ? determineColumnReferenceQualifier( column ) : null ); } + + private boolean usesSingleTableDml(MutationStatement statement) { + // As of MariaDB 11.1, the self-join rewrite optimization can handle this, so no need force single table DML + return getDialect().getVersion().isBefore( 11, 1 ) && hasTargetTableCorrelation( statement ); + } + + private boolean needsDmlSubqueryWrapper() { + final Statement statement = getStatement(); + // As of MariaDB 11.1, the self-join rewrite optimization can handle this, so no need for the wrapper + return getDialect().getVersion().isBefore( 11, 1 ) + && statement instanceof AbstractUpdateOrDeleteStatement updateOrDeleteStatement + && !NestedOrTargetTableCorrelationVisitor.hasCorrelation( updateOrDeleteStatement ); + } + + @Override + public void visitSelectStatement(SelectStatement statement) { + final boolean needsParenthesis = !statement.getQueryPart().isRoot(); + if ( needsParenthesis && needsDmlSubqueryWrapper() ) { + appendSql( OPEN_PARENTHESIS ); + appendSql( "select * from " ); + super.visitSelectStatement( statement ); + appendSql( " _sub_" ); + appendSql( CLOSE_PARENTHESIS ); + } + else { + super.visitSelectStatement( statement ); + } + } + + @Override + protected void renderRelationalEmulationSubQuery( + QuerySpec subQuery, + X lhsTuple, + SubQueryRelationalRestrictionEmulationRenderer renderer, + ComparisonOperator tupleComparisonOperator) { + if ( needsDmlSubqueryWrapper() ) { + appendSql( OPEN_PARENTHESIS ); + appendSql( "select * from " ); + super.renderRelationalEmulationSubQuery( subQuery, lhsTuple, renderer, tupleComparisonOperator ); + appendSql( " _sub_" ); + appendSql( CLOSE_PARENTHESIS ); + } + else { + super.renderRelationalEmulationSubQuery( subQuery, lhsTuple, renderer, tupleComparisonOperator ); + } + } + + @Override + protected void renderQuantifiedEmulationSubQuery( + QuerySpec subQuery, + ComparisonOperator tupleComparisonOperator) { + if ( needsDmlSubqueryWrapper() ) { + appendSql( OPEN_PARENTHESIS ); + appendSql( "select * from " ); + super.renderQuantifiedEmulationSubQuery( subQuery, tupleComparisonOperator ); + appendSql( " _sub_" ); + appendSql( CLOSE_PARENTHESIS ); + } + else { + super.renderQuantifiedEmulationSubQuery( subQuery, tupleComparisonOperator ); + } + } + + @Override + protected void renderFetchFirstRow() { + appendSql( " limit 1" ); + } + } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/MySQLSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/MySQLSqlAstTranslator.java index f71014d744f8..c72eb552c616 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/MySQLSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/MySQLSqlAstTranslator.java @@ -4,10 +4,12 @@ */ package org.hibernate.dialect.sql.ast; -import org.hibernate.dialect.Dialect; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.List; + import org.hibernate.dialect.DmlTargetColumnQualifierSupport; import org.hibernate.dialect.MySQLDialect; -import org.hibernate.engine.jdbc.Size; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.internal.util.collections.Stack; import org.hibernate.query.sqm.ComparisonOperator; @@ -15,7 +17,6 @@ import org.hibernate.sql.ast.tree.Statement; import org.hibernate.sql.ast.tree.delete.DeleteStatement; import org.hibernate.sql.ast.tree.expression.BinaryArithmeticExpression; -import org.hibernate.sql.ast.tree.expression.CastTarget; import org.hibernate.sql.ast.tree.expression.ColumnReference; import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.Literal; @@ -32,94 +33,69 @@ import org.hibernate.sql.ast.tree.select.QueryGroup; import org.hibernate.sql.ast.tree.select.QueryPart; import org.hibernate.sql.ast.tree.select.QuerySpec; +import org.hibernate.sql.ast.tree.select.SelectClause; import org.hibernate.sql.ast.tree.select.SelectStatement; +import org.hibernate.sql.ast.tree.select.SortSpecification; import org.hibernate.sql.ast.tree.update.UpdateStatement; import org.hibernate.sql.exec.spi.JdbcOperation; import org.hibernate.sql.model.ast.ColumnValueBinding; +import org.hibernate.sql.ast.spi.FullJoinEmulation; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; /** * A SQL AST translator for MySQL. * * @author Christian Beikov + * @author Yoobin Yoon */ public class MySQLSqlAstTranslator extends SqlAstTranslatorWithOnDuplicateKeyUpdate { - /** - * On MySQL, 1GB or {@code 2^30 - 1} is the maximum size that a char value can be casted. - */ - private static final int MAX_CHAR_SIZE = (1 << 30) - 1; - private final MySQLDialect dialect; + private final ArrayDeque fullJoinEmulations = new ArrayDeque<>(); public MySQLSqlAstTranslator(SessionFactoryImplementor sessionFactory, Statement statement, MySQLDialect dialect) { super( sessionFactory, statement ); this.dialect = dialect; + this.fullJoinEmulations.push( new FullJoinEmulation( this ) ); + } + + private FullJoinEmulation currentFullJoinEmulationHelper() { + return fullJoinEmulations.getFirst(); } - public static String getSqlType(CastTarget castTarget, SessionFactoryImplementor factory) { - final String sqlType = getCastTypeName( castTarget, factory.getTypeConfiguration() ); - return getSqlType( castTarget, sqlType, factory.getJdbcServices().getDialect() ); - } - - //TODO: this is really, really bad since it circumvents the whole machinery we have in DdlType - // and in the Dialect for doing this in a unified way! These mappings should be held in - // the DdlTypes themselves and should be set up in registerColumnTypes(). Doing it here - // means we have problems distinguishing, say, the 'as Character' special case - private static String getSqlType(CastTarget castTarget, String sqlType, Dialect dialect) { - if ( sqlType != null ) { - int parenthesesIndex = sqlType.indexOf( '(' ); - final String baseName = parenthesesIndex == -1 ? sqlType : sqlType.substring( 0, parenthesesIndex ).trim(); - switch ( baseName.toLowerCase( Locale.ROOT ) ) { - case "bit": - return "unsigned"; - case "tinyint": - case "smallint": - case "integer": - case "bigint": - return "signed"; - case "float": - case "real": - case "double precision": - if ( ((MySQLDialect) dialect).getMySQLVersion().isSameOrAfter( 8, 0, 17 ) ) { - return sqlType; - } - final int precision = castTarget.getPrecision() == null - ? dialect.getDefaultDecimalPrecision() - : castTarget.getPrecision(); - final int scale = castTarget.getScale() == null ? Size.DEFAULT_SCALE : castTarget.getScale(); - return "decimal(" + precision + "," + scale + ")"; - case "char": - case "varchar": - case "nchar": - case "nvarchar": - case "text": - case "mediumtext": - case "longtext": - case "enum": - if ( castTarget.getLength() == null ) { - // TODO: this is ugly and fragile, but could easily be handled in a DdlType - if ( castTarget.getJdbcMapping().getJdbcJavaType().getJavaType() == Character.class ) { - return "char(1)"; - } - else { - return "char"; - } - } - return castTarget.getLength() > MAX_CHAR_SIZE ? "char" : "char(" + castTarget.getLength() + ")"; - case "binary": - case "varbinary": - case "mediumblob": - case "longblob": - return castTarget.getLength() == null - ? "binary" - : "binary(" + castTarget.getLength() + ")"; + @Override + public void visitQuerySpec(QuerySpec querySpec) { + final var helper = currentFullJoinEmulationHelper(); + final boolean needsNestedHelper = + helper.hasActiveFullJoinEmulation() + && !helper.isFullJoinEmulationQueryPart( querySpec ); + if ( needsNestedHelper ) { + fullJoinEmulations.push( new FullJoinEmulation( this ) ); + } + try { + final var currentHelper = currentFullJoinEmulationHelper(); + if ( !currentHelper.renderFullJoinEmulationBranchIfNeeded( querySpec, super::visitQuerySpec ) + && !currentHelper.emulateFullJoinWithUnionIfNeeded( querySpec ) ) { + if ( shouldEmulateFetchClause( querySpec ) ) { + emulateFetchOffsetWithWindowFunctions( querySpec, true ); + } + else { + super.visitQuerySpec( querySpec ); + } + } + } + finally { + if ( needsNestedHelper ) { + fullJoinEmulations.pop(); } } - return sqlType; + } + + @Override + public void visitSelectClause(SelectClause selectClause) { + if ( !currentFullJoinEmulationHelper().renderSelectClauseIfNeeded( selectClause ) ) { + super.visitSelectClause( selectClause ); + } } @Override @@ -292,8 +268,10 @@ public void visitBooleanExpressionPredicate(BooleanExpressionPredicate booleanEx protected boolean shouldEmulateFetchClause(QueryPart queryPart) { // Check if current query part is already row numbering to avoid infinite recursion - return useOffsetFetchClause( queryPart ) && getQueryPartForRowNumbering() != queryPart - && getDialect().supportsWindowFunctions() && !isRowsOnlyFetchClauseType( queryPart ); + return useOffsetFetchClause( queryPart ) + && getQueryPartForRowNumbering() != queryPart + && getDialect().supportsWindowFunctions() + && !isRowsOnlyFetchClauseType( queryPart ); } @Override @@ -306,16 +284,6 @@ public void visitQueryGroup(QueryGroup queryGroup) { } } - @Override - public void visitQuerySpec(QuerySpec querySpec) { - if ( shouldEmulateFetchClause( querySpec ) ) { - emulateFetchOffsetWithWindowFunctions( querySpec, true ); - } - else { - super.visitQuerySpec( querySpec ); - } - } - @Override public void visitValuesTableReference(ValuesTableReference tableReference) { emulateValuesTableReferenceColumnAliasing( tableReference ); @@ -332,9 +300,15 @@ protected void renderDerivedTableReference(DerivedTableReference tableReference) } } + @Override + protected void visitOrderBy(List sortSpecifications) { + currentFullJoinEmulationHelper().renderOrderByIfNeeded( getCurrentQueryPart(), sortSpecifications, super::visitOrderBy ); + } + @Override public void visitOffsetFetchClause(QueryPart queryPart) { - if ( !isRowNumberingCurrentQueryPart() ) { + if ( !currentFullJoinEmulationHelper().isFullJoinEmulationQueryPart( queryPart ) + && !isRowNumberingCurrentQueryPart() ) { renderCombinedLimitClause( queryPart ); } } @@ -413,17 +387,6 @@ public MySQLDialect getDialect() { return dialect; } - @Override - public void visitCastTarget(CastTarget castTarget) { - String sqlType = getSqlType( castTarget, getSessionFactory() ); - if ( sqlType != null ) { - appendSql( sqlType ); - } - else { - super.visitCastTarget( castTarget ); - } - } - @Override protected void renderStringContainsExactlyPredicate(Expression haystack, Expression needle) { // MySQL can't cope with NUL characters in the position function, so we use a like predicate instead @@ -449,7 +412,7 @@ protected void renderNewRowAlias() { } @Override - protected void renderUpdatevalue(ColumnValueBinding columnValueBinding) { + protected void renderUpdateValue(ColumnValueBinding columnValueBinding) { renderAlias(); appendSql( "." ); appendSql( columnValueBinding.getColumnReference().getColumnExpression() ); @@ -467,4 +430,63 @@ protected void appendAssignmentColumn(ColumnReference column) { ? determineColumnReferenceQualifier( column ) : null ); } + + private boolean needsDmlSubqueryWrapper() { + final Statement statement = getStatement(); + return statement instanceof DeleteStatement || statement instanceof UpdateStatement; + } + + @Override + public void visitSelectStatement(SelectStatement statement) { + final boolean needsParenthesis = !statement.getQueryPart().isRoot(); + if ( needsParenthesis && needsDmlSubqueryWrapper() ) { + appendSql( OPEN_PARENTHESIS ); + appendSql( "select * from " ); + super.visitSelectStatement( statement ); + appendSql( " _sub_" ); + appendSql( CLOSE_PARENTHESIS ); + } + else { + super.visitSelectStatement( statement ); + } + } + + @Override + protected void renderRelationalEmulationSubQuery( + QuerySpec subQuery, + X lhsTuple, + SubQueryRelationalRestrictionEmulationRenderer renderer, + ComparisonOperator tupleComparisonOperator) { + if ( needsDmlSubqueryWrapper() ) { + appendSql( OPEN_PARENTHESIS ); + appendSql( "select * from " ); + super.renderRelationalEmulationSubQuery( subQuery, lhsTuple, renderer, tupleComparisonOperator ); + appendSql( " _sub_" ); + appendSql( CLOSE_PARENTHESIS ); + } + else { + super.renderRelationalEmulationSubQuery( subQuery, lhsTuple, renderer, tupleComparisonOperator ); + } + } + + @Override + protected void renderQuantifiedEmulationSubQuery( + QuerySpec subQuery, + ComparisonOperator tupleComparisonOperator) { + if ( needsDmlSubqueryWrapper() ) { + appendSql( OPEN_PARENTHESIS ); + appendSql( "select * from " ); + super.renderQuantifiedEmulationSubQuery( subQuery, tupleComparisonOperator ); + appendSql( " _sub_" ); + appendSql( CLOSE_PARENTHESIS ); + } + else { + super.renderQuantifiedEmulationSubQuery( subQuery, tupleComparisonOperator ); + } + } + + @Override + protected void renderFetchFirstRow() { + appendSql( " limit 1" ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/PostgreSQLSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/PostgreSQLSqlAstTranslator.java index da605ae88d61..fef96801d12b 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/PostgreSQLSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/PostgreSQLSqlAstTranslator.java @@ -32,6 +32,7 @@ import org.hibernate.sql.exec.internal.JdbcOperationQueryInsertImpl; import org.hibernate.sql.exec.spi.JdbcOperation; import org.hibernate.sql.exec.spi.JdbcOperationQueryInsert; +import org.hibernate.sql.model.internal.OptionalTableInsert; import org.hibernate.sql.model.internal.TableInsertStandard; import org.hibernate.type.SqlTypes; @@ -59,6 +60,39 @@ protected String getArrayContainsFunction() { return super.getArrayContainsFunction(); } + @Override + public void visitStandardTableInsert(TableInsertStandard tableInsert) { + getCurrentClauseStack().push( Clause.INSERT ); + try { + renderInsertInto( tableInsert ); + if ( tableInsert instanceof OptionalTableInsert optionalTableInsert ) { + appendSql( " on conflict " ); + final String constraintName = optionalTableInsert.getConstraintName(); + if ( constraintName != null ) { + appendSql( " on constraint " ); + appendSql( constraintName ); + } + else { + char separator = '('; + for ( String constraintColumnName : optionalTableInsert.getConstraintColumnNames() ) { + appendSql( separator ); + appendSql( constraintColumnName ); + separator = ','; + } + appendSql( ')' ); + } + appendSql( " do nothing" ); + } + + if ( tableInsert.getNumberOfReturningColumns() > 0 ) { + visitReturningColumns( tableInsert::getReturningColumns ); + } + } + finally { + getCurrentClauseStack().pop(); + } + } + @Override protected void renderInsertIntoNoColumns(TableInsertStandard tableInsert) { renderIntoIntoAndTable( tableInsert ); @@ -259,23 +293,11 @@ else if ( expression instanceof Summarization summarization ) { } @Override - public void visitLikePredicate(LikePredicate likePredicate) { + protected void renderLikePredicate(LikePredicate likePredicate) { // We need a custom implementation here because PostgreSQL // uses the backslash character as default escape character // According to the documentation, we can overcome this by specifying an empty escape character // See https://www.postgresql.org/docs/current/functions-matching.html#FUNCTIONS-LIKE - likePredicate.getMatchExpression().accept( this ); - if ( likePredicate.isNegated() ) { - appendSql( " not" ); - } - if ( likePredicate.isCaseSensitive() ) { - appendSql( " like " ); - } - else { - appendSql( WHITESPACE ); - appendSql( getDialect().getCaseInsensitiveLike() ); - appendSql( WHITESPACE ); - } likePredicate.getPattern().accept( this ); if ( likePredicate.getEscapeCharacter() != null ) { appendSql( " escape " ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/SpannerSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/SpannerSqlAstTranslator.java index 2a46627baa6e..59a9ccc769f4 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/SpannerSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/SpannerSqlAstTranslator.java @@ -4,24 +4,52 @@ */ package org.hibernate.dialect.sql.ast; +import java.math.BigDecimal; +import java.math.BigInteger; import java.util.List; -import org.hibernate.Locking; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.sql.ast.Clause; import org.hibernate.sql.ast.spi.AbstractSqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; 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.delete.DeleteStatement; +import org.hibernate.sql.ast.tree.expression.BinaryArithmeticExpression; +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; import org.hibernate.sql.ast.tree.expression.Summarization; +import org.hibernate.sql.ast.tree.expression.UnparsedNumericLiteral; import org.hibernate.sql.ast.tree.from.DerivedTableReference; +import org.hibernate.sql.ast.tree.from.NamedTableReference; +import org.hibernate.sql.ast.tree.from.QueryPartTableReference; +import org.hibernate.sql.ast.tree.from.TableReference; +import org.hibernate.sql.ast.tree.insert.ConflictClause; +import org.hibernate.sql.ast.tree.predicate.InArrayPredicate; +import org.hibernate.sql.ast.tree.predicate.LikePredicate; import org.hibernate.sql.ast.tree.select.QueryPart; import org.hibernate.sql.ast.tree.select.QuerySpec; import org.hibernate.sql.ast.tree.select.SelectClause; +import org.hibernate.sql.ast.tree.expression.Any; +import org.hibernate.sql.ast.tree.expression.Every; +import org.hibernate.sql.ast.tree.expression.ModifiedSubQueryExpression; +import org.hibernate.sql.ast.tree.select.SelectStatement; +import org.hibernate.sql.ast.tree.update.UpdateStatement; import org.hibernate.sql.exec.spi.JdbcOperation; +import org.hibernate.sql.model.MutationOperation; +import org.hibernate.sql.model.ast.RestrictedTableMutation; +import org.hibernate.sql.model.internal.OptionalTableUpdate; +import org.hibernate.sql.model.internal.TableUpdateStandard; +import org.hibernate.sql.ast.spi.SqlAliasStemHelper; +import org.hibernate.sql.model.jdbc.UpsertOperation; +import org.hibernate.jdbc.Expectation; +import org.hibernate.sql.model.jdbc.DeleteOrUpsertOperation; +import org.hibernate.persister.entity.mutation.EntityTableMapping; +import org.hibernate.sql.model.ast.ColumnValueBinding; /** * A SQL AST translator for Spanner. @@ -37,13 +65,6 @@ public SpannerSqlAstTranslator(SessionFactoryImplementor sessionFactory, Stateme super( sessionFactory, statement ); } - @Override - protected LockStrategy determineLockingStrategy( - QuerySpec querySpec, - Locking.FollowOn followOnLocking) { - return LockStrategy.NONE; - } - @Override public void visitOffsetFetchClause(QueryPart queryPart) { renderLimitOffsetClause( queryPart ); @@ -51,7 +72,48 @@ public void visitOffsetFetchClause(QueryPart queryPart) { @Override protected void renderComparison(Expression lhs, ComparisonOperator operator, Expression rhs) { - renderComparisonEmulateIntersect( lhs, operator, rhs ); + if ( rhs instanceof Every || rhs instanceof Any ) { + final boolean all = rhs instanceof Every; + final SelectStatement subquery = all ? ( (Every) rhs ).getSubquery() : ( (Any) rhs ).getSubquery(); + + final AbstractSqlAstTranslator.SubQueryRelationalRestrictionEmulationRenderer singleRenderer = (lhsSelections, singleExpr, op) -> { + lhsSelections.get( 0 ).getExpression().accept( this ); + appendSql( op.invert().sqlText() ); + singleExpr.accept( this ); + }; + + emulateSubQueryRelationalRestrictionPredicate( + null, + all, + subquery, + lhs, + singleRenderer, + all ? operator.negated() : operator + ); + } + else if ( rhs instanceof ModifiedSubQueryExpression expression ) { + SelectStatement subquery = expression.getSubQuery(); + if ( subquery.getQueryPart() instanceof QuerySpec querySpec ) { + if ( operator != ComparisonOperator.NOT_EQUAL && operator != ComparisonOperator.NOT_DISTINCT_FROM ) { + if ( expression.getModifier() == ModifiedSubQueryExpression.Modifier.ALL ) { + // Emulate ALL + lhs.accept( this ); + appendSql( operator.sqlText() ); + renderQuantifiedEmulationSubQuery( querySpec, operator ); + } + else if ( expression.getModifier() == ModifiedSubQueryExpression.Modifier.ANY || + expression.getModifier() == ModifiedSubQueryExpression.Modifier.SOME ) { + // Emulate ANY + lhs.accept( this ); + appendSql( operator.sqlText() ); + renderQuantifiedEmulationSubQuery( querySpec, operator.invert() ); + } + } + } + } + else { + renderComparisonEmulateIntersect( lhs, operator, rhs ); + } } @Override @@ -62,6 +124,11 @@ protected void renderSelectTupleComparison( emulateSelectTupleComparison( lhsExpressions, tuple.getExpressions(), operator, true ); } + @Override + protected void renderFetchFirstRow() { + appendSql( " limit 1" ); + } + @Override protected void renderPartitionItem(Expression expression) { if ( expression instanceof Literal ) { @@ -109,7 +176,349 @@ protected void renderDerivedTableReference(DerivedTableReference tableReference) if ( correlated ) { this.correlated = oldCorrelated; appendSql( CLOSE_PARENTHESIS ); + // Spanner requires the alias to be outside the parentheses UNNEST(... ) alias + super.renderTableReferenceIdentificationVariable( tableReference ); + } + } + + @Override + protected void visitDeleteStatementOnly(DeleteStatement statement) { + // Spanner requires a WHERE in delete clause so we add "where true" if there is none + if ( !hasWhere( statement.getRestriction() ) ) { + renderDeleteClause( statement ); + appendSql( " where true" ); + visitReturningColumns( statement.getReturningColumns() ); + } + else { + super.visitDeleteStatementOnly( statement ); + } + } + + @Override + protected void visitUpdateStatementOnly(UpdateStatement statement) { + // Spanner requires a WHERE in update clause so we add "where true" if there is none + if ( !hasWhere( statement.getRestriction() ) ) { + renderUpdateClause( statement ); + renderSetClause( statement.getAssignments() ); + appendSql( " where true" ); + visitReturningColumns( statement.getReturningColumns() ); + } + else { + super.visitUpdateStatementOnly( statement ); + } + } + + @Override + public void visitStandardTableUpdate(TableUpdateStandard tableUpdate) { + getClauseStack().push( Clause.UPDATE ); + try { + visitSpannerTableUpdate( tableUpdate ); + if ( tableUpdate.getWhereFragment() != null ) { + appendSql( " and (" ); + appendSql( tableUpdate.getWhereFragment() ); + appendSql( ")" ); + } + + if ( tableUpdate.getNumberOfReturningColumns() > 0 ) { + visitReturningColumns( tableUpdate::getReturningColumns ); + } + } + finally { + getClauseStack().pop(); + } + } + + @Override + public void visitOptionalTableUpdate(OptionalTableUpdate tableUpdate) { + getClauseStack().push( Clause.UPDATE ); + try { + visitSpannerTableUpdate( tableUpdate ); + } + finally { + getClauseStack().pop(); + } + } + + /** + * Spanner requires table aliasing in UPDATE statements to disambiguate table and column names + * if they are identical (e.g., table 'Discount' and column 'discount'). + * This method overrides standard Hibernate rendering to inject generated aliases. + */ + private void visitSpannerTableUpdate(RestrictedTableMutation tableUpdate) { + applySqlComment( tableUpdate.getMutationComment() ); + final String stem = SqlAliasStemHelper.INSTANCE.generateStemFromEntityName( tableUpdate.getMutatingTable().getTableName() ); + final String alias = stem + "1_0"; + + appendSql( "update " ); + appendSql( tableUpdate.getMutatingTable().getTableName() ); + appendSql( " " + alias ); + registerAffectedTable( tableUpdate.getMutatingTable().getTableName() ); + + getClauseStack().push( Clause.SET ); + try { + appendSql( " set" ); + tableUpdate.forEachValueBinding( (columnPosition, columnValueBinding) -> { + if ( columnPosition == 0 ) { + appendSql( " " ); + } + else { + appendSql( "," ); + } + appendSql( alias + "." ); + appendSql( columnValueBinding.getColumnReference().getColumnExpression() ); + appendSql( "=" ); + columnValueBinding.getValueExpression().accept( this ); + } ); + } + finally { + getClauseStack().pop(); + } + + getClauseStack().push( Clause.WHERE ); + try { + appendSql( " where" ); + tableUpdate.forEachKeyBinding( (position, columnValueBinding) -> { + if ( position == 0 ) { + appendSql( " " ); + } + else { + appendSql( " and " ); + } + appendSql( alias + "." ); + appendSql( columnValueBinding.getColumnReference().getColumnExpression() ); + appendSql( "=" ); + columnValueBinding.getValueExpression().accept( this ); + } ); + + if ( tableUpdate.getNumberOfOptimisticLockBindings() > 0 ) { + tableUpdate.forEachOptimisticLockBinding( (position, columnValueBinding) -> { + appendSql( " and " ); + appendSql( alias + "." ); + appendSql( columnValueBinding.getColumnReference().getColumnExpression() ); + if ( columnValueBinding.getValueExpression() == null + || columnValueBinding.getValueExpression().getFragment() == null ) { + appendSql( " is null" ); + } + else { + appendSql( "=" ); + columnValueBinding.getValueExpression().accept( this ); + } + } ); + } + } + finally { + getClauseStack().pop(); + } + } + + private void applySqlComment(String comment) { + if ( getSessionFactory().getSessionFactoryOptions().isCommentsEnabled() ) { + if ( comment != null ) { + appendSql( "/* " ); + appendSql( org.hibernate.dialect.Dialect.escapeComment( comment ) ); + appendSql( " */" ); + } + } + } + + @Override + protected void renderTableReferenceIdentificationVariable(TableReference tableReference) { + // Spanner requires `UNNEST(...) alias`. Standard rendering places the alias + // inside the parentheses UNNEST(... alias). We suppress it here to manually + // render it outside the UNNEST wrapper in `renderDerivedTableReference`. + if ( correlated + && tableReference instanceof DerivedTableReference + && ((DerivedTableReference) tableReference).isLateral() ) { + return; } + super.renderTableReferenceIdentificationVariable( tableReference ); } + @Override + protected void renderDmlTargetTableExpression(NamedTableReference tableReference) { + super.renderDmlTargetTableExpression( tableReference ); + if ( getClauseStack().getCurrent() != Clause.INSERT ) { + renderTableReferenceIdentificationVariable( tableReference ); + } + } + + @Override + protected void renderDerivedTableReferenceIdentificationVariable(DerivedTableReference tableReference) { + renderTableReferenceIdentificationVariable( tableReference ); + } + + @Override + public void visitQueryPartTableReference(QueryPartTableReference tableReference) { + emulateQueryPartTableReferenceColumnAliasing( tableReference ); + } + + @Override + public void visitInArrayPredicate(InArrayPredicate inArrayPredicate) { + inArrayPredicate.getTestExpression().accept( this ); + appendSql( " in unnest(" ); + inArrayPredicate.getArrayParameter().accept( this ); + appendSql( ')' ); + } + + @Override + protected void renderLikePredicate(LikePredicate likePredicate) { + // Spanner uses the backslash character as the default escape character + if (likePredicate.getEscapeCharacter() == null) { + renderBackslashEscapedLikePattern( likePredicate.getPattern(), likePredicate.getEscapeCharacter(), false ); + } + else { + renderLikePattern( likePredicate.getPattern(), likePredicate.getEscapeCharacter() ); + } + } + + @Override + protected void renderLikePattern(Expression pattern, Expression escapeCharacter) { + if (escapeCharacter == null) { + super.renderLikePattern( pattern, escapeCharacter ); + } + else { + appendSql( "replace(replace(replace(" ); + pattern.accept( this ); + appendSql( ", " ); + escapeCharacter.accept( this ); + appendSql( "||" ); + escapeCharacter.accept( this ); + appendSql( ", '\\\\\\\\'), " ); + escapeCharacter.accept( this ); + appendSql( "||'%', '\\\\%'), " ); + escapeCharacter.accept( this ); + appendSql( "||'_', '\\\\_')" ); + } + } + + @Override + protected void renderEscapeCharacter(Expression escapeCharacter) { + // Spanner doesn't support passing escape character + } + + @Override + protected void appendBackslashEscapedLikeLiteral(SqlAppender appender, String literal, boolean noBackslashEscapes) { + appender.appendSql( '\'' ); + for ( int i = 0; i < literal.length(); i++ ) { + final char c = literal.charAt( i ); + switch ( c ) { + case '\'': + appender.appendSql( '\\' ); + break; + case '\\': + appender.appendSql( "\\\\\\" ); + break; + } + appender.appendSql( c ); + } + appender.appendSql( '\'' ); + } + + @Override + public void visitBinaryArithmeticExpression(BinaryArithmeticExpression arithmeticExpression) { + if ( isIntegerDivisionEmulationRequired( arithmeticExpression ) ) { + // Spanner uses functional syntax: DIV(numerator, denominator) + appendSql( "div(" ); + visitArithmeticOperand( arithmeticExpression.getLeftHandOperand() ); + appendSql( "," ); + visitArithmeticOperand( arithmeticExpression.getRightHandOperand() ); + appendSql( ")" ); + } + else { + super.visitBinaryArithmeticExpression( arithmeticExpression ); + } + } + + @Override + public void visitUnparsedNumericLiteral(UnparsedNumericLiteral literal) { + final Class javaTypeClass = literal.getJdbcMapping().getJavaTypeDescriptor().getJavaTypeClass(); + if ( BigDecimal.class.isAssignableFrom( javaTypeClass ) + || BigInteger.class.isAssignableFrom( javaTypeClass ) ) { + appendSql( "NUMERIC '" ); + appendSql( literal.getUnparsedLiteralValue() ); + appendSql( "'" ); + } + else { + super.visitUnparsedNumericLiteral( literal ); + } + } + + @Override + protected void visitConflictClause(ConflictClause conflictClause) { + visitStandardConflictClause( conflictClause ); + } + + @Override + protected String determineColumnReferenceQualifier(ColumnReference columnReference) { + // Cloud Spanner does not support target table aliases in INSERT statements, + // so we must qualify column references with the actual table name to avoid ambiguity. + if ( getClauseStack().findCurrentFirst( clause -> clause == Clause.CONFLICT ? Boolean.TRUE : null ) != null ) { + if ( !"excluded".equalsIgnoreCase( columnReference.getQualifier() ) ) { + final MutationStatement currentDmlStatement = getCurrentDmlStatement(); + if ( currentDmlStatement != null && currentDmlStatement.getTargetTable() != null ) { + return currentDmlStatement.getTargetTable().getTableExpression(); + } + } + } + return super.determineColumnReferenceQualifier( columnReference ); + } + + public MutationOperation createMergeOperation(OptionalTableUpdate optionalTableUpdate, boolean hasUpdatableBindings) { + renderInsertOrUpdate( optionalTableUpdate, hasUpdatableBindings ); + + final UpsertOperation upsertOperation = new UpsertOperation( + optionalTableUpdate.getMutatingTable().getTableMapping(), + optionalTableUpdate.getMutationTarget(), + getSql(), + hasUpdatableBindings ? new Expectation.RowCount() : new Expectation.OptionalRowCount(), + getParameterBinders() + ); + + return new DeleteOrUpsertOperation( + optionalTableUpdate.getMutationTarget(), + (EntityTableMapping) optionalTableUpdate.getMutatingTable().getTableMapping(), + upsertOperation, + optionalTableUpdate + ); + } + + protected void renderInsertOrUpdate(OptionalTableUpdate optionalTableUpdate, boolean hasUpdatableBindings) { + if ( hasUpdatableBindings ) { + appendSql( "insert or update into " ); + } + else { + appendSql( "insert or ignore into " ); + } + + appendSql( optionalTableUpdate.getMutatingTable().getTableName() ); + appendSql( " (" ); + + final List keyBindings = optionalTableUpdate.getKeyBindings(); + char separator = ' '; + for ( ColumnValueBinding keyBinding : keyBindings ) { + appendSql( separator ); + appendSql( keyBinding.getColumnReference().getColumnExpression() ); + separator = ','; + } + + optionalTableUpdate.forEachValueBinding( (columnPosition, columnValueBinding) -> { + appendSql( ',' ); + appendSql( columnValueBinding.getColumnReference().getColumnExpression() ); + } ); + + appendSql( ") values (" ); + + separator = ' '; + for ( ColumnValueBinding keyBinding : keyBindings ) { + appendSql( separator ); + keyBinding.getValueExpression().accept( this ); + separator = ','; + } + + optionalTableUpdate.forEachValueBinding( (columnPosition, columnValueBinding) -> { + appendSql( ',' ); + columnValueBinding.getValueExpression().accept( this ); + } ); + appendSql( ") " ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/SqlAstTranslatorWithOnDuplicateKeyUpdate.java b/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/SqlAstTranslatorWithOnDuplicateKeyUpdate.java index 73fab31d26ca..c842455068a8 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/SqlAstTranslatorWithOnDuplicateKeyUpdate.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/SqlAstTranslatorWithOnDuplicateKeyUpdate.java @@ -5,8 +5,9 @@ package org.hibernate.dialect.sql.ast; -import org.hibernate.dialect.MySQLDeleteOrUpsertOperation; +import org.hibernate.StaleStateException; import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.jdbc.Expectation; import org.hibernate.persister.entity.mutation.EntityTableMapping; import org.hibernate.sql.ast.spi.SqlAstTranslatorWithUpsert; import org.hibernate.sql.ast.tree.Statement; @@ -14,9 +15,12 @@ import org.hibernate.sql.model.MutationOperation; import org.hibernate.sql.model.ast.ColumnValueBinding; import org.hibernate.sql.model.internal.OptionalTableUpdate; +import org.hibernate.sql.model.jdbc.DeleteOrUpsertOperation; import org.hibernate.sql.model.jdbc.UpsertOperation; +import java.sql.PreparedStatement; import java.util.List; +import java.util.function.BiConsumer; /** * @author Jan Schatteman @@ -37,10 +41,11 @@ public MutationOperation createMergeOperation(OptionalTableUpdate optionalTableU optionalTableUpdate.getMutatingTable().getTableMapping(), optionalTableUpdate.getMutationTarget(), getSql(), + new MySQLRowCountExpectation(), getParameterBinders() ); - return new MySQLDeleteOrUpsertOperation( + return new DeleteOrUpsertOperation( optionalTableUpdate.getMutationTarget(), (EntityTableMapping) optionalTableUpdate.getMutatingTable().getTableMapping(), upsertOperation, @@ -48,6 +53,19 @@ public MutationOperation createMergeOperation(OptionalTableUpdate optionalTableU ); } + private static class MySQLRowCountExpectation implements Expectation { + @Override + public final void verifyOutcome(int rowCount, PreparedStatement statement, int batchPosition, String sql) { + if ( rowCount > 2 ) { + throw new StaleStateException( + "Unexpected row count" + + " (the expected row count for an ON DUPLICATE KEY UPDATE statement should be either 0, 1 or 2 )" + + " [" + sql + "]" + ); + } + } + } + @Override protected void renderUpsertStatement(OptionalTableUpdate optionalTableUpdate) { renderInsertInto( optionalTableUpdate ); @@ -56,38 +74,48 @@ protected void renderUpsertStatement(OptionalTableUpdate optionalTableUpdate) { } protected void renderInsertInto(OptionalTableUpdate optionalTableUpdate) { - appendSql( "insert into " ); + if ( optionalTableUpdate.getValueBindings().isEmpty() ) { + appendSql( "insert ignore into " ); + } + else { + appendSql( "insert into " ); + } appendSql( optionalTableUpdate.getMutatingTable().getTableName() ); - appendSql( " (" ); + appendSql( " " ); final List keyBindings = optionalTableUpdate.getKeyBindings(); + char separator = '('; for ( ColumnValueBinding keyBinding : keyBindings ) { + appendSql( separator ); appendSql( keyBinding.getColumnReference().getColumnExpression() ); - appendSql( ',' ); + separator = ','; } optionalTableUpdate.forEachValueBinding( (columnPosition, columnValueBinding) -> { - appendSql( columnValueBinding.getColumnReference().getColumnExpression() ); - if ( columnPosition != optionalTableUpdate.getValueBindings().size() - 1 ) { - appendSql( ',' ); - } + appendSql( ',' ); + appendSql( columnValueBinding.getColumnReference().getColumnExpression() ); } ); - appendSql( ") values (" ); + appendSql( ") values " ); + separator = '('; for ( ColumnValueBinding keyBinding : keyBindings ) { + appendSql( separator ); keyBinding.getValueExpression().accept( this ); - appendSql( ',' ); + separator = ','; } optionalTableUpdate.forEachValueBinding( (columnPosition, columnValueBinding) -> { - if ( columnPosition > 0 ) { + if ( columnValueBinding.isAttributeInsertable() ) { appendSql( ',' ); + columnValueBinding.getValueExpression().accept( this ); } - columnValueBinding.getValueExpression().accept( this ); } ); appendSql(") "); - renderNewRowAlias(); + if ( optionalTableUpdate.getValueBindings().stream() + .anyMatch( ColumnValueBinding::isAttributeUpdatable ) ) { + renderNewRowAlias(); + } } protected void renderNewRowAlias() { @@ -95,18 +123,39 @@ protected void renderNewRowAlias() { protected void renderOnDuplicateKeyUpdate(OptionalTableUpdate optionalTableUpdate) { appendSql( "on duplicate key update " ); - optionalTableUpdate.forEachValueBinding( (columnPosition, columnValueBinding) -> { - final String columnName = columnValueBinding.getColumnReference().getColumnExpression(); - if ( columnPosition > 0 ) { - appendSql( ',' ); + if ( optionalTableUpdate.getValueBindings().stream() + .anyMatch( ColumnValueBinding::isAttributeUpdatable ) ) { + class BindingProcessor implements BiConsumer { + boolean first = true; + @Override + public void accept(Integer columnPosition, ColumnValueBinding columnValueBinding) { + if ( columnValueBinding.isAttributeUpdatable() ) { + final String columnName = columnValueBinding.getColumnReference().getColumnExpression(); + if ( first ) { + first = false; + } + else { + appendSql( ',' ); + } + appendSql( columnName ); + append( " = " ); + renderUpdateValue( columnValueBinding ); + } + } } - appendSql( columnName ); - append( " = " ); - renderUpdatevalue( columnValueBinding ); - } ); + optionalTableUpdate.forEachValueBinding( new BindingProcessor() ); + } + else { + final String keyColName = + optionalTableUpdate.getKeyBindings().get( 0 ) + .getColumnReference().getColumnExpression(); + appendSql( keyColName ); + appendSql( "=" ); + appendSql( keyColName ); + } } - protected void renderUpdatevalue(ColumnValueBinding columnValueBinding) { + protected void renderUpdateValue(ColumnValueBinding columnValueBinding) { } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/SybaseASESqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/SybaseASESqlAstTranslator.java index 62c902b1f196..a155047caa9e 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/SybaseASESqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/SybaseASESqlAstTranslator.java @@ -4,6 +4,10 @@ */ package org.hibernate.dialect.sql.ast; +import java.util.ArrayDeque; +import java.util.List; +import java.util.function.Consumer; + import org.hibernate.Internal; import org.hibernate.LockMode; import org.hibernate.Locking; @@ -17,6 +21,7 @@ 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.FullJoinEmulation; import org.hibernate.sql.ast.spi.SqlSelection; import org.hibernate.sql.ast.tree.MutationStatement; import org.hibernate.sql.ast.tree.Statement; @@ -40,13 +45,11 @@ import org.hibernate.sql.ast.tree.select.QueryPart; import org.hibernate.sql.ast.tree.select.QuerySpec; import org.hibernate.sql.ast.tree.select.SelectClause; +import org.hibernate.sql.ast.tree.select.SortSpecification; import org.hibernate.sql.ast.tree.update.UpdateStatement; import org.hibernate.sql.exec.spi.JdbcOperation; import org.hibernate.type.SqlTypes; -import java.util.List; -import java.util.function.Consumer; - import static org.hibernate.Timeouts.SKIP_LOCKED_MILLI; /** @@ -57,9 +60,15 @@ public class SybaseASESqlAstTranslator extends AbstractSqlAstTranslator { private static final String UNION_ALL = " union all "; + private final ArrayDeque fullJoinEmulations = new ArrayDeque<>(); public SybaseASESqlAstTranslator(SessionFactoryImplementor sessionFactory, Statement statement) { super( sessionFactory, statement ); + this.fullJoinEmulations.push( new FullJoinEmulation( this ) ); + } + + private FullJoinEmulation currentFullJoinEmulationHelper() { + return fullJoinEmulations.getFirst(); } @Override @@ -247,6 +256,36 @@ protected void renderFetchPlusOffsetExpression( renderFetchPlusOffsetExpressionAsLiteral( fetchClauseExpression, offsetClauseExpression, offset ); } + @Override + public void visitQuerySpec(QuerySpec querySpec) { + final var helper = currentFullJoinEmulationHelper(); + final boolean needsNestedHelper = + helper.hasActiveFullJoinEmulation() + && !helper.isFullJoinEmulationQueryPart( querySpec ); + if ( needsNestedHelper ) { + fullJoinEmulations.push( new FullJoinEmulation( this ) ); + } + try { + final var currentHelper = currentFullJoinEmulationHelper(); + if ( !currentHelper.renderFullJoinEmulationBranchIfNeeded( querySpec, super::visitQuerySpec ) + && !currentHelper.emulateFullJoinWithUnionIfNeeded( querySpec ) ) { + super.visitQuerySpec( querySpec ); + } + } + finally { + if ( needsNestedHelper ) { + fullJoinEmulations.pop(); + } + } + } + + @Override + public void visitSelectClause(SelectClause selectClause) { + if ( !currentFullJoinEmulationHelper().renderSelectClauseIfNeeded( selectClause ) ) { + super.visitSelectClause( selectClause ); + } + } + @Override public void visitQueryGroup(QueryGroup queryGroup) { if ( queryGroup.hasSortSpecifications() || queryGroup.hasOffsetOrFetchClause() ) { @@ -262,8 +301,8 @@ public void visitQueryGroup(QueryGroup queryGroup) { renderQueryGroup( queryGroup, false ); appendSql( ") grp_(c0" ); // Sybase doesn't have implicit names for non-column select expressions, so we need to assign names - final int itemCount = queryGroup.getFirstQuerySpec().getSelectClause().getSqlSelections().size(); - for (int i = 1; i < itemCount; i++) { + final int itemCount = assignNamesToSelectItems( queryGroup ); + for ( int i = 1; i < itemCount; i++ ) { appendSql( ",c" ); appendSql( i ); } @@ -275,6 +314,27 @@ public void visitQueryGroup(QueryGroup queryGroup) { } } + private int assignNamesToSelectItems(QueryGroup queryGroup) { + int itemCount = + currentFullJoinEmulationHelper() + .countRenderedSelectItemsIncludingEmulationSelections( + queryGroup.getFirstQuerySpec() ); + final var sortSpecifications = queryGroup.getSortSpecifications(); + if ( sortSpecifications != null ) { + for ( var sortSpecification : sortSpecifications ) { + final int[] sortSelectionIndexes = sortSpecification.getSortSelectionIndexes(); + if ( sortSelectionIndexes != null ) { + for ( int sortSelectionIndex : sortSelectionIndexes ) { + if ( sortSelectionIndex >= 0 && sortSelectionIndex + 1 > itemCount ) { + itemCount = sortSelectionIndex + 1; + } + } + } + } + } + return itemCount; + } + @Override protected void visitValuesList(List valuesList) { visitValuesListEmulateSelectUnion( valuesList ); @@ -290,14 +350,21 @@ public void visitValuesTableReference(ValuesTableReference tableReference) { @Override public void visitOffsetFetchClause(QueryPart queryPart) { - assertRowsOnlyFetchClauseType( queryPart ); - if ( !queryPart.isRoot() && queryPart.hasOffsetOrFetchClause() ) { - if ( queryPart.getFetchClauseExpression() != null && queryPart.getOffsetClauseExpression() != null ) { - throw new IllegalArgumentException( "Can't emulate offset fetch clause in subquery" ); + if ( !currentFullJoinEmulationHelper().isFullJoinEmulationQueryPart( queryPart ) ) { + assertRowsOnlyFetchClauseType( queryPart ); + if ( !queryPart.isRoot() && queryPart.hasOffsetOrFetchClause() ) { + if ( queryPart.getFetchClauseExpression() != null && queryPart.getOffsetClauseExpression() != null ) { + throw new IllegalArgumentException( "Can't emulate offset fetch clause in subquery" ); + } } } } + @Override + protected void visitOrderBy(List sortSpecifications) { + currentFullJoinEmulationHelper().renderOrderByIfNeeded( getCurrentQueryPart(), sortSpecifications, super::visitOrderBy ); + } + @Override protected void renderFetchExpression(Expression fetchExpression) { if ( supportsParameterOffsetFetchExpression() ) { diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/SybaseSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/SybaseSqlAstTranslator.java index 7afd01da5c41..e14eb19ffa08 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/SybaseSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/SybaseSqlAstTranslator.java @@ -4,6 +4,7 @@ */ package org.hibernate.dialect.sql.ast; +import java.util.ArrayDeque; import java.util.List; import java.util.function.Consumer; @@ -17,6 +18,7 @@ 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.FullJoinEmulation; import org.hibernate.sql.ast.spi.SqlSelection; import org.hibernate.sql.ast.tree.Statement; import org.hibernate.sql.ast.tree.delete.DeleteStatement; @@ -35,6 +37,8 @@ import org.hibernate.sql.ast.tree.insert.Values; import org.hibernate.sql.ast.tree.select.QueryPart; import org.hibernate.sql.ast.tree.select.QuerySpec; +import org.hibernate.sql.ast.tree.select.SelectClause; +import org.hibernate.sql.ast.tree.select.SortSpecification; import org.hibernate.sql.ast.tree.update.UpdateStatement; import org.hibernate.sql.exec.spi.JdbcOperation; @@ -46,9 +50,15 @@ public class SybaseSqlAstTranslator extends AbstractSqlAstTranslator { private static final String UNION_ALL = " union all "; + private final ArrayDeque fullJoinEmulations = new ArrayDeque<>(); public SybaseSqlAstTranslator(SessionFactoryImplementor sessionFactory, Statement statement) { super( sessionFactory, statement ); + this.fullJoinEmulations.push( new FullJoinEmulation( this ) ); + } + + private FullJoinEmulation currentFullJoinEmulationHelper() { + return fullJoinEmulations.getFirst(); } @Override @@ -99,7 +109,8 @@ protected void visitConflictClause(ConflictClause conflictClause) { protected void visitAnsiCaseSearchedExpression( CaseSearchedExpression caseSearchedExpression, Consumer resultRenderer) { - if ( getParameterRenderingMode() == SqlAstNodeRenderingMode.DEFAULT && areAllResultsParameters( caseSearchedExpression ) ) { + if ( getParameterRenderingMode() == SqlAstNodeRenderingMode.DEFAULT + && areAllResultsParameters( caseSearchedExpression ) ) { final List whenFragments = caseSearchedExpression.getWhenFragments(); final Expression firstResult = whenFragments.get( 0 ).getResult(); super.visitAnsiCaseSearchedExpression( @@ -123,7 +134,8 @@ protected void visitAnsiCaseSearchedExpression( protected void visitAnsiCaseSimpleExpression( CaseSimpleExpression caseSimpleExpression, Consumer resultRenderer) { - if ( getParameterRenderingMode() == SqlAstNodeRenderingMode.DEFAULT && areAllResultsParameters( caseSimpleExpression ) ) { + if ( getParameterRenderingMode() == SqlAstNodeRenderingMode.DEFAULT + && areAllResultsParameters( caseSimpleExpression ) ) { final List whenFragments = caseSimpleExpression.getWhenFragments(); final Expression firstResult = whenFragments.get( 0 ).getResult(); super.visitAnsiCaseSimpleExpression( @@ -206,10 +218,47 @@ public void visitValuesTableReference(ValuesTableReference tableReference) { @Override public void visitOffsetFetchClause(QueryPart queryPart) { - assertRowsOnlyFetchClauseType( queryPart ); - if ( !queryPart.isRoot() && queryPart.getOffsetClauseExpression() != null ) { - throw new IllegalArgumentException( "Can't emulate offset clause in subquery" ); + if ( !currentFullJoinEmulationHelper().isFullJoinEmulationQueryPart( queryPart ) ) { + assertRowsOnlyFetchClauseType( queryPart ); + if ( !queryPart.isRoot() && queryPart.getOffsetClauseExpression() != null ) { + throw new IllegalArgumentException( "Can't emulate offset clause in subquery" ); + } + } + } + + @Override + public void visitQuerySpec(QuerySpec querySpec) { + final var helper = currentFullJoinEmulationHelper(); + final boolean needsNestedHelper = + helper.hasActiveFullJoinEmulation() + && !helper.isFullJoinEmulationQueryPart( querySpec ); + if ( needsNestedHelper ) { + fullJoinEmulations.push( new FullJoinEmulation( this ) ); + } + try { + final var currentHelper = currentFullJoinEmulationHelper(); + if ( !currentHelper.renderFullJoinEmulationBranchIfNeeded( querySpec, super::visitQuerySpec ) + && !currentHelper.emulateFullJoinWithUnionIfNeeded( querySpec ) ) { + super.visitQuerySpec( querySpec ); + } } + finally { + if ( needsNestedHelper ) { + fullJoinEmulations.pop(); + } + } + } + + @Override + public void visitSelectClause(SelectClause selectClause) { + if ( !currentFullJoinEmulationHelper().renderSelectClauseIfNeeded( selectClause ) ) { + super.visitSelectClause( selectClause ); + } + } + + @Override + protected void visitOrderBy(List sortSpecifications) { + currentFullJoinEmulationHelper().renderOrderByIfNeeded( getCurrentQueryPart(), sortSpecifications, super::visitOrderBy ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/temporal/DB2TemporalTableSupport.java b/hibernate-core/src/main/java/org/hibernate/dialect/temporal/DB2TemporalTableSupport.java new file mode 100644 index 000000000000..94a88f123e5d --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/temporal/DB2TemporalTableSupport.java @@ -0,0 +1,101 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.temporal; + +import org.hibernate.boot.model.relational.Database; +import org.hibernate.boot.model.relational.NamedAuxiliaryDatabaseObject; +import org.hibernate.temporal.TemporalTableStrategy; +import org.hibernate.dialect.DB2Dialect; +import org.hibernate.mapping.Table; + +import static java.util.Collections.emptySet; + +/** + * @author Gavin King + */ +public class DB2TemporalTableSupport extends DefaultTemporalTableSupport { + + public DB2TemporalTableSupport(DB2Dialect dialect) { + super( dialect ); + } + + @Override + public int getTemporalColumnPrecision() { + return 12; // required! + } + + @Override + public boolean supportsNativeTemporalTables() { + return true; + } + + @Override + public boolean supportsTemporalTablePartitioning() { + return true; + } + + @Override + public String getExtraTemporalTableDeclarations( + TemporalTableStrategy strategy, + String rowStartColumn, String rowEndColumn, + boolean partitioned) { + // no 'for' keyword + if ( strategy == TemporalTableStrategy.NATIVE ) { + return "transaction_start_id timestamp(12) not null generated always as transaction start id implicitly hidden" + + ", period system_time (" + rowStartColumn + ", " + rowEndColumn + ")"; + } + else if ( partitioned ) { + return rowEndColumn + "_null smallint generated always as (case when " + rowEndColumn + " is null then 1 else 0 end) implicitly hidden"; + } + else { + return null; + } + } + + @Override + public String getTemporalTableOptions( + TemporalTableStrategy strategy, + String rowEndColumnName, + boolean partitioned, + String currentPartition, + String historyPartition) { + return partitioned + ? "partition by range (" + rowEndColumnName + "_null)" + + " (partition " + historyPartition + " starting from (0) ending at (0)," + + " partition " + currentPartition + " starting from (1) ending at (1))" + : null; + } + + @Override + public void addTemporalTableAuxiliaryObjects( + TemporalTableStrategy strategy, + Table table, Database database, + boolean partitioned, + String currentPartitionName, + String historyPartitionName) { + if ( strategy == TemporalTableStrategy.NATIVE ) { + final String name = table.getQuotedName( dialect ); + final String historyTableName = name + "_history"; + database.addAuxiliaryDatabaseObject( + new NamedAuxiliaryDatabaseObject( + historyTableName, + database.getDefaultNamespace(), + new String[] { + "create table " + historyTableName + " like " + name, + "alter table " + name + " add versioning use history table " + historyTableName, + }, + new String[] {"drop table " + historyTableName}, + emptySet() + ) + ); + } + } + + @Override + public TemporalTableStrategy getDefaultTemporalTableStrategy() { + return TemporalTableStrategy.NATIVE; + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/temporal/DefaultTemporalTableSupport.java b/hibernate-core/src/main/java/org/hibernate/dialect/temporal/DefaultTemporalTableSupport.java new file mode 100644 index 000000000000..d2e72e4a91dd --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/temporal/DefaultTemporalTableSupport.java @@ -0,0 +1,119 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.temporal; + +import org.hibernate.MappingException; +import org.hibernate.boot.model.relational.Database; +import org.hibernate.temporal.TemporalTableStrategy; +import org.hibernate.dialect.Dialect; +import org.hibernate.engine.spi.LoadQueryInfluencers; +import org.hibernate.mapping.Table; +import org.hibernate.type.SqlTypes; + +import static org.hibernate.temporal.TemporalTableStrategy.HISTORY_TABLE; + +/** + * @author Gavin King + */ +public class DefaultTemporalTableSupport implements TemporalTableSupport { + + final Dialect dialect; + + public DefaultTemporalTableSupport(Dialect dialect) { + this.dialect = dialect; + } + + + @Override + public boolean supportsNativeTemporalTables() { + return false; + } + + @Override + public int getTemporalColumnType() { + return SqlTypes.TIMESTAMP; + } + + @Override + public int getTemporalColumnPrecision() { + return dialect.getDefaultTimestampPrecision(); + } + + @Override + public String getTemporalTableOptions( + TemporalTableStrategy strategy, + String rowEndColumnName, + boolean partitioned, + String currentPartitionName, + String historyPartitionName) { + return null; + } + + @Override + public boolean suppressesTemporalTablePrimaryKeys(boolean partitioned) { + return partitioned && supportsTemporalTablePartitioning(); + } + + @Override + public boolean supportsTemporalTablePartitioning() { + return false; + } + + @Override + public void addTemporalTableAuxiliaryObjects( + TemporalTableStrategy strategy, + Table table, Database database, + boolean partitioned, + String currentPartitionName, + String historyPartitionName) { + } + + @Override + public String getExtraTemporalTableDeclarations( + TemporalTableStrategy strategy, + String rowStartColumn, String rowEndColumn, + boolean partitioned) { + return null; + } + + @Override + public boolean createTemporalTableCheckConstraint(TemporalTableStrategy strategy) { + return strategy != TemporalTableStrategy.NATIVE + && dialect.supportsTableCheck(); + } + + @Override + public String getAsOfOperator(TemporalTableStrategy strategy) { + return "for system_time as of"; + } + + @Override + public boolean useAsOfOperator(TemporalTableStrategy strategy) { + return strategy == TemporalTableStrategy.NATIVE; + } + + @Override + public boolean useTemporalRestriction(LoadQueryInfluencers influencers) { + final var strategy = + influencers.getSessionFactory().getSessionFactoryOptions() + .getTemporalTableStrategy(); + return switch ( strategy ) { + case HISTORY_TABLE -> influencers.getTemporalIdentifier() != null; + case NATIVE -> false; + default -> true; + }; + } + + @Override + public String getTemporalExclusionColumnOption() { + throw new MappingException( "Native temporal exclusion column option is not supported by this dialect" ); + } + + @Override + public TemporalTableStrategy getDefaultTemporalTableStrategy() { + return HISTORY_TABLE; + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/temporal/MariaDBTemporalTableSupport.java b/hibernate-core/src/main/java/org/hibernate/dialect/temporal/MariaDBTemporalTableSupport.java new file mode 100644 index 000000000000..41ec49d81e54 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/temporal/MariaDBTemporalTableSupport.java @@ -0,0 +1,64 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.temporal; + +import org.hibernate.temporal.TemporalTableStrategy; +import org.hibernate.dialect.MariaDBDialect; +import org.hibernate.type.SqlTypes; + +/** + * @author Gavin King + */ +public class MariaDBTemporalTableSupport extends MySQLTemporalTableSupport { + + public MariaDBTemporalTableSupport(MariaDBDialect dialect) { + super( dialect ); + } + + @Override + public boolean supportsNativeTemporalTables() { + return true; + } + + @Override + public boolean supportsTemporalTablePartitioning() { + return false; + } + + @Override + public String getTemporalTableOptions( + TemporalTableStrategy strategy, + String rowEndColumnName, + boolean partitioned, + String currentPartitionName, + String historyPartitionName) { + return strategy == TemporalTableStrategy.NATIVE + ? "with system versioning" + : null; + } + + @Override + public String getExtraTemporalTableDeclarations(TemporalTableStrategy strategy, String rowStartColumn, String rowEndColumn, boolean partitioned) { + return strategy == TemporalTableStrategy.NATIVE + ? "period for system_time (" + rowStartColumn + ", " + rowEndColumn + ")" + : null; + } + + @Override + public int getTemporalColumnType() { + return SqlTypes.TIMESTAMP_WITH_TIMEZONE; + } + + @Override + public String getTemporalExclusionColumnOption() { + return "without system versioning"; + } + + @Override + public TemporalTableStrategy getDefaultTemporalTableStrategy() { + return TemporalTableStrategy.NATIVE; + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/temporal/MySQLTemporalTableSupport.java b/hibernate-core/src/main/java/org/hibernate/dialect/temporal/MySQLTemporalTableSupport.java new file mode 100644 index 000000000000..ee4b94438416 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/temporal/MySQLTemporalTableSupport.java @@ -0,0 +1,53 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.temporal; + +import org.hibernate.temporal.TemporalTableStrategy; +import org.hibernate.dialect.MySQLDialect; +import org.hibernate.type.SqlTypes; + +/** + * @author Gavin King + */ +public class MySQLTemporalTableSupport extends DefaultTemporalTableSupport { + + public MySQLTemporalTableSupport(MySQLDialect dialect) { + super( dialect ); + } + + @Override + public boolean supportsTemporalTablePartitioning() { + return true; + } + + @Override + public String getTemporalTableOptions( + TemporalTableStrategy strategy, + String rowEndColumnName, + boolean partitioned, + String currentPartition, + String historyPartition) { + return partitioned + ? "partition by list (" + rowEndColumnName + "_null)" + + " (partition " + historyPartition + " values in (0)," + + " partition " + currentPartition + " values in (1))" + : null; + } + + @Override + public String getExtraTemporalTableDeclarations( + TemporalTableStrategy strategy, + String rowStartColumn, String rowEndColumn, + boolean partitioned) { + return partitioned + ? rowEndColumn + "_null tinyint as (" + rowEndColumn + " is null) virtual invisible" + : null; + } + + @Override + public int getTemporalColumnType() { + return SqlTypes.TIMESTAMP_UTC; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/temporal/OracleTemporalTableSupport.java b/hibernate-core/src/main/java/org/hibernate/dialect/temporal/OracleTemporalTableSupport.java new file mode 100644 index 000000000000..778c85d27873 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/temporal/OracleTemporalTableSupport.java @@ -0,0 +1,125 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.temporal; + +import org.hibernate.boot.model.relational.Database; +import org.hibernate.boot.model.relational.NamedAuxiliaryDatabaseObject; +import org.hibernate.boot.model.relational.SimpleAuxiliaryDatabaseObject; +import org.hibernate.temporal.TemporalTableStrategy; +import org.hibernate.dialect.OracleDialect; +import org.hibernate.engine.spi.LoadQueryInfluencers; +import org.hibernate.mapping.Table; + +import static java.util.Collections.emptySet; +import static org.hibernate.temporal.TemporalTableStrategy.HISTORY_TABLE; + +/** + * @author Gavin King + */ +public class OracleTemporalTableSupport extends DefaultTemporalTableSupport { + + public OracleTemporalTableSupport(OracleDialect dialect) { + super( dialect ); + } + + /** + * Return {@code false} because we use {@code period for system_time} + * to implement the constraint on Oracle. + */ + @Override + public boolean createTemporalTableCheckConstraint(TemporalTableStrategy strategy) { + return false; + } + + @Override + public String getExtraTemporalTableDeclarations(TemporalTableStrategy strategy, String rowStartColumn, String rowEndColumn, boolean partitioned) { + return "period for system_time (" + rowStartColumn + ", " + rowEndColumn + ")"; + } + + @Override + public String getAsOfOperator(TemporalTableStrategy strategy) { + return strategy == TemporalTableStrategy.NATIVE + ? "as of timestamp" + : "as of period for system_time"; + } + + @Override + public boolean useAsOfOperator(TemporalTableStrategy strategy) { + return strategy != HISTORY_TABLE; + } + + @Override + public boolean useTemporalRestriction(LoadQueryInfluencers influencers) { + final var sessionFactory = influencers.getSessionFactory(); + return sessionFactory.getTransactionIdentifierService().isIdentifierTypeInstant() + ? sessionFactory.getSessionFactoryOptions().getTemporalTableStrategy() == HISTORY_TABLE + && influencers.getTemporalIdentifier() != null + : super.useTemporalRestriction( influencers ); + } + + @Override + public boolean supportsTemporalTablePartitioning() { + return true; + } + + @Override + public boolean suppressesTemporalTablePrimaryKeys(boolean partitioned) { + return false; + } + + @Override + public String getTemporalTableOptions( + TemporalTableStrategy strategy, + String rowEndColumnName, + boolean partitioned, + String currentPartition, + String historyPartition) { + if ( strategy == TemporalTableStrategy.NATIVE ) { + return "flashback archive fba_history"; + } + else if ( partitioned ) { + return "partition by list( " + rowEndColumnName + ")" + + " (partition " + currentPartition + " values (null)," + + " partition " + historyPartition + " values (default))" + + " enable row movement"; + } + else { + return null; + } + } + + @Override + public boolean supportsNativeTemporalTables() { + return true; + } + + @Override + public void addTemporalTableAuxiliaryObjects( + TemporalTableStrategy strategy, + Table table, + Database database, + boolean partitioned, + String currentPartitionName, + String historyPartitionName) { + if ( strategy == TemporalTableStrategy.NATIVE ) { + database.addAuxiliaryDatabaseObject( new SimpleAuxiliaryDatabaseObject( + database.getDefaultNamespace(), + new String[0], + new String[] { "alter table " + table.getQuotedName(dialect) + " no flashback archive" } , + emptySet(), + false + ) ); + database.addAuxiliaryDatabaseObject( new NamedAuxiliaryDatabaseObject( + "fba_history", + database.getDefaultNamespace(), + "create flashback archive fba_history tablespace users quota 1M retention 1 month", + "drop flashback archive fba_history", + emptySet(), + true + ) ); + } + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/temporal/PostgreSQLTemporalTableSupport.java b/hibernate-core/src/main/java/org/hibernate/dialect/temporal/PostgreSQLTemporalTableSupport.java new file mode 100644 index 000000000000..569bc18bfde9 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/temporal/PostgreSQLTemporalTableSupport.java @@ -0,0 +1,73 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.temporal; + +import org.hibernate.boot.model.relational.Database; +import org.hibernate.boot.model.relational.NamedAuxiliaryDatabaseObject; +import org.hibernate.temporal.TemporalTableStrategy; +import org.hibernate.dialect.PostgreSQLDialect; +import org.hibernate.mapping.Table; +import org.hibernate.type.SqlTypes; + +import static java.util.Collections.emptySet; + +/** + * @author Gavin King + */ +public class PostgreSQLTemporalTableSupport extends DefaultTemporalTableSupport { + + public PostgreSQLTemporalTableSupport(PostgreSQLDialect dialect) { + super( dialect ); + } + + @Override + public boolean supportsTemporalTablePartitioning() { + return true; + } + + @Override + public String getTemporalTableOptions( + TemporalTableStrategy strategy, + String rowEndColumnName, + boolean partitioned, + String currentPartitionName, + String historyPartitionName) { + return partitioned + ? "partition by list (" + rowEndColumnName + ")" + : null; + } + + @Override + public void addTemporalTableAuxiliaryObjects( + TemporalTableStrategy strategy, + Table table, + Database database, + boolean partitioned, + String currentPartition, + String historyPartition) { + if ( partitioned ) { + final String tableName = table.getQuotedName( dialect ); + database.addAuxiliaryDatabaseObject( new NamedAuxiliaryDatabaseObject( + currentPartition, + database.getDefaultNamespace(), + "create table " + currentPartition + " partition of " + tableName + " for values in (null)", + "drop table if exists " + currentPartition + " cascade", + emptySet() + ) ); + database.addAuxiliaryDatabaseObject( new NamedAuxiliaryDatabaseObject( + historyPartition, + database.getDefaultNamespace(), + "create table " + historyPartition + " partition of " + tableName + " default", + "drop table if exists " + historyPartition + " cascade", + emptySet() + ) ); + } + } + + @Override + public int getTemporalColumnType() { + return SqlTypes.TIMESTAMP_UTC; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/temporal/SQLServerTemporalTableSupport.java b/hibernate-core/src/main/java/org/hibernate/dialect/temporal/SQLServerTemporalTableSupport.java new file mode 100644 index 000000000000..e7acd9696822 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/temporal/SQLServerTemporalTableSupport.java @@ -0,0 +1,73 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.temporal; + +import org.hibernate.boot.model.relational.Database; +import org.hibernate.boot.model.relational.SimpleAuxiliaryDatabaseObject; +import org.hibernate.temporal.TemporalTableStrategy; +import org.hibernate.dialect.SQLServerDialect; +import org.hibernate.mapping.Table; + +import static java.util.Collections.emptySet; + +/** + * @author Gavin King + */ +public class SQLServerTemporalTableSupport extends DefaultTemporalTableSupport { + + public SQLServerTemporalTableSupport(SQLServerDialect dialect) { + super( dialect ); + } + + @Override + public boolean supportsNativeTemporalTables() { + return true; + } + + @Override + public String getExtraTemporalTableDeclarations( + TemporalTableStrategy strategy, + String rowStartColumn, String rowEndColumn, + boolean partitioned) { + return strategy == TemporalTableStrategy.NATIVE + // Transaction id support was only added in SQL Server 2022 (16.x) + ? (dialect.getVersion().isSameOrAfter( 16 ) + ? "transaction_start_id bigint generated always as transaction_id start hidden not null, " : "") + + "period for system_time (" + rowStartColumn + ", " + rowEndColumn + ")" + : null; + } + + @Override + public String getTemporalTableOptions( + TemporalTableStrategy strategy, + String rowEndColumnName, + boolean partitioned, + String currentPartitionName, + String historyPartitionName) { + return strategy == TemporalTableStrategy.NATIVE + ? "with (system_versioning = on)" + : null; + } + + @Override + public void addTemporalTableAuxiliaryObjects( + TemporalTableStrategy strategy, + Table table, + Database database, + boolean partitioned, + String currentPartitionName, + String historyPartitionName) { + if ( strategy == TemporalTableStrategy.NATIVE ) { + database.addAuxiliaryDatabaseObject( new SimpleAuxiliaryDatabaseObject( + database.getDefaultNamespace(), + new String[0], + new String[] { "alter table " + table.getQuotedName(dialect) + " set (system_versioning = off)" }, + emptySet(), + false + ) ); + } + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/temporal/TemporalTableSupport.java b/hibernate-core/src/main/java/org/hibernate/dialect/temporal/TemporalTableSupport.java new file mode 100644 index 000000000000..b208e5d307db --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/temporal/TemporalTableSupport.java @@ -0,0 +1,169 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.temporal; + +import org.hibernate.Incubating; +import org.hibernate.boot.model.relational.Database; +import org.hibernate.temporal.TemporalTableStrategy; +import org.hibernate.dialect.Dialect; +import org.hibernate.engine.spi.LoadQueryInfluencers; +import org.hibernate.mapping.Table; +import org.hibernate.type.SqlTypes; + +/** + * Abstracts the support for temporal tables. + * + * @author Gavin King + * + * @since 7.4 + */ +@Incubating +public interface TemporalTableSupport { + + /** + * Does this dialect natively support SQL 2011-style + * temporal tables? + * + * @see TemporalTableStrategy#NATIVE + */ + boolean supportsNativeTemporalTables(); + + /** + * The column type to use for effectivity columns of + * temporal tables. The default implementation returns + * {@link SqlTypes#TIMESTAMP TIMESTAMP}. + */ + int getTemporalColumnType(); + + /** + * The column precision to use for effectivity columns + * of native temporal tables when the precision is not + * explicitly specified. The default implementation + * returns {@linkplain Dialect#getDefaultTimestampPrecision + * the default timestamp precision} for this dialect. + * + * @see org.hibernate.annotations.Temporal#secondPrecision + */ + int getTemporalColumnPrecision(); + + /** + * Table {@linkplain jakarta.persistence.Table#options options} + * to use for temporal tables, used to specify system versioning + * or table partitioning. + * + * @param strategy The temporal table strategy + * @param rowEndColumnName The name of the {@code row end} column + * specified via {@link org.hibernate.annotations.Temporal#rowEnd} + * @param partitioned Is partitioning requested + * @param currentPartitionName The current partition name, if specified + * @param historyPartitionName The history partition name, if specified + * @return The options, or {@code null} if there are no options + */ + String getTemporalTableOptions( + TemporalTableStrategy strategy, + String rowEndColumnName, + boolean partitioned, + String currentPartitionName, + String historyPartitionName); + + /** + * Do we need to suppress creation of the primary key + * constraint on a temporal table? + * + * @param partitioned Is partitioning requested + */ + boolean suppressesTemporalTablePrimaryKeys(boolean partitioned); + + /** + * Do we support partitioning temporal tables in this + * dialect? + * + * @see org.hibernate.annotations.Temporal.HistoryPartitioning + */ + boolean supportsTemporalTablePartitioning(); + + /** + * Register any auxiliary database objects required + * for the given temporary table and strategy. Used + * to create history tables or table partitions. + * + * @param strategy The temporal table strategy + * @param table A temporal table + * @param database The database to register with + * @param partitioned Is partitioning requested + * @param currentPartitionName The current partition name, if specified + * @param historyPartitionName The history partition name, if specified + */ + void addTemporalTableAuxiliaryObjects( + TemporalTableStrategy strategy, + Table table, Database database, + boolean partitioned, + String currentPartitionName, + String historyPartitionName); + + /** + * Any extra declarations required as part of the {@code create table} + * statement for a temporal table. These declarations, unlike the + * {@linkplain #getTemporalTableOptions options} come inside the + * parentheses, along with the column and constraint definitions. + * Examples include the {@code period for system_time} clause, the Db2 + * {@code transaction start id} column, the MySQL partitioning column, + * and so on. + * + * @param strategy The temporal table strategy + * @param partitioned Is partitioning requested + */ + String getExtraTemporalTableDeclarations( + TemporalTableStrategy strategy, + String rowStartColumn, String rowEndColumn, + boolean partitioned); + + /** + * Should we create a {@code check} constraint to enforce effectivity + * constraints? (That starting timestamps precede ending timestamps.) + * This is typically not needed for native temporal tables. + */ + boolean createTemporalTableCheckConstraint(TemporalTableStrategy strategy); + + /** + * The operator used to specify a temporal instant for querying + * historical data. Usually {@code for system_time as of}. This + * is usually used together with native temporal tables, but in + * Oracle we use it all the time. + */ + String getAsOfOperator(TemporalTableStrategy strategy); + + /** + * Should be use the {@link #getAsOfOperator for system_time as of} + * operator when querying temporal tables? We usually only use it + * for querying native temporal tables at a historical instant, but + * in Oracle we use it all the time. + * + * @param strategy The strategy + */ + boolean useAsOfOperator(TemporalTableStrategy strategy); + + /** + * Should we use temporal restrictions on the {@code row start} and + * {@code row end} columns when querying temporal tables? We usually + * use them unless we are using native temporal tables, but on Oracle + * we never use them. + * @param influencers The {@link LoadQueryInfluencers} + */ + boolean useTemporalRestriction(LoadQueryInfluencers influencers); + + /** + * Column options for a native implementation of exclusion from + * temporal versioning. + */ + String getTemporalExclusionColumnOption(); + + /** + * The recommended temporal table strategy for this dialect. + * + * @see TemporalTableStrategy#AUTO + */ + TemporalTableStrategy getDefaultTemporalTableStrategy(); +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/temporal/package-info.java b/hibernate-core/src/main/java/org/hibernate/dialect/temporal/package-info.java new file mode 100644 index 000000000000..55ac88b90d79 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/temporal/package-info.java @@ -0,0 +1,6 @@ +/** + * Abstracts over database-native support for temporal tables. + * + * @see org.hibernate.dialect.temporal.TemporalTableSupport + */ +package org.hibernate.dialect.temporal; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/temptable/StandardTemporaryTableExporter.java b/hibernate-core/src/main/java/org/hibernate/dialect/temptable/StandardTemporaryTableExporter.java index 6eb3a86eff5c..725491b1470e 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/temptable/StandardTemporaryTableExporter.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/temptable/StandardTemporaryTableExporter.java @@ -4,6 +4,7 @@ */ package org.hibernate.dialect.temptable; +import java.util.Locale; import java.util.function.Function; import org.hibernate.dialect.Dialect; @@ -72,7 +73,7 @@ private TemporaryTableStrategy getDefaultTemporaryTableStrategy(TemporaryTable t @Override public String getSqlCreateCommand(TemporaryTable temporaryTable) { final TemporaryTableStrategy temporaryTableStrategy = getDefaultTemporaryTableStrategy( temporaryTable ); - final StringBuilder buffer = new StringBuilder( getCreateCommand( temporaryTableStrategy ) ).append( ' ' ); + final var buffer = new StringBuilder( getCreateCommand( temporaryTableStrategy ) ).append( ' ' ); buffer.append( temporaryTable.getQualifiedTableName() ); buffer.append( '(' ); @@ -96,7 +97,9 @@ public String getSqlCreateCommand(TemporaryTable temporaryTable) { } } else { - buffer.append( " not null" ); + if ( !databaseTypeName.toLowerCase( Locale.ROOT ).contains( "not null" ) ) { + buffer.append( " not null" ); + } } } buffer.append( ", " ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/temptable/TemporaryTable.java b/hibernate-core/src/main/java/org/hibernate/dialect/temptable/TemporaryTable.java index 8a434369749f..a59d069fdf48 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/temptable/TemporaryTable.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/temptable/TemporaryTable.java @@ -46,9 +46,9 @@ import org.hibernate.query.sqm.mutation.internal.SqmMutationStrategyHelper; import org.hibernate.type.BasicType; import org.hibernate.type.StandardBasicTypes; -import org.hibernate.type.spi.TypeConfiguration; import static org.hibernate.internal.CoreMessageLogger.CORE_LOGGER; +import static org.hibernate.query.sqm.mutation.internal.SqmMutationStrategyHelper.isPartOfId; /** @@ -111,7 +111,7 @@ private TemporaryTable( this.temporaryTableKind = temporaryTableKind; this.dialect = dialect; if ( temporaryTableKind == TemporaryTableKind.PERSISTENT ) { - final TypeConfiguration typeConfiguration = creationContext.getTypeConfiguration(); + final var typeConfiguration = creationContext.getTypeConfiguration(); final BasicType uuidType = typeConfiguration.getBasicTypeRegistry().resolve( StandardBasicTypes.UUID_CHAR ); @@ -354,7 +354,7 @@ public static TemporaryTable createEntityTable( } } if ( isExternallyGenerated ) { - final TypeConfiguration typeConfiguration = metadata.getTypeConfiguration(); + final var typeConfiguration = metadata.getTypeConfiguration(); // We add a special row number column that we can use to identify and join rows final BasicType integerBasicType = typeConfiguration.getBasicTypeForJavaType( Integer.class ); final String rowNumberType; @@ -512,10 +512,14 @@ else if ( modelPart instanceof EmbeddableValuedModelPart embeddablePart ) { } return offset; } - else if ( modelPart instanceof BasicValuedModelPart basicModelPart ) { - return offset + (basicModelPart.isInsertable() ? modelPart.getJdbcTypeCount() : 0); + else if ( modelPart instanceof BasicValuedModelPart basicModelPart + && !basicModelPart.isInsertable() + && !(modelPart instanceof AttributeMapping attributeMapping && isPartOfId( attributeMapping )) ) { + return offset; + } + else { + return offset + modelPart.getJdbcTypeCount(); } - return offset + modelPart.getJdbcTypeCount(); } public boolean isRowNumberGenerated() { diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/type/AbstractPostgreSQLStructJdbcType.java b/hibernate-core/src/main/java/org/hibernate/dialect/type/AbstractPostgreSQLStructJdbcType.java index 5a28ee9ef16d..694f6b7875f8 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/type/AbstractPostgreSQLStructJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/type/AbstractPostgreSQLStructJdbcType.java @@ -4,27 +4,9 @@ */ package org.hibernate.dialect.type; -import java.lang.reflect.Array; -import java.sql.CallableStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Timestamp; -import java.time.Instant; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.LocalTime; -import java.time.OffsetDateTime; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeFormatterBuilder; -import java.time.temporal.ChronoField; -import java.time.temporal.TemporalAccessor; -import java.util.ArrayList; -import java.util.TimeZone; - import org.hibernate.internal.util.CharSequenceHelper; import org.hibernate.metamodel.mapping.EmbeddableMappingType; import org.hibernate.metamodel.mapping.JdbcMapping; -import org.hibernate.metamodel.mapping.MappingType; import org.hibernate.metamodel.mapping.SelectableMapping; import org.hibernate.metamodel.mapping.ValuedModelPart; import org.hibernate.sql.ast.spi.SqlAppender; @@ -43,13 +25,31 @@ import org.hibernate.type.descriptor.jdbc.StructuredJdbcType; import org.hibernate.type.spi.TypeConfiguration; -import static org.hibernate.type.descriptor.jdbc.StructHelper.getSubPart; -import static org.hibernate.type.descriptor.jdbc.StructHelper.instantiate; +import java.sql.CallableStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.time.temporal.ChronoField; +import java.time.temporal.TemporalAccessor; +import java.util.ArrayList; +import java.util.TimeZone; + +import static java.lang.reflect.Array.get; +import static java.lang.reflect.Array.getLength; 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; import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTimestampWithMillis; +import static org.hibernate.type.descriptor.jdbc.StructHelper.getSubPart; +import static org.hibernate.type.descriptor.jdbc.StructHelper.instantiate; /** * Implementation for serializing/deserializing an embeddable aggregate to/from the PostgreSQL component format. @@ -125,17 +125,13 @@ public EmbeddableMappingType getEmbeddableMappingType() { } @Override - public JavaType getJdbcRecommendedJavaTypeMapping( + public JavaType getRecommendedJavaType( Integer precision, Integer scale, TypeConfiguration typeConfiguration) { - if ( embeddableMappingType == null ) { - return typeConfiguration.getJavaTypeRegistry().getDescriptor( Object[].class ); - } - else { - //noinspection unchecked - return (JavaType) embeddableMappingType.getMappedJavaType(); - } + return embeddableMappingType == null + ? typeConfiguration.getJavaTypeRegistry().resolveDescriptor( Object[].class ) + : embeddableMappingType.getMappedJavaType(); } @Override @@ -189,15 +185,13 @@ protected X fromString(String string, JavaType javaType, WrapperOptions o } assert end == string.length(); if ( returnEmbeddable ) { - final StructAttributeValues attributeValues = getAttributeValues( embeddableMappingType, orderMapping, array, options ); - //noinspection unchecked - return (X) instantiate( embeddableMappingType, attributeValues ); + final var attributeValues = getAttributeValues( embeddableMappingType, orderMapping, array, options ); + return javaType.cast( instantiate( embeddableMappingType, attributeValues ) ); } else if ( inverseOrderMapping != null ) { StructHelper.orderJdbcValues( embeddableMappingType, inverseOrderMapping, array.clone(), array ); } - //noinspection unchecked - return (X) array; + return javaType.cast( array ); } private int deserializeStruct( @@ -336,7 +330,7 @@ private int deserializeStruct( continue; } assert isDoubleQuote( string, i, 1 << quotes ); - final JdbcMapping jdbcMapping = getJdbcValueSelectable( column ).getJdbcMapping(); + final var jdbcMapping = getJdbcValueSelectable( column ).getJdbcMapping(); switch ( jdbcMapping.getJdbcType().getDefaultSqlTypeCode() ) { case SqlTypes.DATE: values[column] = fromRawObject( @@ -450,7 +444,7 @@ private int deserializeStruct( i += expectedQuotes - 1; if ( string.charAt( i + 1 ) == '(' ) { // This could be a nested struct - final JdbcMapping jdbcMapping = getJdbcValueSelectable( column ).getJdbcMapping(); + final var jdbcMapping = getJdbcValueSelectable( column ).getJdbcMapping(); if ( jdbcMapping.getJdbcType() instanceof AbstractPostgreSQLStructJdbcType structJdbcType ) { final Object[] subValues = new Object[structJdbcType.embeddableMappingType.getJdbcValueCount()]; final int subEnd = structJdbcType.deserializeStruct( @@ -500,7 +494,7 @@ private int deserializeStruct( } else if ( string.charAt( i + 1 ) == '{' ) { // This could be a quoted array - final JdbcMapping jdbcMapping = getJdbcValueSelectable( column ).getJdbcMapping(); + final var jdbcMapping = getJdbcValueSelectable( column ).getJdbcMapping(); if ( jdbcMapping instanceof BasicPluralType pluralType ) { final ArrayList arrayList = new ArrayList<>(); //noinspection unchecked @@ -543,7 +537,7 @@ else if ( string.charAt( i + 1 ) == '{' ) { values[column] = null; } else { - final JdbcMapping jdbcMapping = getJdbcValueSelectable( column ).getJdbcMapping(); + final var jdbcMapping = getJdbcValueSelectable( column ).getJdbcMapping(); if ( jdbcMapping.getJdbcType().getDefaultSqlTypeCode() == SqlTypes.BOOLEAN ) { values[column] = fromRawObject( jdbcMapping, @@ -579,7 +573,7 @@ else if ( jdbcMapping.getJavaTypeDescriptor().getJavaTypeClass().isEnum() values[column] = null; } else { - final JdbcMapping jdbcMapping = getJdbcValueSelectable( column ).getJdbcMapping(); + final var jdbcMapping = getJdbcValueSelectable( column ).getJdbcMapping(); if ( jdbcMapping.getJdbcType().getDefaultSqlTypeCode() == SqlTypes.BOOLEAN ) { values[column] = fromRawObject( jdbcMapping, @@ -610,7 +604,7 @@ else if ( jdbcMapping.getJavaTypeDescriptor().getJavaTypeClass().isEnum() break; case '{': if ( !inQuote ) { - final BasicPluralType pluralType = (BasicPluralType) getJdbcValueSelectable( column ).getJdbcMapping(); + final var pluralType = (BasicPluralType) getJdbcValueSelectable( column ).getJdbcMapping(); final ArrayList arrayList = new ArrayList<>(); //noinspection unchecked i = deserializeArray( @@ -645,14 +639,10 @@ private boolean isBinary(int column) { } private static boolean isBinary(JdbcMapping jdbcMapping) { - switch ( jdbcMapping.getJdbcType().getDefaultSqlTypeCode() ) { - case SqlTypes.BINARY: - case SqlTypes.VARBINARY: - case SqlTypes.LONGVARBINARY: - case SqlTypes.LONG32VARBINARY: - return true; - } - return false; + return switch ( jdbcMapping.getJdbcType().getDefaultSqlTypeCode() ) { + case SqlTypes.BINARY, SqlTypes.VARBINARY, SqlTypes.LONGVARBINARY, SqlTypes.LONG32VARBINARY -> true; + default -> false; + }; } private int deserializeArray( @@ -803,7 +793,7 @@ private int deserializeArray( ); break; default: - if ( escapingSb == null || escapingSb.length() == 0 ) { + if ( escapingSb == null || escapingSb.isEmpty() ) { values.add( fromString( elementType, @@ -1027,19 +1017,6 @@ private static boolean isDoubleQuote(String string, int start, int escapes) { return false; } - private Object fromString( - int selectableIndex, - String string, - int start, - int end) { - return fromString( - getJdbcValueSelectable( selectableIndex ).getJdbcMapping(), - string, - start, - end - ); - } - private static Object fromString(JdbcMapping jdbcMapping, CharSequence charSequence, int start, int end) { return jdbcMapping.getJdbcJavaType().fromEncodedString( charSequence, @@ -1064,22 +1041,19 @@ private Object parseTime(CharSequence subSequence) { } private Object parseTimestamp(CharSequence subSequence, JavaType jdbcJavaType) { - final TemporalAccessor temporalAccessor = LOCAL_DATE_TIME.parse( subSequence ); - final LocalDateTime localDateTime = LocalDateTime.from( temporalAccessor ); - final Timestamp timestamp = Timestamp.valueOf( localDateTime ); + final var temporalAccessor = LOCAL_DATE_TIME.parse( subSequence ); + final var localDateTime = LocalDateTime.from( temporalAccessor ); + final var timestamp = Timestamp.valueOf( localDateTime ); timestamp.setNanos( temporalAccessor.get( ChronoField.NANO_OF_SECOND ) ); return timestamp; } private Object parseTimestampWithTimeZone(CharSequence subSequence, JavaType jdbcJavaType) { - final TemporalAccessor temporalAccessor = LOCAL_DATE_TIME.parse( subSequence ); + final var temporalAccessor = LOCAL_DATE_TIME.parse( subSequence ); if ( temporalAccessor.isSupported( ChronoField.OFFSET_SECONDS ) ) { - if ( jdbcJavaType.getJavaTypeClass() == Instant.class ) { - return Instant.from( temporalAccessor ); - } - else { - return OffsetDateTime.from( temporalAccessor ); - } + return jdbcJavaType.getJavaTypeClass() == Instant.class + ? Instant.from( temporalAccessor ) + : OffsetDateTime.from( temporalAccessor ); } return LocalDateTime.from( temporalAccessor ); } @@ -1101,7 +1075,7 @@ private static String unescape(CharSequence string, int start, int end) { @Override public Object createJdbcValue(Object domainValue, WrapperOptions options) throws SQLException { assert embeddableMappingType != null; - final StringBuilder sb = new StringBuilder(); + final var sb = new StringBuilder(); serializeStructTo( new PostgreSQLAppender( sb ), domainValue, options ); return sb.toString(); } @@ -1129,7 +1103,7 @@ protected String toString(X value, JavaType javaType, WrapperOptions opti if ( value == null ) { return null; } - final StringBuilder sb = new StringBuilder(); + final var sb = new StringBuilder(); serializeStructTo( new PostgreSQLAppender( sb ), value, options ); return sb.toString(); } @@ -1164,10 +1138,10 @@ private void serializeJdbcValuesTo( if ( jdbcValue == null ) { continue; } - final SelectableMapping selectableMapping = orderMapping == null ? + final var selectableMapping = orderMapping == null ? embeddableMappingType.getJdbcValueSelectable( i ) : embeddableMappingType.getJdbcValueSelectable( orderMapping[i] ); - final JdbcMapping jdbcMapping = selectableMapping.getJdbcMapping(); + final var jdbcMapping = selectableMapping.getJdbcMapping(); if ( jdbcMapping.getJdbcType() instanceof AbstractPostgreSQLStructJdbcType structJdbcType ) { appender.quoteStart(); structJdbcType.serializeJdbcValuesTo( @@ -1267,7 +1241,7 @@ private void serializeConvertedBasicTo( break; case SqlTypes.ARRAY: if ( subValue != null ) { - final int length = Array.getLength( subValue ); + final int length = getLength( subValue ); if ( length == 0 ) { appender.append( "{}" ); } @@ -1276,7 +1250,7 @@ private void serializeConvertedBasicTo( final BasicType elementType = ((BasicPluralType) jdbcMapping).getElementType(); appender.quoteStart(); appender.append( '{' ); - Object arrayElement = Array.get( subValue, 0 ); + Object arrayElement = get( subValue, 0 ); if ( arrayElement == null ) { appender.appendNull(); } @@ -1284,7 +1258,7 @@ private void serializeConvertedBasicTo( serializeConvertedBasicTo( appender, options, elementType, arrayElement ); } for ( int i = 1; i < length; i++ ) { - arrayElement = Array.get( subValue, i ); + arrayElement = get( subValue, i ); appender.append( ',' ); if ( arrayElement == null ) { appender.appendNull(); @@ -1301,7 +1275,7 @@ private void serializeConvertedBasicTo( break; case SqlTypes.STRUCT: if ( subValue != null ) { - final AbstractPostgreSQLStructJdbcType structJdbcType = (AbstractPostgreSQLStructJdbcType) jdbcMapping.getJdbcType(); + final var structJdbcType = (AbstractPostgreSQLStructJdbcType) jdbcMapping.getJdbcType(); appender.quoteStart(); structJdbcType.serializeJdbcValuesTo( appender, options, (Object[]) subValue, '(' ); appender.append( ')' ); @@ -1354,7 +1328,7 @@ private int injectAttributeValue( Object[] rawJdbcValues, int jdbcIndex, WrapperOptions options) throws SQLException { - final MappingType mappedType = modelPart.getMappedType(); + final var mappedType = modelPart.getMappedType(); final int jdbcValueCount; final Object rawJdbcValue = rawJdbcValues[jdbcIndex]; if ( mappedType instanceof EmbeddableMappingType embeddableMappingType ) { @@ -1378,7 +1352,7 @@ private int injectAttributeValue( else { assert modelPart.getJdbcTypeCount() == 1; jdbcValueCount = 1; - final JdbcMapping jdbcMapping = modelPart.getSingleJdbcMapping(); + final var jdbcMapping = modelPart.getSingleJdbcMapping(); final Object jdbcValue = jdbcMapping.getJdbcJavaType().wrap( rawJdbcValue, options diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/type/DB2StructJdbcType.java b/hibernate-core/src/main/java/org/hibernate/dialect/type/DB2StructJdbcType.java index 7966480a4de6..e0672eab7bad 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/type/DB2StructJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/type/DB2StructJdbcType.java @@ -93,17 +93,13 @@ public String getStructTypeName() { } @Override - public JavaType getJdbcRecommendedJavaTypeMapping( + public JavaType getRecommendedJavaType( Integer precision, Integer scale, TypeConfiguration typeConfiguration) { - if ( embeddableMappingType == null ) { - return typeConfiguration.getJavaTypeRegistry().getDescriptor( Object[].class ); - } - else { - //noinspection unchecked - return (JavaType) embeddableMappingType.getMappedJavaType(); - } + return embeddableMappingType == null + ? typeConfiguration.getJavaTypeRegistry().resolveDescriptor( Object[].class ) + : embeddableMappingType.getMappedJavaType(); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/type/OracleArrayJdbcType.java b/hibernate-core/src/main/java/org/hibernate/dialect/type/OracleArrayJdbcType.java index 7712cd7e08d2..9aeb04eaffff 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/type/OracleArrayJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/type/OracleArrayJdbcType.java @@ -14,7 +14,6 @@ import org.hibernate.HibernateException; import org.hibernate.boot.model.relational.Database; -import org.hibernate.boot.model.relational.Namespace; import org.hibernate.dialect.Dialect; import org.hibernate.engine.jdbc.Size; import org.hibernate.mapping.UserDefinedArrayType; @@ -71,60 +70,68 @@ public JdbcLiteralFormatter getJdbcLiteralFormatter(JavaType javaTypeD @Override public ValueBinder getBinder(final JavaType javaTypeDescriptor) { - @SuppressWarnings("unchecked") - final BasicPluralJavaType pluralJavaType = (BasicPluralJavaType) javaTypeDescriptor; - final ValueBinder elementBinder = getElementJdbcType().getBinder( pluralJavaType.getElementJavaType() ); - return new BasicBinder<>( javaTypeDescriptor, this ) { - private String typeName(WrapperOptions options) { - final BasicPluralJavaType javaType = (BasicPluralJavaType) getJavaType(); - final ArrayJdbcType jdbcType = (ArrayJdbcType) getJdbcType(); + return new Binder<>( javaTypeDescriptor, + (BasicPluralJavaType) javaTypeDescriptor ); + } + + private class Binder extends BasicBinder { + private final BasicPluralJavaType pluralJavaType; + + private Binder(JavaType javaType, BasicPluralJavaType pluralJavaType) { + super( javaType, OracleArrayJdbcType.this ); + this.pluralJavaType = pluralJavaType; + } + + private String typeName(WrapperOptions options) { + final var javaType = (BasicPluralJavaType) getJavaType(); + final var jdbcType = (ArrayJdbcType) getJdbcType(); return upperTypeName == null ? getTypeName( options, javaType, jdbcType ).toUpperCase( Locale.ROOT ) : upperTypeName; - } - @Override - protected void doBindNull(PreparedStatement st, int index, WrapperOptions options) throws SQLException { - st.setNull( index, ARRAY, typeName( options ) ); - } + } - @Override - protected void doBindNull(CallableStatement st, String name, WrapperOptions options) throws SQLException { - st.setNull( name, ARRAY, typeName( options ) ); - } + @Override + protected void doBindNull(PreparedStatement st, int index, WrapperOptions options) throws SQLException { + st.setNull( index, ARRAY, typeName( options ) ); + } - @Override - protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) throws SQLException { - st.setArray( index, getBindValue( value, options ) ); - } + @Override + protected void doBindNull(CallableStatement st, String name, WrapperOptions options) throws SQLException { + st.setNull( name, ARRAY, typeName( options ) ); + } - @Override - protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) - throws SQLException { - final java.sql.Array arr = getBindValue( value, options ); - try { - st.setObject( name, arr, ARRAY ); - } - catch (SQLException ex) { - throw new HibernateException( "JDBC driver does not support named parameters for setArray. Use positional.", ex ); - } + @Override + protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) throws SQLException { + st.setArray( index, getBindValue( value, options ) ); + } + + @Override + protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) + throws SQLException { + final java.sql.Array arr = getBindValue( value, options ); + try { + st.setObject( name, arr, ARRAY ); + } + catch (SQLException ex) { + throw new HibernateException( "JDBC driver does not support named parameters for setArray. Use positional.", ex ); } + } - @Override - public java.sql.Array getBindValue(X value, WrapperOptions options) throws SQLException { - final OracleArrayJdbcType oracleArrayJdbcType = (OracleArrayJdbcType) getJdbcType(); - final Object[] objects = oracleArrayJdbcType.getArray( this, elementBinder, value, options ); - final String arrayTypeName = typeName( options ); - final OracleConnection oracleConnection = - options.getSession().getJdbcCoordinator().getLogicalConnection().getPhysicalConnection() - .unwrap( OracleConnection.class ); - try { - return oracleConnection.createOracleArray( arrayTypeName, objects ); - } - catch (Exception e) { - throw new HibernateException( "Couldn't create a java.sql.Array", e ); - } + @Override + public java.sql.Array getBindValue(X value, WrapperOptions options) throws SQLException { + final var elementBinder = getElementJdbcType().getBinder( pluralJavaType.getElementJavaType() ); + final var objects = convertToArray( this, elementBinder, pluralJavaType, value, options ); + final String arrayTypeName = typeName( options ); + final var oracleConnection = + options.getSession().getJdbcCoordinator().getLogicalConnection().getPhysicalConnection() + .unwrap( OracleConnection.class ); + try { + return oracleConnection.createOracleArray( arrayTypeName, objects ); } - }; + catch (Exception e) { + throw new HibernateException( "Couldn't create a java.sql.Array", e ); + } + } } @Override @@ -153,17 +160,18 @@ static String getTypeName(WrapperOptions options, BasicPluralJavaType contain } static String getTypeName(BasicType elementType, Dialect dialect) { - final BasicValueConverter converter = elementType.getValueConverter(); - if ( converter != null ) { - return dialect.getArrayTypeName( - converterClassName( converter ), - null, // not needed by OracleDialect.getArrayTypeName() - null // not needed by OracleDialect.getArrayTypeName() - ); - } - else { - return getTypeName( elementType.getJavaTypeDescriptor(), elementType.getJdbcType(), dialect ); - } + final var converter = elementType.getValueConverter(); + return converter != null + ? dialect.getArrayTypeName( + converterClassName( converter ), + null, // not needed by OracleDialect.getArrayTypeName() + null // not needed by OracleDialect.getArrayTypeName() + ) + : getTypeName( + elementType.getJavaTypeDescriptor(), + elementType.getJdbcType(), + dialect + ); } private static String converterClassName(BasicValueConverter converter) { @@ -181,7 +189,7 @@ static String getTypeName(JavaType elementJavaType, JdbcType elementJdbcType, } private static String arrayClassName(JavaType elementJavaType, JdbcType elementJdbcType, Dialect dialect) { - final Class javaClass = elementJavaType.getJavaTypeClass(); + final var javaClass = elementJavaType.getJavaTypeClass(); if ( javaClass.isArray() ) { return dialect.getArrayTypeName( javaClass.getComponentType().getSimpleName(), @@ -193,20 +201,23 @@ else if ( elementJdbcType instanceof StructuredJdbcType structJdbcType ) { return structJdbcType.getStructTypeName(); } else { - final Class preferredJavaTypeClass = elementJdbcType.getPreferredJavaTypeClass( null ); + final var preferredJavaTypeClass = + elementJdbcType.getPreferredJavaTypeClass( null ); if ( preferredJavaTypeClass == javaClass) { return javaClass.getSimpleName(); } else { if ( preferredJavaTypeClass.isArray() ) { - return javaClass.getSimpleName() + dialect.getArrayTypeName( - preferredJavaTypeClass.getComponentType().getSimpleName(), - null, - null - ); + return javaClass.getSimpleName() + + dialect.getArrayTypeName( + preferredJavaTypeClass.getComponentType().getSimpleName(), + null, + null + ); } else { - return javaClass.getSimpleName() + preferredJavaTypeClass.getSimpleName(); + return javaClass.getSimpleName() + + preferredJavaTypeClass.getSimpleName(); } } } @@ -219,18 +230,18 @@ public void addAuxiliaryDatabaseObjects( Size columnSize, Database database, JdbcTypeIndicators context) { - final JdbcType elementJdbcType = getElementJdbcType(); - if ( elementJdbcType instanceof StructuredJdbcType ) { - // OracleAggregateSupport will take care of contributing the auxiliary database object - return; + final var elementJdbcType = getElementJdbcType(); + if ( !(elementJdbcType instanceof StructuredJdbcType) ) { + final var dialect = database.getDialect(); + final var pluralJavaType = (BasicPluralJavaType) javaType; + final var elementJavaType = pluralJavaType.getElementJavaType(); + final String elementTypeName = + elementType( elementJavaType, elementJdbcType, columnSize, context.getTypeConfiguration(), + dialect ); + final String arrayTypeName = arrayTypeName( elementJavaType, elementJdbcType, dialect ); + createUserDefinedArrayType( arrayTypeName, elementTypeName, columnSize, elementJdbcType, database ); } - final Dialect dialect = database.getDialect(); - final BasicPluralJavaType pluralJavaType = (BasicPluralJavaType) javaType; - final JavaType elementJavaType = pluralJavaType.getElementJavaType(); - final String elementTypeName = - elementType( elementJavaType, elementJdbcType, columnSize, context.getTypeConfiguration(), dialect ); - final String arrayTypeName = arrayTypeName( elementJavaType, elementJdbcType, dialect ); - createUserDefinedArrayType( arrayTypeName, elementTypeName, columnSize, elementJdbcType, database ); + // else OracleAggregateSupport will take care of contributing the auxiliary database object } private String arrayTypeName(JavaType elementJavaType, JdbcType elementJdbcType, Dialect dialect) { @@ -241,8 +252,8 @@ private String arrayTypeName(JavaType elementJavaType, JdbcType elementJdbcTy private void createUserDefinedArrayType( String arrayTypeName, String elementTypeName, Size columnSize, JdbcType elementJdbcType, Database database) { - final Namespace defaultNamespace = database.getDefaultNamespace(); - final UserDefinedArrayType userDefinedArrayType = + final var defaultNamespace = database.getDefaultNamespace(); + final var userDefinedArrayType = defaultNamespace.createUserDefinedArrayType( toIdentifier( arrayTypeName ), name -> new UserDefinedArrayType( "orm", defaultNamespace, name ) @@ -275,7 +286,7 @@ public void registerOutParameter(CallableStatement callableStatement, int index) @Override public String getExtraCreateTableInfo(JavaType javaType, String columnName, String tableName, Database database) { - final BasicPluralJavaType pluralJavaType = (BasicPluralJavaType) javaType; + final var pluralJavaType = (BasicPluralJavaType) javaType; return getElementJdbcType() .getExtraCreateTableInfo( pluralJavaType.getElementJavaType(), columnName, tableName, database ); } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/type/OracleArrayJdbcTypeConstructor.java b/hibernate-core/src/main/java/org/hibernate/dialect/type/OracleArrayJdbcTypeConstructor.java index 9b7b358806c6..990b5a929825 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/type/OracleArrayJdbcTypeConstructor.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/type/OracleArrayJdbcTypeConstructor.java @@ -54,7 +54,7 @@ public JdbcType resolveType( precision = columnTypeInformation.getColumnSize(); scale = columnTypeInformation.getDecimalDigits(); } - typeName = OracleArrayJdbcType.getTypeName( elementType.getJdbcRecommendedJavaTypeMapping( + typeName = OracleArrayJdbcType.getTypeName( elementType.getRecommendedJavaType( precision, scale, typeConfiguration diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/type/OracleNestedTableJdbcTypeConstructor.java b/hibernate-core/src/main/java/org/hibernate/dialect/type/OracleNestedTableJdbcTypeConstructor.java index 6bbae75b3305..a93e216dca83 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/type/OracleNestedTableJdbcTypeConstructor.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/type/OracleNestedTableJdbcTypeConstructor.java @@ -48,7 +48,7 @@ public JdbcType resolveType( precision = columnTypeInformation.getColumnSize(); scale = columnTypeInformation.getDecimalDigits(); } - typeName = OracleArrayJdbcType.getTypeName( elementType.getJdbcRecommendedJavaTypeMapping( + typeName = OracleArrayJdbcType.getTypeName( elementType.getRecommendedJavaType( precision, scale, typeConfiguration diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/type/OracleUserDefinedTypeExporter.java b/hibernate-core/src/main/java/org/hibernate/dialect/type/OracleUserDefinedTypeExporter.java index 4808017f80ce..0641293a735c 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/type/OracleUserDefinedTypeExporter.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/type/OracleUserDefinedTypeExporter.java @@ -30,6 +30,7 @@ /** * @author Christian Beikov + * @author Yoobin Yoon */ public class OracleUserDefinedTypeExporter extends StandardUserDefinedTypeExporter { @@ -220,6 +221,53 @@ public String[] getSqlCreateStrings( "end loop; " + "return res; " + "end;", + "create or replace function " + arrayTypeName + "_reverse(arr in " + arrayTypeName + + ") return " + arrayTypeName + " deterministic is " + + "res " + arrayTypeName + ":=" + arrayTypeName + "(); begin " + + "if arr is null then return null; end if; " + + "for i in reverse 1 .. arr.count loop " + + "res.extend; " + + "res(res.count) := arr(i); " + + "end loop; " + + "return res; " + + "end;", + "create or replace function " + arrayTypeName + "_sort(" + + "arr in " + arrayTypeName + "," + + "p_descending in number default 0," + + "p_nulls_first in number default null" + + ") return " + arrayTypeName + " deterministic is " + + "v_result " + arrayTypeName + "; " + + "v_nulls_first number; " + + "begin " + + "if arr is null then return null; end if; " + + "if p_nulls_first is null then " + + "v_nulls_first := p_descending; " + + "else " + + "v_nulls_first := p_nulls_first; " + + "end if; " + + "if p_descending = 0 then " + + "if v_nulls_first = 0 then " + + "select cast(multiset(select column_value from table(arr) " + + "order by column_value asc nulls last) as " + arrayTypeName + ") " + + "into v_result from dual; " + + "else " + + "select cast(multiset(select column_value from table(arr) " + + "order by column_value asc nulls first) as " + arrayTypeName + ") " + + "into v_result from dual; " + + "end if; " + + "else " + + "if v_nulls_first = 0 then " + + "select cast(multiset(select column_value from table(arr) " + + "order by column_value desc nulls last) as " + arrayTypeName + ") " + + "into v_result from dual; " + + "else " + + "select cast(multiset(select column_value from table(arr) " + + "order by column_value desc nulls first) as " + arrayTypeName + ") " + + "into v_result from dual; " + + "end if; " + + "end if; " + + "return v_result; " + + "end;", "create or replace function " + arrayTypeName + "_fill(elem in " + getRawTypeName( elementType ) + ", elems number) return " + arrayTypeName + " deterministic is " + "res " + arrayTypeName + ":=" + arrayTypeName + "(); begin " + @@ -303,6 +351,8 @@ public String[] getSqlDropStrings(UserDefinedArrayType userDefinedType, Metadata buildDropFunctionSqlString(arrayTypeName + "_slice"), buildDropFunctionSqlString(arrayTypeName + "_replace"), buildDropFunctionSqlString(arrayTypeName + "_trim"), + buildDropFunctionSqlString(arrayTypeName + "_reverse"), + buildDropFunctionSqlString(arrayTypeName + "_sort"), buildDropFunctionSqlString(arrayTypeName + "_fill"), buildDropFunctionSqlString(arrayTypeName + "_positions"), buildDropFunctionSqlString(arrayTypeName + "_to_string"), @@ -395,7 +445,7 @@ protected String createOrReplaceConcatFunction(String arrayTypeName) { } protected String createOrReplaceConcatFunction(String arrayTypeName, int maxConcatParams) { - final StringBuilder sb = new StringBuilder(); + final var sb = new StringBuilder(); sb.append( "create or replace function " ).append( arrayTypeName ).append( "_concat(" ); sb.append( "arr0 in " ).append( arrayTypeName ).append( ",arr1 in " ).append( arrayTypeName ); for ( int i = 2; i < maxConcatParams; i++ ) { diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/type/PostgreSQLArrayJdbcType.java b/hibernate-core/src/main/java/org/hibernate/dialect/type/PostgreSQLArrayJdbcType.java index 4f02999d4d0b..25fd637c1891 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/type/PostgreSQLArrayJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/type/PostgreSQLArrayJdbcType.java @@ -10,7 +10,6 @@ import java.sql.Types; import org.hibernate.HibernateException; -import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.type.descriptor.ValueBinder; import org.hibernate.type.descriptor.WrapperOptions; import org.hibernate.type.descriptor.java.BasicPluralJavaType; @@ -31,62 +30,66 @@ public PostgreSQLArrayJdbcType(JdbcType elementJdbcType) { @Override public ValueBinder getBinder(final JavaType javaTypeDescriptor) { - @SuppressWarnings("unchecked") - final BasicPluralJavaType pluralJavaType = (BasicPluralJavaType) javaTypeDescriptor; - final ValueBinder elementBinder = getElementJdbcType().getBinder( pluralJavaType.getElementJavaType() ); - return new BasicBinder<>( javaTypeDescriptor, this ) { + return new Binder<>( javaTypeDescriptor, + (BasicPluralJavaType) javaTypeDescriptor ); + } - @Override - protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) throws SQLException { - st.setArray( index, getArray( value, options ) ); - } + private class Binder extends BasicBinder { + private final BasicPluralJavaType pluralJavaType; - @Override - protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) - throws SQLException { - final java.sql.Array arr = getArray( value, options ); - try { - st.setObject( name, arr, java.sql.Types.ARRAY ); - } - catch (SQLException ex) { - throw new HibernateException( "JDBC driver does not support named parameters for setArray. Use positional.", ex ); - } - } + private Binder(JavaType javaType, BasicPluralJavaType pluralJavaType) { + super( javaType, PostgreSQLArrayJdbcType.this ); + this.pluralJavaType = pluralJavaType; + } + + @Override + protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) throws SQLException { + st.setArray( index, getArray( value, options ) ); + } - @Override - public Object getBindValue(X value, WrapperOptions options) throws SQLException { - return ( (PostgreSQLArrayJdbcType) getJdbcType() ).getArray( this, elementBinder, value, options ); + @Override + protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) + throws SQLException { + final java.sql.Array arr = getArray( value, options ); + try { + st.setObject( name, arr, java.sql.Types.ARRAY ); } + catch (SQLException ex) { + throw new HibernateException( "JDBC driver does not support named parameters for setArray. Use positional.", ex ); + } + } + + @Override + public Object[] getBindValue(X value, WrapperOptions options) throws SQLException { + final var elementBinder = getElementJdbcType().getBinder( pluralJavaType.getElementJavaType() ); + return convertToArray( this, elementBinder, pluralJavaType, value, options ); + } - private java.sql.Array getArray(X value, WrapperOptions options) throws SQLException { - final PostgreSQLArrayJdbcType arrayJdbcType = (PostgreSQLArrayJdbcType) getJdbcType(); - final Object[] objects; + private java.sql.Array getArray(X value, WrapperOptions options) throws SQLException { + final var session = options.getSession(); + return session.getJdbcCoordinator().getLogicalConnection().getPhysicalConnection() + .createArrayOf( getElementTypeName( getJavaType(), session ), + elements( value, options, PostgreSQLArrayJdbcType.this ) ); + } - final JdbcType elementJdbcType = arrayJdbcType.getElementJdbcType(); - if ( elementJdbcType instanceof AggregateJdbcType aggregateJdbcType ) { - // The PostgreSQL JDBC driver does not support arrays of structs, which contain byte[] - final Object[] domainObjects = getJavaType().unwrap( - value, - Object[].class, - options - ); - objects = new Object[domainObjects.length]; - for ( int i = 0; i < domainObjects.length; i++ ) { - if ( domainObjects[i] != null ) { - objects[i] = aggregateJdbcType.createJdbcValue( domainObjects[i], options ); - } + private Object[] elements(X value, WrapperOptions options, PostgreSQLArrayJdbcType arrayJdbcType) + throws SQLException { + final var elementJdbcType = arrayJdbcType.getElementJdbcType(); + if ( elementJdbcType instanceof AggregateJdbcType aggregateJdbcType ) { + // The PostgreSQL JDBC driver does not support arrays of structs, which contain byte[] + final var domainObjects = getJavaType().unwrap( value, Object[].class, options ); + final var objects = new Object[domainObjects.length]; + for ( int i = 0; i < domainObjects.length; i++ ) { + if ( domainObjects[i] != null ) { + objects[i] = aggregateJdbcType.createJdbcValue( domainObjects[i], options ); } } - else { - objects = arrayJdbcType.getArray( this, elementBinder, value, options ); - } - - final SharedSessionContractImplementor session = options.getSession(); - final String typeName = arrayJdbcType.getElementTypeName( getJavaType(), session ); - return session.getJdbcCoordinator().getLogicalConnection().getPhysicalConnection() - .createArrayOf( typeName, objects ); + return objects; + } + else { + return getBindValue( value, options ); } - }; + } } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/type/SpannerJsonJdbcType.java b/hibernate-core/src/main/java/org/hibernate/dialect/type/SpannerJsonJdbcType.java new file mode 100644 index 000000000000..e2aae14b6fce --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/type/SpannerJsonJdbcType.java @@ -0,0 +1,70 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.type; + +import org.hibernate.metamodel.mapping.EmbeddableMappingType; +import org.hibernate.metamodel.spi.RuntimeModelCreationContext; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.descriptor.ValueBinder; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.jdbc.AggregateJdbcType; +import org.hibernate.type.descriptor.jdbc.BasicBinder; +import org.hibernate.type.descriptor.jdbc.JsonJdbcType; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +public class SpannerJsonJdbcType extends JsonJdbcType { + private static final int VENDOR_TYPE_NUMBER = 100011; + + public static final SpannerJsonJdbcType INSTANCE = new SpannerJsonJdbcType( null ); + + public SpannerJsonJdbcType(EmbeddableMappingType embeddableMappingType) { + super( embeddableMappingType ); + } + + @Override + public int getDdlTypeCode() { + return SqlTypes.JSON; + } + + @Override + public AggregateJdbcType resolveAggregateJdbcType( + EmbeddableMappingType mappingType, + String sqlType, + RuntimeModelCreationContext creationContext) { + return new SpannerJsonJdbcType( mappingType ); + } + + @Override + public ValueBinder getBinder(JavaType javaType) { + return new BasicBinder<>( javaType, this ) { + @Override + protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) + throws java.sql.SQLException { + final String json = ( (SpannerJsonJdbcType) getJdbcType() ).toString( value, getJavaType(), options ); + st.setObject( index, json, VENDOR_TYPE_NUMBER ); + } + + @Override + protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) throws SQLException { + final String json = ( (SpannerJsonJdbcType) getJdbcType() ).toString( value, getJavaType(), options ); + st.setObject( name, json, VENDOR_TYPE_NUMBER ); + } + + @Override + protected void doBindNull(PreparedStatement st, int index, WrapperOptions options) throws SQLException { + st.setNull( index, VENDOR_TYPE_NUMBER ); + } + + @Override + protected void doBindNull(CallableStatement st, String name, WrapperOptions options) throws SQLException { + st.setNull( name, VENDOR_TYPE_NUMBER ); + } + }; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/unique/AlterTableUniqueDelegate.java b/hibernate-core/src/main/java/org/hibernate/dialect/unique/AlterTableUniqueDelegate.java index ca126f56e4d2..e68c3cad2c90 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/unique/AlterTableUniqueDelegate.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/unique/AlterTableUniqueDelegate.java @@ -5,12 +5,18 @@ package org.hibernate.dialect.unique; import org.hibernate.boot.Metadata; +import org.hibernate.boot.model.naming.Identifier; +import org.hibernate.boot.model.naming.NamingHelper; +import org.hibernate.boot.model.relational.Database; import org.hibernate.boot.model.relational.SqlStringGenerationContext; import org.hibernate.dialect.Dialect; import org.hibernate.mapping.Column; import org.hibernate.mapping.Table; import org.hibernate.mapping.UniqueKey; +import java.util.ArrayList; +import java.util.List; + import static org.hibernate.internal.util.StringHelper.isNotEmpty; /** @@ -29,6 +35,25 @@ public AlterTableUniqueDelegate(Dialect dialect ) { this.dialect = dialect; } + static String constraintName(UniqueKey uniqueKey, Database database) { + final String uniqueKeyName = uniqueKey.getName(); + if ( uniqueKeyName == null ) { + final List columnIdentifiers = new ArrayList<>(); + for ( var column : uniqueKey.getColumns() ) { + columnIdentifiers.add( column.getNameIdentifier( database ) ); + } + return NamingHelper.INSTANCE.generateHashedConstraintName("UK", + uniqueKey.getTable().getNameIdentifier(), columnIdentifiers ); + } + else { + return database.getDialect().quote( uniqueKeyName ); + } + } + + static String tableName(UniqueKey uniqueKey, SqlStringGenerationContext context) { + return context.format( uniqueKey.getTable().getQualifiedTableName() ); + } + // legacy model ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @Override @@ -44,19 +69,19 @@ public String getTableCreationUniqueConstraintsFragment(Table table, } @Override - public String getAlterTableToAddUniqueKeyCommand(UniqueKey uniqueKey, Metadata metadata, + public String getAlterTableToAddUniqueKeyCommand( + UniqueKey uniqueKey, Metadata metadata, SqlStringGenerationContext context) { - final String tableName = context.format( uniqueKey.getTable().getQualifiedTableName() ); - final String constraintName = dialect.quote( uniqueKey.getName() ); - return dialect.getAlterTableString( tableName ) - + " add constraint " + constraintName + " " + uniqueConstraintSql( uniqueKey ); + return dialect.getAlterTableString( tableName( uniqueKey, context ) ) + + " add constraint " + constraintName( uniqueKey, metadata.getDatabase() ) + + " " + uniqueConstraintSql( uniqueKey ); } protected String uniqueConstraintSql(UniqueKey uniqueKey) { - final StringBuilder fragment = new StringBuilder(); + final var fragment = new StringBuilder(); fragment.append( "unique (" ); boolean first = true; - for ( Column column : uniqueKey.getColumns() ) { + for ( var column : uniqueKey.getColumns() ) { if ( first ) { first = false; } @@ -78,22 +103,23 @@ protected String uniqueConstraintSql(UniqueKey uniqueKey) { @Override public String getAlterTableToDropUniqueKeyCommand(UniqueKey uniqueKey, Metadata metadata, SqlStringGenerationContext context) { - final String tableName = context.format( uniqueKey.getTable().getQualifiedTableName() ); - final StringBuilder command = new StringBuilder( dialect.getAlterTableString(tableName) ); + final String tableName = tableName( uniqueKey, context ); + final String constraintName = constraintName( uniqueKey, metadata.getDatabase() ); + final var command = new StringBuilder( dialect.getAlterTableString( tableName ) ); command.append( ' ' ); command.append( dialect.getDropUniqueKeyString() ); if ( dialect.supportsIfExistsBeforeConstraintName() ) { command.append( " if exists " ); - command.append( dialect.quote( uniqueKey.getName() ) ); + command.append( constraintName ); } else if ( dialect.supportsIfExistsAfterConstraintName() ) { command.append( ' ' ); - command.append( dialect.quote( uniqueKey.getName() ) ); + command.append( constraintName ); command.append( " if exists" ); } else { command.append( ' ' ); - command.append( dialect.quote( uniqueKey.getName() ) ); + command.append( constraintName ); } return command.toString(); } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/unique/AlterTableUniqueIndexDelegate.java b/hibernate-core/src/main/java/org/hibernate/dialect/unique/AlterTableUniqueIndexDelegate.java index f4e8ed5c92c1..a105ec625925 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/unique/AlterTableUniqueIndexDelegate.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/unique/AlterTableUniqueIndexDelegate.java @@ -7,13 +7,8 @@ import org.hibernate.boot.Metadata; import org.hibernate.boot.model.relational.SqlStringGenerationContext; import org.hibernate.dialect.Dialect; -import org.hibernate.mapping.Column; import org.hibernate.mapping.UniqueKey; -import java.util.List; -import java.util.Map; - -import static org.hibernate.internal.util.StringHelper.qualify; import static org.hibernate.internal.util.StringHelper.unqualify; /** @@ -25,6 +20,8 @@ *
  • SQL Server does allow unique constraints on nullable columns, but the semantics * are that two null values are non-unique. So here we need to jump through hoops with the * {@code create unique nonclustered index ... where ...} command. + *
  • Spanner does not allow unique column definition, but it does allow the creation of unique + * indexes instead, using {@code create unique index ...} * * * @author Brett Meyer @@ -37,28 +34,29 @@ public AlterTableUniqueIndexDelegate(Dialect dialect ) { @Override public String getAlterTableToAddUniqueKeyCommand(UniqueKey uniqueKey, Metadata metadata, SqlStringGenerationContext context) { - if ( uniqueKey.hasNullableColumn() ) { - final Dialect dialect = context.getDialect(); - final String name = uniqueKey.getName(); - final String tableName = context.format( uniqueKey.getTable().getQualifiedTableName() ); - final List columns = uniqueKey.getColumns(); - final Map columnOrderMap = uniqueKey.getColumnOrderMap(); - final StringBuilder statement = + final var dialect = context.getDialect(); + if ( needsUniqueIndex( uniqueKey, dialect ) ) { + final String constraintName = constraintName( uniqueKey, metadata.getDatabase() ); + final var statement = new StringBuilder( dialect.getCreateIndexString( true ) ) .append( " " ) - .append( dialect.qualifyIndexName() ? name : unqualify( name ) ) + .append( dialect.qualifyIndexName() + ? constraintName + : unqualify( constraintName ) ) .append( " on " ) - .append( tableName ) + .append( tableName( uniqueKey, context ) ) .append( " (" ); + final var columns = uniqueKey.getColumns(); + final var columnOrderMap = uniqueKey.getColumnOrderMap(); boolean first = true; - for ( Column column : columns ) { + for ( var column : columns ) { if ( first ) { first = false; } else { statement.append(", "); } - statement.append( column.getQuotedName(dialect) ); + statement.append( column.getQuotedName( dialect ) ); if ( columnOrderMap.containsKey( column ) ) { statement.append( " " ).append( columnOrderMap.get( column ) ); } @@ -75,13 +73,22 @@ public String getAlterTableToAddUniqueKeyCommand(UniqueKey uniqueKey, Metadata m @Override public String getAlterTableToDropUniqueKeyCommand(UniqueKey uniqueKey, Metadata metadata, SqlStringGenerationContext context) { - if ( uniqueKey.hasNullableColumn() ) { - final String tableName = context.format( uniqueKey.getTable().getQualifiedTableName() ); - return "drop index " + qualify( tableName, uniqueKey.getName() ); + if ( needsUniqueIndex( uniqueKey, context.getDialect() ) ) { + final var statement = new StringBuilder().append( "drop index " ); + if ( dialect.supportsIfExistsBeforeIndexName() ) { + statement.append( "if exists " ); + } + statement.append( tableName( uniqueKey, context ) ).append( '.' ) + .append( constraintName( uniqueKey, metadata.getDatabase() ) ); + return statement.toString(); } else { return super.getAlterTableToDropUniqueKeyCommand( uniqueKey, metadata, context ); } } + private boolean needsUniqueIndex(UniqueKey uniqueKey, Dialect dialect) { + return uniqueKey.hasNullableColumn() || !dialect.supportsUniqueConstraints(); + } + } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/unique/CreateTableUniqueDelegate.java b/hibernate-core/src/main/java/org/hibernate/dialect/unique/CreateTableUniqueDelegate.java index 97d1f0a77cc5..75683dfb6669 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/unique/CreateTableUniqueDelegate.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/unique/CreateTableUniqueDelegate.java @@ -58,7 +58,7 @@ public String getTableCreationUniqueConstraintsFragment(Table table, SqlStringGe // then getColumnDefinitionUniquenessFragment() already handled it, and // so we don't need to bother creating a constraint. The only downside // to this is that if the user added a column marked unique=true to a - // named unique constraint, then the name gets lost. Unfortunately the + // named unique constraint, then the name gets lost. Unfortunately, the // signature of getColumnDefinitionUniquenessFragment() doesn't let me // detect this case. (But that would be easy to fix!) if ( !isSingleColumnUnique( table, uniqueKey ) ) { diff --git a/hibernate-core/src/main/java/org/hibernate/engine/config/internal/ConfigurationServiceImpl.java b/hibernate-core/src/main/java/org/hibernate/engine/config/internal/ConfigurationServiceImpl.java index 005ee176c523..ed0ac5d4fecc 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/config/internal/ConfigurationServiceImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/config/internal/ConfigurationServiceImpl.java @@ -66,42 +66,49 @@ public void injectServices(ServiceRegistryImplementor serviceRegistry) { return target !=null ? target : defaultValue; } - @SuppressWarnings("unchecked") public @Nullable T cast(Class expected, @Nullable Object candidate){ - if (candidate == null) { + if ( candidate == null ) { return null; } + else if ( expected.isInstance( candidate ) ) { + return expected.cast( candidate ); + } + else { + final var target = getTargetClass( expected, candidate ); + return target == null ? null : instantiate( expected, target ); - if ( expected.isInstance( candidate ) ) { - return (T) candidate; } + } + + private static @Nullable T instantiate(Class expected, Class target) { + try { + return target.getDeclaredConstructor().newInstance(); + } + catch (Exception e) { + //TODO: should this really be a debug-level + // log instead of a proper error? + CORE_LOGGER.debugf( "Unable to instantiate %s class %s", + expected.getName(), target.getName() ); + return null; + } + } - Class target; - if (candidate instanceof Class) { - target = (Class) candidate; + private @Nullable Class getTargetClass(Class expected, Object candidate) { + if ( candidate instanceof Class candidateClass ) { + return candidateClass.asSubclass( expected ); } else { try { - target = serviceRegistry.requireService( ClassLoaderService.class ) + return serviceRegistry.requireService( ClassLoaderService.class ) .classForName( candidate.toString() ); } - catch ( ClassLoadingException e ) { + catch (ClassLoadingException e) { + //TODO: should this really be a debug-level + // log instead of a proper error? CORE_LOGGER.debugf( "Unable to locate %s implementation class %s", expected.getName(), candidate.toString() ); - target = null; + return null; } } - if ( target != null ) { - try { - return target.newInstance(); - } - catch ( Exception e ) { - CORE_LOGGER.debugf( "Unable to instantiate %s class %s", - expected.getName(), target.getName() ); - } - } - return null; } - - } diff --git a/hibernate-core/src/main/java/org/hibernate/engine/creation/CommonBuilder.java b/hibernate-core/src/main/java/org/hibernate/engine/creation/CommonBuilder.java index dc9aedb051ec..af93563e3b2c 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/creation/CommonBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/creation/CommonBuilder.java @@ -12,8 +12,10 @@ import org.hibernate.Session; import org.hibernate.SharedSessionContract; import org.hibernate.StatelessSession; +import org.hibernate.cfg.StateManagementSettings; import java.sql.Connection; +import java.time.Instant; import java.util.TimeZone; import java.util.function.UnaryOperator; @@ -170,4 +172,30 @@ public interface CommonBuilder { /// /// @return `this`, for method chaining CommonBuilder jdbcTimeZone(TimeZone timeZone); + + /** + * Specify the instant for reading + * {@linkplain org.hibernate.annotations.Temporal temporal} entity data. + * Instances of temporal entities retrieved in the session will represent + * the revisions effective at the given instant. + * + * @see org.hibernate.annotations.Temporal + */ + @Incubating + CommonBuilder asOf(Instant instant); + + /** + * Specify the + * {@linkplain StateManagementSettings#TRANSACTION_ID_SUPPLIER + * transaction id} for reading {@linkplain org.hibernate.annotations.Temporal + * temporal} entity data. Instances of temporal entities retrieved in the + * session will represent the revisions effective at the end of the given + * transaction. + * The given value should match the type returned by the configured + * transaction id supplier. + * + * @see org.hibernate.annotations.Temporal + */ + @Incubating + CommonBuilder atTransaction(Object transactionId); } diff --git a/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/AbstractCommonBuilder.java b/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/AbstractCommonBuilder.java index 134e25de0c3b..18dc0442ca86 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/AbstractCommonBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/AbstractCommonBuilder.java @@ -15,6 +15,7 @@ import org.hibernate.resource.jdbc.spi.StatementInspector; import java.sql.Connection; +import java.time.Instant; import java.util.TimeZone; import java.util.function.UnaryOperator; @@ -36,6 +37,7 @@ public abstract class AbstractCommonBuilder implements protected boolean readOnly; protected CacheMode cacheMode; protected TimeZone jdbcTimeZone; + protected Object temporalIdentifier; public AbstractCommonBuilder(SessionFactoryImplementor factory) { sessionFactory = factory; @@ -161,4 +163,16 @@ public T jdbcTimeZone(TimeZone timeZone) { jdbcTimeZone = timeZone; return getThis(); } + + @Override + public T asOf(Instant instant) { + this.temporalIdentifier = instant; + return getThis(); + } + + @Override + public T atTransaction(Object transactionId) { + this.temporalIdentifier = transactionId; + return getThis(); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/SessionBuilderImpl.java b/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/SessionBuilderImpl.java index 5a23d775850c..dfde5093e26d 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/SessionBuilderImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/SessionBuilderImpl.java @@ -138,6 +138,11 @@ public TimeZone getJdbcTimeZone() { return jdbcTimeZone; } + @Override + public Object getTemporalIdentifier() { + return temporalIdentifier; + } + @Override public List getCustomSessionEventListeners() { return listeners; diff --git a/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/SessionCreationOptions.java b/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/SessionCreationOptions.java index b09fede8a06e..bf21484aa16b 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/SessionCreationOptions.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/SessionCreationOptions.java @@ -56,6 +56,8 @@ public interface SessionCreationOptions { TimeZone getJdbcTimeZone(); + Object getTemporalIdentifier(); + /** * @return the full list of SessionEventListener if this was customized, * or null if this Session is being created with the default list. diff --git a/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/SessionCreationOptionsAdaptor.java b/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/SessionCreationOptionsAdaptor.java index 2235305d3053..067fd2562164 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/SessionCreationOptionsAdaptor.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/SessionCreationOptionsAdaptor.java @@ -150,4 +150,9 @@ public TransactionCompletionCallbacksImplementor getTransactionCompletionCallbac public void registerParentSessionObserver(ParentSessionObserver observer) { registerParentSessionObserver( observer, originalSession ); } + + @Override + public Object getTemporalIdentifier() { + return null; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/SharedSessionBuilderImpl.java b/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/SharedSessionBuilderImpl.java index 8f529e21436e..194442e1a9df 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/SharedSessionBuilderImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/SharedSessionBuilderImpl.java @@ -68,6 +68,7 @@ public SharedSessionBuilderImpl(SharedSessionContractImplementor original) { identifierRollback = original.isIdentifierRollbackEnabled(); // good idea to inherit this jdbcTimeZone = original.getJdbcTimeZone(); + temporalIdentifier = original.getLoadQueryInfluencers().getTemporalIdentifier(); } protected abstract SessionImplementor createSession(); @@ -81,7 +82,7 @@ protected SharedSessionBuilderImplementor getThis() { // SharedSessionBuilder @Override - public SessionImplementor openSession() { + public SessionImplementor open() { CORE_LOGGER.openingSession( tenantIdentifier ); if ( original.getFactory().getSessionFactoryOptions().isMultiTenancyEnabled() ) { if ( shareTransactionContext ) { @@ -98,6 +99,11 @@ public SessionImplementor openSession() { return createSession(); } + @Override + public SessionImplementor openSession() { + return open(); + } + @Override @Deprecated(forRemoval = true) @SuppressWarnings("removal") @@ -381,6 +387,11 @@ public TimeZone getJdbcTimeZone() { return jdbcTimeZone; } + @Override + public Object getTemporalIdentifier() { + return temporalIdentifier; + } + @Override public List getCustomSessionEventListeners() { return listeners; diff --git a/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/SharedStatelessSessionBuilderImpl.java b/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/SharedStatelessSessionBuilderImpl.java index 585579dbf1a8..9af5a6c25f93 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/SharedStatelessSessionBuilderImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/SharedStatelessSessionBuilderImpl.java @@ -92,7 +92,7 @@ public SharedStatelessSessionBuilder interceptor() { @Override public SharedStatelessSessionBuilder statementInspector() { - this.statementInspector = original.getJdbcSessionContext().getStatementInspector(); + statementInspector = original.getJdbcSessionContext().getStatementInspector(); return this; } diff --git a/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/StatelessSessionBuilderImpl.java b/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/StatelessSessionBuilderImpl.java index 40fc388ebcfa..66ff4cad73b9 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/StatelessSessionBuilderImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/creation/internal/StatelessSessionBuilderImpl.java @@ -146,6 +146,11 @@ public TimeZone getJdbcTimeZone() { return jdbcTimeZone; } + @Override + public Object getTemporalIdentifier() { + return temporalIdentifier; + } + @Override public List getCustomSessionEventListeners() { return null; diff --git a/hibernate-core/src/main/java/org/hibernate/engine/creation/spi/SessionBuilderImplementor.java b/hibernate-core/src/main/java/org/hibernate/engine/creation/spi/SessionBuilderImplementor.java index d9b869a65dca..b0802ee0d051 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/creation/spi/SessionBuilderImplementor.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/creation/spi/SessionBuilderImplementor.java @@ -16,6 +16,7 @@ import org.hibernate.resource.jdbc.spi.StatementInspector; import java.sql.Connection; +import java.time.Instant; import java.util.TimeZone; import java.util.function.UnaryOperator; @@ -99,4 +100,10 @@ public interface SessionBuilderImplementor extends SessionBuilder { @Override SessionBuilderImplementor subselectFetchEnabled(boolean subselectFetchEnabled); + + @Override + SessionBuilderImplementor asOf(Instant instant); + + @Override + SessionBuilderImplementor atTransaction(Object transactionId); } diff --git a/hibernate-core/src/main/java/org/hibernate/engine/creation/spi/SharedSessionBuilderImplementor.java b/hibernate-core/src/main/java/org/hibernate/engine/creation/spi/SharedSessionBuilderImplementor.java index 9b4529b0b7df..7187e9fd50b7 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/creation/spi/SharedSessionBuilderImplementor.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/creation/spi/SharedSessionBuilderImplementor.java @@ -15,6 +15,7 @@ import org.hibernate.resource.jdbc.spi.StatementInspector; import java.sql.Connection; +import java.time.Instant; import java.util.TimeZone; import java.util.function.UnaryOperator; @@ -117,4 +118,10 @@ public interface SharedSessionBuilderImplementor extends SharedSessionBuilder, S @Override SharedSessionBuilderImplementor subselectFetchEnabled(boolean subselectFetchEnabled); + + @Override + SharedSessionBuilderImplementor asOf(Instant instant); + + @Override + SharedSessionBuilderImplementor atTransaction(Object transactionId); } diff --git a/hibernate-core/src/main/java/org/hibernate/engine/internal/AbstractTransactionCompletionProcessQueue.java b/hibernate-core/src/main/java/org/hibernate/engine/internal/AbstractTransactionCompletionProcessQueue.java index a5b706b9c107..ac785e78ceb9 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/internal/AbstractTransactionCompletionProcessQueue.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/internal/AbstractTransactionCompletionProcessQueue.java @@ -18,8 +18,8 @@ */ abstract class AbstractTransactionCompletionProcessQueue { SharedSessionContractImplementor session; - // Concurrency handling required when transaction completion process is dynamically registered - // inside event listener (HHH-7478). + // Concurrency handling required when the transaction completion process + // is dynamically registered inside an event listener (HHH-7478). ConcurrentLinkedQueue<@NonNull T> processes = new ConcurrentLinkedQueue<>(); AbstractTransactionCompletionProcessQueue(SharedSessionContractImplementor session) { diff --git a/hibernate-core/src/main/java/org/hibernate/engine/internal/AfterTransactionCompletionProcessQueue.java b/hibernate-core/src/main/java/org/hibernate/engine/internal/AfterTransactionCompletionProcessQueue.java index ddcd55f70d84..8a9a3f57024c 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/internal/AfterTransactionCompletionProcessQueue.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/internal/AfterTransactionCompletionProcessQueue.java @@ -5,9 +5,8 @@ package org.hibernate.engine.internal; import org.hibernate.HibernateException; -import org.hibernate.action.internal.BulkOperationCleanupAction; +import org.hibernate.action.internal.BulkOperationCleanupAction.BulkOperationCleanUpAfterTransactionCompletionProcess; import org.hibernate.cache.CacheException; -import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.engine.spi.TransactionCompletionCallbacks.AfterCompletionCallback; @@ -15,6 +14,7 @@ import java.util.Set; import static org.hibernate.internal.CoreMessageLogger.CORE_LOGGER; +import static org.hibernate.internal.util.collections.ArrayHelper.EMPTY_STRING_ARRAY; /** * Encapsulates behavior needed for after transaction processing @@ -40,57 +40,54 @@ boolean hasActions() { void afterTransactionCompletion(boolean success) { AfterCompletionCallback process; while ( (process = processes.poll()) != null ) { - try { - process.doAfterTransactionCompletion( success, session ); - } - catch (CacheException ce) { - CORE_LOGGER.unableToReleaseCacheLock( ce ); - // continue loop - } - catch (Exception e) { - throw new HibernateException( - "Unable to perform afterTransactionCompletion callback: " + e.getMessage(), e ); - } + callAfterCompletion( success, process ); } + invalidateCaches(); + } - final SessionFactoryImplementor factory = session.getFactory(); - if ( factory.getSessionFactoryOptions().isQueryCacheEnabled() ) { - factory.getCache().getTimestampsCache() - .invalidate( querySpacesToInvalidate.toArray( new String[0] ), session ); + void executePendingBulkOperationCleanUpActions() { + if ( performBulkOperationCallbacks() ) { + invalidateCaches(); } - querySpacesToInvalidate.clear(); } - void executePendingBulkOperationCleanUpActions() { - AfterCompletionCallback process; + private boolean performBulkOperationCallbacks() { boolean hasPendingBulkOperationCleanUpActions = false; - while ( ( process = processes.poll() ) != null ) { - if ( process instanceof BulkOperationCleanupAction.BulkOperationCleanUpAfterTransactionCompletionProcess ) { - try { - hasPendingBulkOperationCleanUpActions = true; - process.doAfterTransactionCompletion( true, session ); - } - catch (CacheException ce) { - CORE_LOGGER.unableToReleaseCacheLock( ce ); - // continue loop - } - catch (Exception e) { - throw new HibernateException( - "Unable to perform afterTransactionCompletion callback: " + e.getMessage(), - e - ); + var iterator = processes.iterator(); + while ( iterator.hasNext() ) { + var process = iterator.next(); + if ( process instanceof BulkOperationCleanUpAfterTransactionCompletionProcess ) { + hasPendingBulkOperationCleanUpActions = true; + if ( callAfterCompletion( true, process ) ) { + iterator.remove(); } } } + return hasPendingBulkOperationCleanUpActions; + } - if ( hasPendingBulkOperationCleanUpActions ) { - if ( session.getFactory().getSessionFactoryOptions().isQueryCacheEnabled() ) { - session.getFactory().getCache().getTimestampsCache().invalidate( - querySpacesToInvalidate.toArray( new String[0] ), - session - ); - } - querySpacesToInvalidate.clear(); + private boolean callAfterCompletion(boolean success, AfterCompletionCallback process) { + try { + process.doAfterTransactionCompletion( success, session ); + return true; + } + catch (CacheException ce) { + CORE_LOGGER.unableToReleaseCacheLock( ce ); + // continue loop + return false; + } + catch (Exception e) { + throw new HibernateException( + "Unable to perform afterTransactionCompletion callback: " + e.getMessage(), e ); } } + + private void invalidateCaches() { + final var factory = session.getFactory(); + if ( factory.getSessionFactoryOptions().isQueryCacheEnabled() ) { + factory.getCache().getTimestampsCache(). + invalidate( querySpacesToInvalidate.toArray( EMPTY_STRING_ARRAY ), session ); + } + querySpacesToInvalidate.clear(); + } } 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 d490df522cc4..b646310bc2fd 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 @@ -322,8 +322,9 @@ private static void cascadeLogicalOneToOneOrphanRemoval( if ( child == null || loadedValue != null && child != loadedValue ) { EntityEntry valueEntry = persistenceContext.getEntry( loadedValue ); if ( valueEntry == null && isHibernateProxy( loadedValue ) ) { - // un-proxy and re-associate for cascade operation + // unproxy and reassociate for cascade operation // useful for @OneToOne defined as FetchType.LAZY + //TODO: what should really happen here??? loadedValue = persistenceContext.unproxyAndReassociate( loadedValue ); valueEntry = persistenceContext.getEntry( loadedValue ); // HHH-11965 @@ -565,9 +566,7 @@ private static void cascadeCollectionElements( final T anything, final boolean isCascadeDeleteEnabled) throws HibernateException { - final boolean reallyDoCascade = style.reallyDoCascade( action ) - && child != CollectionType.UNFETCHED_COLLECTION; - if ( reallyDoCascade ) { + if ( style.reallyDoCascade( action ) ) { final boolean traceEnabled = CORE_LOGGER.isTraceEnabled(); if ( traceEnabled ) { CORE_LOGGER.cascadingCollection( action, collectionType.getRole() ); diff --git a/hibernate-core/src/main/java/org/hibernate/engine/internal/Collections.java b/hibernate-core/src/main/java/org/hibernate/engine/internal/Collections.java index 083168527b00..90d82ba69b8e 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/internal/Collections.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/internal/Collections.java @@ -240,22 +240,14 @@ private static void prepareCollectionForUpdate( if ( loadedPersister != null || currentPersister != null ) { // it is or was referenced _somewhere_ - // if either its role changed, or its key changed + // if either its role changed or its key changed final boolean ownerChanged = loadedPersister != currentPersister || wasKeyChanged( collectionEntry, factory, currentPersister ); if ( ownerChanged ) { - // do a check - final boolean orphanDeleteAndRoleChanged = - loadedPersister != null && currentPersister != null && loadedPersister.hasOrphanDelete(); - - if ( orphanDeleteAndRoleChanged ) { - throw new HibernateException( - "Don't change the reference to a collection with delete orphan enabled: " - + loadedPersister.getRole() - ); - } + // do some checks + checkOnChangedOwner( collection, collectionEntry, loadedPersister, currentPersister ); // do the work if ( currentPersister != null ) { @@ -278,6 +270,37 @@ else if ( collection.isDirty() ) { } } + private static void checkOnChangedOwner(PersistentCollection collection, CollectionEntry collectionEntry, CollectionPersister loadedPersister, CollectionPersister currentPersister) { + final boolean immutableDereferenced = + collectionEntry.isReadOnly() + && loadedPersister != null + && !isOwnerDeletedOrGone( collection ); + if ( immutableDereferenced ) { + throw new HibernateException( "Immutable collection dereferenced by owner: " + + collectionInfoString( loadedPersister.getRole(), collectionEntry.getLoadedKey() ) ); + } + + + final boolean orphanDeleteAndRoleChanged = + loadedPersister != null + && currentPersister != null + && loadedPersister.hasOrphanDelete(); + if ( orphanDeleteAndRoleChanged ) { + throw new HibernateException( + "Collection with orphan orphan delete enabled has modifier owner: " + + collectionInfoString( loadedPersister.getRole(), collectionEntry.getLoadedKey() ) ); + } + } + + private static boolean isOwnerDeletedOrGone(PersistentCollection collection) { + final Object owner = collection.getOwner(); + assert owner != null; + final var session = collection.getSession(); + assert session != null; + final var entry = session.getPersistenceContextInternal().getEntry( owner ); + return entry != null && entry.getStatus().isDeletedOrGone(); + } + /** * Check if the key changed. * Excludes marking key changed when the loaded key is a {@code DelayedPostInsertIdentifier}. diff --git a/hibernate-core/src/main/java/org/hibernate/engine/internal/EntityEntryContext.java b/hibernate-core/src/main/java/org/hibernate/engine/internal/EntityEntryContext.java index 535836d0a35e..fcf4decfeff3 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/internal/EntityEntryContext.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/internal/EntityEntryContext.java @@ -161,14 +161,13 @@ private static boolean isReferenceCachingEnabled(EntityPersister persister) { private ManagedEntity getAssociatedManagedEntity(Object entity) { if ( isManagedEntity( entity ) ) { final var managedEntity = asManagedEntity( entity ); - if ( managedEntity.$$_hibernate_getEntityEntry() == null ) { - // it is not associated - return null; - } final var entityEntry = (EntityEntryImpl) managedEntity.$$_hibernate_getEntityEntry(); - + if ( entityEntry == null ) { + // it is not associated + return null; + } if ( entityEntry.getPersister().isMutable() ) { return entityEntry.getPersistenceContext() == persistenceContext ? managedEntity // it is associated @@ -449,12 +448,14 @@ public void serialize(ObjectOutputStream oos) throws IOException { var managedEntity = head; while ( managedEntity != null ) { // so we know whether or not to build a ManagedEntityImpl on deserialize - oos.writeBoolean( managedEntity == managedEntity.$$_hibernate_getEntityInstance() ); - oos.writeObject( managedEntity.$$_hibernate_getEntityInstance() ); + final var instance = managedEntity.$$_hibernate_getEntityInstance(); + oos.writeBoolean( managedEntity == instance ); + oos.writeObject( instance ); // we need to know which implementation of EntityEntry is being serialized - oos.writeInt( managedEntity.$$_hibernate_getEntityEntry().getClass().getName().length() ); - oos.writeChars( managedEntity.$$_hibernate_getEntityEntry().getClass().getName() ); - managedEntity.$$_hibernate_getEntityEntry().serialize( oos ); + final var entry = managedEntity.$$_hibernate_getEntityEntry(); + oos.writeInt( entry.getClass().getName().length() ); + oos.writeChars( entry.getClass().getName() ); + entry.serialize( oos ); managedEntity = managedEntity.$$_hibernate_getNextManagedEntity(); } } diff --git a/hibernate-core/src/main/java/org/hibernate/engine/internal/EntityEntryExtraStateHolder.java b/hibernate-core/src/main/java/org/hibernate/engine/internal/EntityEntryExtraStateHolder.java index 6a53f3f79b0d..95ba26e31fff 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/internal/EntityEntryExtraStateHolder.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/internal/EntityEntryExtraStateHolder.java @@ -36,13 +36,13 @@ public void addExtraState(EntityEntryExtraState extraState) { } } - @Override @SuppressWarnings("unchecked") + @Override public T getExtraState(Class extraStateType) { if ( next == null ) { return null; } - if ( extraStateType.isAssignableFrom( next.getClass() ) ) { - return (T) next; + if ( extraStateType.isInstance( next ) ) { + return extraStateType.cast( next ); } else { return next.getExtraState( extraStateType ); diff --git a/hibernate-core/src/main/java/org/hibernate/engine/internal/EntityEntryImpl.java b/hibernate-core/src/main/java/org/hibernate/engine/internal/EntityEntryImpl.java index f32adf748e9b..0691aadbae32 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/internal/EntityEntryImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/internal/EntityEntryImpl.java @@ -437,8 +437,9 @@ public boolean isReadOnly() { } @Override - public void setReadOnly(boolean readOnly, Object entity) { - if ( readOnly != isReadOnly() ) { + public boolean setReadOnly(boolean readOnly, Object entity) { + final boolean changed = readOnly != isReadOnly(); + if ( changed ) { if ( readOnly ) { setStatus( READ_ONLY ); loadedState = null; @@ -468,6 +469,7 @@ else if ( !persister.isMutable() ) { } } } + return changed; } @Override @@ -548,13 +550,13 @@ public void addExtraState(EntityEntryExtraState extraState) { } } - @Override @SuppressWarnings("unchecked") + @Override public T getExtraState(Class extraStateType) { if ( next == null ) { return null; } - else if ( extraStateType.isAssignableFrom( next.getClass() ) ) { - return (T) next; + else if ( extraStateType.isInstance( next ) ) { + return extraStateType.cast( next ); } else { return next.getExtraState( extraStateType ); 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 bf806dc1e2b8..22ddd82e497f 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 @@ -137,7 +137,7 @@ private Object nullifyCompositeType(Object value, String propertyName, Component } if ( substitute ) { // todo : need to account for entity mode on the CompositeType interface :( - compositeType.setPropertyValues( value, subvalues ); + return compositeType.replacePropertyValues( value, subvalues, session ); } return value; } diff --git a/hibernate-core/src/main/java/org/hibernate/engine/internal/NaturalIdLogging.java b/hibernate-core/src/main/java/org/hibernate/engine/internal/NaturalIdLogging.java index f6efdfc2e508..b530a60db8e5 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/internal/NaturalIdLogging.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/internal/NaturalIdLogging.java @@ -15,6 +15,7 @@ import org.jboss.logging.annotations.ValidIdRange; import java.lang.invoke.MethodHandles; +import java.util.Locale; import static org.jboss.logging.Logger.Level.DEBUG; import static org.jboss.logging.Logger.Level.TRACE; @@ -36,7 +37,8 @@ public interface NaturalIdLogging extends BasicLogger { NaturalIdLogging NATURAL_ID_LOGGER = Logger.getMessageLogger( MethodHandles.lookup(), NaturalIdLogging.class, - LOGGER_NAME + LOGGER_NAME, + Locale.ROOT ); @LogMessage(level = TRACE) diff --git a/hibernate-core/src/main/java/org/hibernate/engine/internal/NaturalIdResolutionsImpl.java b/hibernate-core/src/main/java/org/hibernate/engine/internal/NaturalIdResolutionsImpl.java index 16be10ec9430..d6ead424401d 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/internal/NaturalIdResolutionsImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/internal/NaturalIdResolutionsImpl.java @@ -5,6 +5,7 @@ package org.hibernate.engine.internal; import org.hibernate.AssertionFailure; +import org.hibernate.CacheMode; import org.hibernate.cache.spi.access.NaturalIdDataAccess; import org.hibernate.cache.spi.access.SoftLock; import org.hibernate.engine.spi.CachedNaturalIdValueSource; @@ -31,7 +32,7 @@ import static org.hibernate.engine.internal.CacheHelper.fromSharedCache; import static org.hibernate.engine.internal.NaturalIdLogging.NATURAL_ID_LOGGER; -class NaturalIdResolutionsImpl implements NaturalIdResolutions, Serializable { +public class NaturalIdResolutionsImpl implements NaturalIdResolutions, Serializable { private final StatefulPersistenceContext persistenceContext; private final ConcurrentHashMap resolutionsByEntity = new ConcurrentHashMap<>(); @@ -54,6 +55,14 @@ private SharedSessionContractImplementor session() { return persistenceContext.getSession(); } + public EntityResolutions getEntityResolutions(EntityMappingType entityMappingType) { + return resolutionsByEntity.get( entityMappingType ); + } + + public EntityResolutions getEntityResolutions(Class entityType) { + return getEntityResolutions( session().getFactory().getMappingMetamodel().getEntityDescriptor( entityType ) ); + } + @Override public boolean cacheResolution(Object id, Object naturalId, EntityMappingType entityDescriptor) { validateNaturalId( entityDescriptor, naturalId ); @@ -407,12 +416,15 @@ private void cacheFromLoad( final var session = session(); // prevent identical re-caching if ( fromSharedCache( session, cacheKey, persister, cacheAccess ) == null ) { + final boolean minimalPutsEnabled = + session.getFactory().getSessionFactoryOptions().isMinimalPutsEnabled() + && session.getCacheMode() != CacheMode.REFRESH; final var statistics = session.getFactory().getStatistics(); final var eventMonitor = session.getEventMonitor(); boolean put = false; final var cachePutEvent = eventMonitor.beginCachePutEvent(); try { - put = cacheAccess.putFromLoad( session, cacheKey, id, null ); + put = cacheAccess.putFromLoad( session, cacheKey, id, null, minimalPutsEnabled ); if ( put && statistics.isStatisticsEnabled() ) { statistics.naturalIdCachePut( rootEntityDescriptor.getNavigableRole(), @@ -709,7 +721,7 @@ public void clear() { /** * Represents the entity-specific cross-reference cache. */ - private static class EntityResolutions implements Serializable { + public static class EntityResolutions implements Serializable { private final PersistenceContext persistenceContext; private final EntityMappingType entityDescriptor; @@ -732,6 +744,25 @@ public EntityPersister getPersister() { return getEntityDescriptor().getEntityPersister(); } + /** + * Used for testing. + */ + public Resolution getResolutionByPk(Object pk) { + return pkToNaturalIdMap.get( pk ); + } + + /** + * Used for testing. + */ + public Object getIdResolutionByNaturalId(Object naturalId) { + for ( var entry : pkToNaturalIdMap.entrySet() ) { + if ( entry.getValue().getNaturalIdValue().equals( naturalId ) ) { + return entry.getKey(); + } + } + return null; + } + public boolean sameAsCached(Object pk, Object naturalIdValues) { if ( pk == null ) { return false; diff --git a/hibernate-core/src/main/java/org/hibernate/engine/internal/PersistenceContextLogging.java b/hibernate-core/src/main/java/org/hibernate/engine/internal/PersistenceContextLogging.java index d10fd1fedd73..a5dba6d7528c 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/internal/PersistenceContextLogging.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/internal/PersistenceContextLogging.java @@ -15,6 +15,7 @@ import org.jboss.logging.annotations.ValidIdRange; import java.lang.invoke.MethodHandles; +import java.util.Locale; import static org.jboss.logging.Logger.Level.DEBUG; import static org.jboss.logging.Logger.Level.TRACE; @@ -33,7 +34,7 @@ public interface PersistenceContextLogging extends BasicLogger { String NAME = SubSystemLogging.BASE + ".persistenceContext"; - PersistenceContextLogging PERSISTENCE_CONTEXT_LOGGER = Logger.getMessageLogger( MethodHandles.lookup(), PersistenceContextLogging.class, NAME ); + PersistenceContextLogging PERSISTENCE_CONTEXT_LOGGER = Logger.getMessageLogger( MethodHandles.lookup(), PersistenceContextLogging.class, NAME, Locale.ROOT ); @LogMessage(level = TRACE) @Message("Setting proxy identifier: %s") diff --git a/hibernate-core/src/main/java/org/hibernate/engine/internal/ProxyUtil.java b/hibernate-core/src/main/java/org/hibernate/engine/internal/ProxyUtil.java new file mode 100644 index 000000000000..8704e10a3c87 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/engine/internal/ProxyUtil.java @@ -0,0 +1,107 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.engine.internal; + +import org.hibernate.DetachedObjectException; +import org.hibernate.PersistentObjectException; +import org.hibernate.bytecode.enhance.spi.interceptor.BytecodeLazyAttributeInterceptor; +import org.hibernate.bytecode.enhance.spi.interceptor.EnhancementAsProxyLazinessInterceptor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; + +import static org.hibernate.engine.internal.ManagedTypeHelper.asPersistentAttributeInterceptable; +import static org.hibernate.engine.internal.ManagedTypeHelper.isPersistentAttributeInterceptable; +import static org.hibernate.proxy.HibernateProxy.extractLazyInitializer; + +/** + * @author Gavin King + * @since 7.2 + */ +public class ProxyUtil { + + /** + * Get the entity instance underlying the given proxy, throwing + * an exception if the proxy is uninitialized. If the given + * object is not a proxy, simply return the argument. + */ + public static Object assertInitialized(Object maybeProxy) { + final var lazyInitializer = extractLazyInitializer( maybeProxy ); + if ( lazyInitializer != null ) { + if ( lazyInitializer.isUninitialized() ) { + throw new PersistentObjectException( "Object was an uninitialized proxy for " + + lazyInitializer.getEntityName() ); + } + //unwrap the object and return + return lazyInitializer.getImplementation(); + } + else { + return maybeProxy; + } + } + + /** + * Get the entity instance underlying the given proxy, forcing + * initialization if the proxy is uninitialized. If the given + * object is not a proxy, simply return the argument. + * @throws DetachedObjectException if the given proxy does not + * belong to the given session + */ + public static Object forceInitialize(Object maybeProxy, SharedSessionContractImplementor session) { + final var lazyInitializer = extractLazyInitializer( maybeProxy ); + if ( lazyInitializer != null ) { + if ( lazyInitializer.getSession() != session ) { + throw new DetachedObjectException( "Given proxy does not belong to this persistence context" ); + } + //initialize + unwrap the object and return it + return lazyInitializer.getImplementation(); + } + else if ( isPersistentAttributeInterceptable( maybeProxy ) ) { + final var interceptor = + asPersistentAttributeInterceptable( maybeProxy ) + .$$_hibernate_getInterceptor(); + if ( interceptor instanceof EnhancementAsProxyLazinessInterceptor lazinessInterceptor ) { + if ( lazinessInterceptor.getLinkedSession() != session ) { + throw new DetachedObjectException( "Given proxy does not belong to this persistence context" ); + } + lazinessInterceptor.forceInitialize( maybeProxy, null ); + } + return maybeProxy; + } + else { + return maybeProxy; + } + } + + /** + * Determine of the given proxy is uninitialized. If the given + * object is not a proxy, simply return false. + * @throws DetachedObjectException if the given proxy does not + * belong to the given session + */ + public static boolean isUninitialized(Object value, SharedSessionContractImplementor session) { + // could be a proxy + final var lazyInitializer = extractLazyInitializer( value ); + if ( lazyInitializer != null ) { + if ( lazyInitializer.getSession() != session ) { + throw new DetachedObjectException( "Given proxy does not belong to this persistence context" ); + } + return lazyInitializer.isUninitialized(); + } + // or an uninitialized enhanced entity ("bytecode proxy") + else if ( isPersistentAttributeInterceptable( value ) ) { + final var interceptor = + (BytecodeLazyAttributeInterceptor) + asPersistentAttributeInterceptable( value ) + .$$_hibernate_getInterceptor(); + if ( interceptor != null && interceptor.getLinkedSession() != session ) { + throw new DetachedObjectException( "Given proxy does not belong to this persistence context" ); + } + return interceptor instanceof EnhancementAsProxyLazinessInterceptor enhancementInterceptor + && !enhancementInterceptor.isInitialized(); + } + else { + return false; + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/engine/internal/SessionMetricsLogger.java b/hibernate-core/src/main/java/org/hibernate/engine/internal/SessionMetricsLogger.java index 1bc782582807..7cbf0f358d45 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/internal/SessionMetricsLogger.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/internal/SessionMetricsLogger.java @@ -12,6 +12,7 @@ import org.jboss.logging.annotations.MessageLogger; import java.lang.invoke.MethodHandles; +import java.util.Locale; import static org.jboss.logging.Logger.Level.DEBUG; @@ -23,7 +24,7 @@ public interface SessionMetricsLogger extends BasicLogger { String LOGGER_NAME = "org.hibernate.session.metrics"; - SessionMetricsLogger SESSION_METRICS_LOGGER = Logger.getMessageLogger( MethodHandles.lookup(), SessionMetricsLogger.class, LOGGER_NAME ); + SessionMetricsLogger SESSION_METRICS_LOGGER = Logger.getMessageLogger( MethodHandles.lookup(), SessionMetricsLogger.class, LOGGER_NAME, Locale.ROOT ); @LogMessage(level = DEBUG) @Message( 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 1470d7afd928..ee74c85bb8e3 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 @@ -8,6 +8,7 @@ import java.io.InvalidObjectException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; +import java.io.Serial; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; @@ -28,7 +29,6 @@ import org.hibernate.LockMode; import org.hibernate.MappingException; import org.hibernate.NonUniqueObjectException; -import org.hibernate.PersistentObjectException; import org.hibernate.bytecode.enhance.spi.interceptor.BytecodeLazyAttributeInterceptor; import org.hibernate.bytecode.enhance.spi.interceptor.EnhancementAsProxyLazinessInterceptor; import org.hibernate.collection.spi.PersistentCollection; @@ -52,7 +52,6 @@ import org.hibernate.internal.util.collections.InstanceIdentityMap; import org.hibernate.persister.collection.CollectionPersister; 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.exec.spi.Callback; @@ -72,6 +71,7 @@ import static org.hibernate.engine.internal.PersistenceContextLogging.PERSISTENCE_CONTEXT_LOGGER; import static org.hibernate.internal.util.collections.CollectionHelper.mapOfSize; import static org.hibernate.internal.util.collections.CollectionHelper.setOfSize; +import static org.hibernate.pretty.MessageHelper.infoString; import static org.hibernate.proxy.HibernateProxy.extractLazyInitializer; /** @@ -87,22 +87,32 @@ */ class StatefulPersistenceContext implements PersistenceContext { + /** + * Marker object used to indicate (via reference checking) that no row was returned. + */ + private static final Serializable NO_ROW = new Serializable() { + @Override + public String toString() { + return "NO_ROW"; + } + + @Serial + public Object readResolve() { + return NO_ROW; + } + }; + private static final int INIT_COLL_SIZE = 8; - /* - Eagerly Initialized Fields - the following fields are used in all circumstances, and are not worth (or not suited) to being converted into lazy - */ + // Eagerly initialized fields. The following fields are used in every circumstance + // and are not worth (or not suited) to being converted to lazy initialization. + private final SharedSessionContractImplementor session; private EntityEntryContext entityEntryContext; - /* - Everything else below should be carefully initialized only on first need; - this optimisation is very effective as null checks are free, while allocation costs - are very often the dominating cost of an application using ORM. - This is not general advice, but it's worth the added maintenance burden in this case - as this is a very central component of our library. - */ + // Everything else below should be carefully initialized only on first need. + // This optimization is very effective as null checks are free, while allocation + // costs are very often the dominating cost of an application using ORM. // Loaded entity instances, by EntityKey private HashMap entitiesByKey; @@ -113,8 +123,7 @@ the following fields are used in all circumstances, and are not worth (or not su // Loaded entity instances, by EntityUniqueKey private HashMap entitiesByUniqueKey; - - // Snapshots of current database state for entities + // Snapshots of the current database state for entities // that have *not* been loaded private HashMap entitySnapshotsByKey; @@ -136,7 +145,7 @@ the following fields are used in all circumstances, and are not worth (or not su // Set of EntityKeys of deleted unloaded proxies private HashSet deletedUnloadedEntityKeys; - // properties that we have tried to load, and not found in the database + // properties that we have tried to load and not found in the database private HashSet nullAssociations; // A list of collection wrappers that were instantiating during result set @@ -209,14 +218,6 @@ public boolean hasLoadContext() { return loadContexts != null; } -// @Override -// public void addUnownedCollection(CollectionKey key, PersistentCollection collection) { -// if ( unownedCollections == null ) { -// unownedCollections = CollectionHelper.mapOfSize( INIT_COLL_SIZE ); -// } -// unownedCollections.put( key, collection ); -// } -// @Override public PersistentCollection useUnownedCollection(CollectionKey key) { return unownedCollections == null ? null : unownedCollections.remove( key ); @@ -297,10 +298,11 @@ public void setDefaultReadOnly(boolean defaultReadOnly) { @Override public void setEntryStatus(EntityEntry entry, Status status) { entry.setStatus( status ); - setHasNonReadOnlyEnties( status ); + setHasNonReadOnlyEntities( status ); + // TODO: can/should we also set its collections to read-only? } - private void setHasNonReadOnlyEnties(Status status) { + private void setHasNonReadOnlyEntities(Status status) { if ( status==Status.DELETED || status==Status.MANAGED || status==Status.SAVING ) { hasNonReadOnlyEntities = true; } @@ -376,7 +378,7 @@ public Object[] getCachedDatabaseSnapshot(EntityKey key) { if ( snapshot == NO_ROW ) { throw new IllegalStateException( "persistence context reported no row snapshot for " - + MessageHelper.infoString( key.getEntityName(), key.getIdentifier() ) + + infoString( key.getEntityName(), key.getIdentifier() ) ); } return (Object[]) snapshot; @@ -656,7 +658,7 @@ public EntityEntry addEntry( this ); entityEntryContext.addEntityEntry( entity, entityEntry ); - setHasNonReadOnlyEnties( status ); + setHasNonReadOnlyEntities( status ); return entityEntry; } @@ -667,7 +669,7 @@ public EntityEntry addReferenceEntry( final var entityEntry = asManagedEntity( entity ).$$_hibernate_getEntityEntry(); entityEntry.setStatus( status ); entityEntryContext.addEntityEntry( entity, entityEntry ); - setHasNonReadOnlyEnties( status ); + setHasNonReadOnlyEntities( status ); return entityEntry; } @@ -691,26 +693,35 @@ public boolean containsProxy(Object entity) { @Override public boolean reassociateIfUninitializedProxy(Object value) throws MappingException { - if ( !Hibernate.isInitialized( value ) ) { - // could be a proxy - final var lazyInitializer = extractLazyInitializer( value ); - if ( lazyInitializer != null ) { + if ( Hibernate.isInitialized( value ) ) { + return false; + } + // could be a proxy + final var lazyInitializer = extractLazyInitializer( value ); + if ( lazyInitializer != null ) { + final boolean uninitialized = lazyInitializer.isUninitialized(); + if ( uninitialized ) { reassociateProxy( lazyInitializer, asHibernateProxy( value ) ); - return true; } - // or an uninitialized enhanced entity ("bytecode proxy") - if ( isPersistentAttributeInterceptable( value ) ) { - final var bytecodeProxy = asPersistentAttributeInterceptable( value ); - final var interceptor = - (BytecodeLazyAttributeInterceptor) - bytecodeProxy.$$_hibernate_getInterceptor(); - if ( interceptor != null ) { - interceptor.setSession( getSession() ); - } - return true; + return uninitialized; + } + // or an uninitialized enhanced entity ("bytecode proxy") + else if ( isPersistentAttributeInterceptable( value ) ) { + final var interceptor = + (BytecodeLazyAttributeInterceptor) + asPersistentAttributeInterceptable( value ) + .$$_hibernate_getInterceptor(); + final boolean uninitialized = + interceptor instanceof EnhancementAsProxyLazinessInterceptor enhancementInterceptor + && !enhancementInterceptor.isInitialized(); + if ( uninitialized ) { + interceptor.setSession( getSession() ); } + return uninitialized; + } + else { + return false; } - return false; } @Override @@ -750,22 +761,6 @@ private void reassociateProxy(LazyInitializer li, HibernateProxy proxy) { } } - @Override - public Object unproxy(Object maybeProxy) throws HibernateException { - final var lazyInitializer = extractLazyInitializer( maybeProxy ); - if ( lazyInitializer != null ) { - if ( lazyInitializer.isUninitialized() ) { - throw new PersistentObjectException( "object was an uninitialized proxy for " - + lazyInitializer.getEntityName() ); - } - //unwrap the object and return - return lazyInitializer.getImplementation(); - } - else { - return maybeProxy; - } - } - @Override public Object unproxyAndReassociate(final Object maybeProxy) throws HibernateException { final var lazyInitializer = extractLazyInitializer( maybeProxy ); @@ -935,7 +930,7 @@ else if ( ownerPersister.isInstance( key ) ) { } else { // b) try by EntityKey, which means we need to resolve owner-key -> collection-key - // IMPL NOTE : yes if we get here this impl is very non-performant, but PersistenceContext + // IMPL NOTE: yes if we get here this impl is very non-performant, but PersistenceContext // was never designed to handle this case; adding that capability for real means splitting // the notions of: // 1) collection key @@ -999,8 +994,12 @@ private Object getLoadedCollectionOwnerIdOrNull(CollectionEntry collectionEntry) } @Override - public void addUninitializedCollection(CollectionPersister persister, PersistentCollection collection, Object id) { - final var collectionEntry = new CollectionEntry( collection, persister, id, flushing ); + public void addUninitializedCollection( + CollectionPersister persister, + PersistentCollection collection, + Object id, + boolean readOnly) { + final var collectionEntry = new CollectionEntry( collection, persister, id, flushing, readOnly ); addCollection( collection, collectionEntry, id ); if ( session.getLoadQueryInfluencers().effectivelyBatchLoadable( persister ) ) { getBatchFetchQueue().addBatchLoadableCollection( collection, collectionEntry ); @@ -1038,6 +1037,7 @@ public void replaceCollection(CollectionPersister persister, PersistentCollectio ? new CollectionEntry( collection, session.getFactory() ) // A newly wrapped collection : new CollectionEntry( persister, collection ); + entry.setReadOnly( oldEntry.isReadOnly(), collection ); putCollectionEntry( collection, entry ); final Object key = collection.getKey(); if ( key != null ) { @@ -1102,9 +1102,13 @@ public void addInitializedDetachedCollection(CollectionPersister collectionPersi } @Override - public CollectionEntry addInitializedCollection(CollectionPersister persister, PersistentCollection collection, Object id) + public CollectionEntry addInitializedCollection( + CollectionPersister persister, + PersistentCollection collection, + Object id, + boolean readOnly) throws HibernateException { - final var collectionEntry = new CollectionEntry( collection, persister, id, flushing ); + final var collectionEntry = new CollectionEntry( collection, persister, id, flushing, readOnly ); collectionEntry.postInitialize( collection, session ); addCollection( collection, collectionEntry, id ); return collectionEntry; @@ -1228,14 +1232,6 @@ public Object removeProxy(EntityKey key) { return removeProxyByKey( key ); } -// @Override -// public HashSet getNullifiableEntityKeys() { -// if ( nullifiableEntityKeys == null ) { -// nullifiableEntityKeys = new HashSet<>(); -// } -// return nullifiableEntityKeys; -// } - /** * @deprecated this will be removed: it provides too wide access, making it hard to optimise the internals * for specific access needs. Consider using #iterateEntities instead. @@ -1727,8 +1723,19 @@ private void setEntityReadOnly(Object entity, boolean readOnly) { if ( entry == null ) { throw new IllegalArgumentException( "Given entity is not associated with the persistence context" ); } - entry.setReadOnly( readOnly, entity ); - hasNonReadOnlyEntities = hasNonReadOnlyEntities || ! readOnly; + if ( entry.setReadOnly( readOnly, entity ) ) { + hasNonReadOnlyEntities = hasNonReadOnlyEntities || !readOnly; + if ( collectionEntries != null && entry.getPersister().hasCollections() ) { + forEachCollectionEntry( + (collection, collectionEntry) -> { + if ( collection.getOwner() == entity ) { + collectionEntry.setReadOnly( readOnly, collection ); + } + }, + false + ); + } + } } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/engine/internal/VersionLogger.java b/hibernate-core/src/main/java/org/hibernate/engine/internal/VersionLogger.java index 950767dcf29c..49dfa605b707 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/internal/VersionLogger.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/internal/VersionLogger.java @@ -14,6 +14,7 @@ import org.jboss.logging.annotations.ValidIdRange; import java.lang.invoke.MethodHandles; +import java.util.Locale; import static org.jboss.logging.Logger.Level.TRACE; @@ -26,7 +27,7 @@ @Internal public interface VersionLogger extends BasicLogger { String LOGGER_NAME = SubSystemLogging.BASE + ".versioning"; - VersionLogger INSTANCE = Logger.getMessageLogger( MethodHandles.lookup(), VersionLogger.class, LOGGER_NAME ); + VersionLogger INSTANCE = Logger.getMessageLogger( MethodHandles.lookup(), VersionLogger.class, LOGGER_NAME, Locale.ROOT ); @LogMessage(level = TRACE) @Message(value = "Seeding version: %s", id = 160001) diff --git a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/JdbcLogging.java b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/JdbcLogging.java index 5c69484fa99f..f9d131bf138b 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/JdbcLogging.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/JdbcLogging.java @@ -18,6 +18,7 @@ import java.lang.invoke.MethodHandles; import java.sql.SQLException; +import java.util.Locale; import static org.jboss.logging.Logger.Level.DEBUG; import static org.jboss.logging.Logger.Level.INFO; @@ -39,7 +40,7 @@ public interface JdbcLogging extends BasicLogger { String NAME = SubSystemLogging.BASE + ".jdbc"; - JdbcLogging JDBC_LOGGER = Logger.getMessageLogger( MethodHandles.lookup(), JdbcLogging.class, NAME ); + JdbcLogging JDBC_LOGGER = Logger.getMessageLogger( MethodHandles.lookup(), JdbcLogging.class, NAME, Locale.ROOT ); @LogMessage(level = WARN) @Message( @@ -76,6 +77,10 @@ public interface JdbcLogging extends BasicLogger { @Message(value = "Closing unreleased batch in JdbcCoordinator @%s", id = 100008) void closingUnreleasedBatch(int hashCode); + @LogMessage(level = DEBUG) + @Message(value = "Skipping aggressive release in AFTER_STATEMENT mode (%s)", id = 100010) + void skippingAggressiveRelease(String reason); + @LogMessage(level = DEBUG) @Message(value = """ Database: @@ -192,4 +197,9 @@ public interface JdbcLogging extends BasicLogger { @LogMessage(level = TRACE) @Message(value = "AutoCommit was initially %s", id = 100047) void initialAutoCommit(boolean wasInitiallyAutoCommit); + + + @LogMessage(level = TRACE) + @Message(value = "ResultSet statement was not registered (on register)", id = 100048) + void resultSetStatementWasNotRegistered(); } 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 1a693908d5fb..86c5adfca3f1 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,6 +15,7 @@ import org.jboss.logging.annotations.ValidIdRange; import java.lang.invoke.MethodHandles; +import java.util.Locale; import static org.jboss.logging.Logger.Level.INFO; import static org.jboss.logging.Logger.Level.TRACE; @@ -35,7 +36,7 @@ public interface JdbcBatchLogging extends BasicLogger { String NAME = "org.hibernate.orm.jdbc.batch"; - JdbcBatchLogging BATCH_MESSAGE_LOGGER = Logger.getMessageLogger( MethodHandles.lookup(), JdbcBatchLogging.class, NAME ); + JdbcBatchLogging BATCH_MESSAGE_LOGGER = Logger.getMessageLogger( MethodHandles.lookup(), JdbcBatchLogging.class, NAME, Locale.ROOT ); @LogMessage(level = INFO) @Message(id=100501, value = "Automatic JDBC statement batching enabled (maximum batch size %s)") 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 bb6ce470199a..0665e0386ebb 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 @@ -17,6 +17,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.jdbc.spi.SqlExceptionHelper; import org.hibernate.engine.jdbc.spi.SqlStatementLogger; @@ -38,6 +39,7 @@ public class BatchImpl implements Batch { private final JdbcCoordinator jdbcCoordinator; private final SqlStatementLogger sqlStatementLogger; private final SqlExceptionHelper sqlExceptionHelper; + private final JdbcServices jdbcServices; private final LinkedHashSet observers = new LinkedHashSet<>(); @@ -58,7 +60,7 @@ public BatchImpl( this.jdbcCoordinator = jdbcCoordinator; this.statementGroup = statementGroup; - final var jdbcServices = + this.jdbcServices = jdbcCoordinator.getJdbcSessionOwner().getJdbcSessionContext().getJdbcServices(); sqlStatementLogger = jdbcServices.getSqlStatementLogger(); sqlExceptionHelper = jdbcServices.getSqlExceptionHelper(); @@ -255,6 +257,10 @@ protected void performExecution() { eventHandler.jdbcExecuteBatchStart(); rowCounts = statement.executeBatch(); } + catch (SQLException sqle) { + jdbcCoordinator.afterFailedStatementExecution( sqle ); + throw sqle; + } finally { eventMonitor.completeJdbcBatchExecutionEvent( executionEvent, sql ); eventHandler.jdbcExecuteBatchEnd(); diff --git a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/internal/ConnectionProviderInitiator.java b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/internal/ConnectionProviderInitiator.java index e39abf7d9278..ef0112e9ada2 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/internal/ConnectionProviderInitiator.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/internal/ConnectionProviderInitiator.java @@ -116,9 +116,7 @@ private static Class connectionProviderClass(Class throw new ConnectionProviderConfigurationException( "Class '" + providerClass.getName() + "' does not implement 'ConnectionProvider'" ); } - @SuppressWarnings("unchecked") // Safe, we just checked - final var connectionProviderClass = (Class) providerClass; - return connectionProviderClass; + return providerClass.asSubclass( ConnectionProvider.class ); } private ConnectionProvider instantiateNamedConnectionProvider( diff --git a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/internal/ConnectionProviderLogging.java b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/internal/ConnectionProviderLogging.java index b67f5059f27f..b09ce1a11dae 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/internal/ConnectionProviderLogging.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/internal/ConnectionProviderLogging.java @@ -12,6 +12,7 @@ import org.jboss.logging.annotations.ValidIdRange; import java.lang.invoke.MethodHandles; +import java.util.Locale; import static org.jboss.logging.Logger.Level.INFO; import static org.jboss.logging.Logger.Level.WARN; @@ -29,7 +30,7 @@ @ValidIdRange(min = 102001, max = 102100) interface ConnectionProviderLogging { String NAME = SubSystemLogging.BASE + ".connection"; - ConnectionProviderLogging CONNECTION_PROVIDER_LOGGER = Logger.getMessageLogger( MethodHandles.lookup(), ConnectionProviderLogging.class, NAME ); + ConnectionProviderLogging CONNECTION_PROVIDER_LOGGER = Logger.getMessageLogger( MethodHandles.lookup(), ConnectionProviderLogging.class, NAME, Locale.ROOT ); @LogMessage(level = WARN) @Message(id = 102001, diff --git a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/internal/DataSourceConnectionProvider.java b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/internal/DataSourceConnectionProvider.java index 5fb8e27e552e..960835f54d92 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/internal/DataSourceConnectionProvider.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/internal/DataSourceConnectionProvider.java @@ -23,8 +23,10 @@ import org.hibernate.service.spi.Stoppable; import static org.hibernate.cfg.JdbcSettings.DATASOURCE; +import static org.hibernate.cfg.JdbcSettings.LOGIN_TIMEOUT; import static org.hibernate.engine.jdbc.connections.internal.ConnectionProviderInitiator.toIsolationNiceName; import static org.hibernate.internal.log.ConnectionInfoLogger.CONNECTION_INFO_LOGGER; +import static org.hibernate.internal.util.config.ConfigurationHelper.getInteger; /** * A {@link ConnectionProvider} that manages connections from an underlying {@link DataSource}. @@ -73,13 +75,12 @@ public boolean isUnwrappableAs(Class unwrapType) { } @Override - @SuppressWarnings("unchecked") public T unwrap(Class unwrapType) { if ( unwrapType.isAssignableFrom( DataSourceConnectionProvider.class ) ) { - return (T) this; + return unwrapType.cast( this ); } else if ( unwrapType.isAssignableFrom( DataSource.class) ) { - return (T) getDataSource(); + return unwrapType.cast( getDataSource() ); } else { throw new UnknownUnwrapTypeException( unwrapType ); @@ -109,6 +110,16 @@ else if ( dataSourceSetting instanceof String jndiName ) { throw new ConnectionProviderConfigurationException( "Unable to determine appropriate DataSource to use" ); } + final Integer loginTimeout = getInteger( LOGIN_TIMEOUT, configuration ); + if ( loginTimeout != null ) { + try { + dataSource.setLoginTimeout( loginTimeout ); + } + catch (SQLException e) { + CONNECTION_INFO_LOGGER.couldNotSetLoginTimeout( e ); + } + } + if ( configuration.containsKey( JdbcSettings.AUTOCOMMIT ) ) { CONNECTION_INFO_LOGGER.ignoredSetting( JdbcSettings.AUTOCOMMIT, DataSourceConnectionProvider.class ); @@ -138,6 +149,14 @@ public Connection getConnection() throws SQLException { return useCredentials ? dataSource.getConnection( user, pass ) : dataSource.getConnection(); } + @Override + public Connection getConnection(String user, String password) throws SQLException { + if ( !available ) { + throw new HibernateException( "Provider is closed" ); + } + return dataSource.getConnection( user, password ); + } + @Override public void closeConnection(Connection connection) throws SQLException { connection.close(); 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 5595eb0b0ebf..48b26ca3d325 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 @@ -8,5 +8,5 @@ * @deprecated Use {@link DataSourceConnectionProvider} */ @Deprecated(since = "7.1", forRemoval = true) -public class DatasourceConnectionProviderImpl extends DriverManagerConnectionProvider { +public class DatasourceConnectionProviderImpl extends DataSourceConnectionProvider { } diff --git a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/internal/DriverManagerConnectionProvider.java b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/internal/DriverManagerConnectionProvider.java index 3013dd5054d0..84867629bded 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/internal/DriverManagerConnectionProvider.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/internal/DriverManagerConnectionProvider.java @@ -9,7 +9,6 @@ import java.sql.DriverManager; import java.sql.SQLException; import java.util.Map; -import java.util.Properties; import org.hibernate.boot.registry.classloading.spi.ClassLoaderService; import org.hibernate.dialect.Database; @@ -30,6 +29,7 @@ import static org.hibernate.cfg.JdbcSettings.AUTOCOMMIT; import static org.hibernate.cfg.JdbcSettings.DRIVER; import static org.hibernate.cfg.JdbcSettings.JAKARTA_JDBC_URL; +import static org.hibernate.cfg.JdbcSettings.LOGIN_TIMEOUT; import static org.hibernate.cfg.JdbcSettings.POOL_SIZE; import static org.hibernate.cfg.JdbcSettings.URL; import static org.hibernate.engine.jdbc.connections.internal.ConnectionProviderInitiator.extractIsolation; @@ -45,6 +45,7 @@ import static org.hibernate.internal.log.ConnectionInfoLogger.CONNECTION_INFO_LOGGER; import static org.hibernate.internal.util.config.ConfigurationHelper.getBoolean; import static org.hibernate.internal.util.config.ConfigurationHelper.getInt; +import static org.hibernate.internal.util.config.ConfigurationHelper.getInteger; import static org.hibernate.internal.util.config.ConfigurationHelper.getLong; import static org.hibernate.internal.util.config.ConfigurationHelper.getString; @@ -83,7 +84,7 @@ public void injectServices(ServiceRegistryImplementor serviceRegistry) { @Override public void configure(Map configurationValues) { CONNECTION_INFO_LOGGER.usingHibernateBuiltInConnectionPool(); - final PooledConnections pool = buildPool( configurationValues, serviceRegistry ); + final var pool = buildPool( configurationValues, serviceRegistry ); final long validationInterval = getLong( VALIDATION_INTERVAL, configurationValues, 30 ); state = new PoolState( pool, validationInterval ); } @@ -92,17 +93,22 @@ private PooledConnections buildPool(Map configuration, ServiceReg // connection settings final String url = jdbcUrl( configuration ); final String driverClassName = getString( DRIVER, configuration ); - final Properties connectionProps = getConnectionProperties( configuration ); + final var connectionProps = getConnectionProperties( configuration ); final boolean autoCommit = getBoolean( AUTOCOMMIT, configuration ); // default autocommit to false final Integer isolation = extractIsolation( configuration ); final String initSql = getString( INIT_SQL, configuration ); + final Integer loginTimeout = getInteger( LOGIN_TIMEOUT, configuration ); // pool settings final int minSize = getInt( MIN_SIZE, configuration, 1 ); final int maxSize = getInt( POOL_SIZE, configuration, 20 ); final int initialSize = getInt( INITIAL_SIZE, configuration, minSize ); - final Driver driver = loadDriver( driverClassName, serviceRegistry, url ); + if ( loginTimeout!= null ) { + DriverManager.setLoginTimeout( loginTimeout ); + } + + final var driver = loadDriver( driverClassName, serviceRegistry, url ); if ( driver == null ) { //we're hoping that the driver is already loaded logAvailableDrivers(); @@ -322,10 +328,9 @@ public boolean isUnwrappableAs(Class unwrapType) { } @Override - @SuppressWarnings( {"unchecked"}) public T unwrap(Class unwrapType) { if ( unwrapType.isAssignableFrom( DriverManagerConnectionProvider.class ) ) { - return (T) this; + return unwrapType.cast( this ); } else { throw new UnknownUnwrapTypeException( unwrapType ); diff --git a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/internal/UserSuppliedConnectionProviderImpl.java b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/internal/UserSuppliedConnectionProviderImpl.java index f85a9144faac..35c79503eba4 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/internal/UserSuppliedConnectionProviderImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/internal/UserSuppliedConnectionProviderImpl.java @@ -25,10 +25,9 @@ public boolean isUnwrappableAs(Class unwrapType) { } @Override - @SuppressWarnings( {"unchecked"}) public T unwrap(Class unwrapType) { if ( unwrapType.isAssignableFrom( UserSuppliedConnectionProviderImpl.class ) ) { - return (T) this; + return unwrapType.cast( this ); } else { throw new UnknownUnwrapTypeException( unwrapType ); diff --git a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/spi/AbstractDataSourceBasedMultiTenantConnectionProviderImpl.java b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/spi/AbstractDataSourceBasedMultiTenantConnectionProviderImpl.java index e14bafb1729b..0649321fd924 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/spi/AbstractDataSourceBasedMultiTenantConnectionProviderImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/spi/AbstractDataSourceBasedMultiTenantConnectionProviderImpl.java @@ -52,13 +52,12 @@ public boolean isUnwrappableAs(Class unwrapType) { } @Override - @SuppressWarnings("unchecked") - public T unwrap(Class unwrapType) { + public X unwrap(Class unwrapType) { if ( unwrapType.isInstance( this ) ) { - return (T) this; + return unwrapType.cast( this ); } else if ( unwrapType.isAssignableFrom( DataSource.class ) ) { - return (T) selectAnyDataSource(); + return unwrapType.cast( selectAnyDataSource() ); } else { throw new UnknownUnwrapTypeException( unwrapType ); diff --git a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/spi/AbstractMultiTenantConnectionProvider.java b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/spi/AbstractMultiTenantConnectionProvider.java index 6117ea3e59d0..55e98ec0b8d1 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/spi/AbstractMultiTenantConnectionProvider.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/spi/AbstractMultiTenantConnectionProvider.java @@ -54,13 +54,12 @@ public boolean isUnwrappableAs(Class unwrapType) { } @Override - @SuppressWarnings("unchecked") - public T unwrap(Class unwrapType) { + public X unwrap(Class unwrapType) { if ( unwrapType.isInstance( this ) ) { - return (T) this; + return unwrapType.cast( this ); } else if ( unwrapType.isAssignableFrom( ConnectionProvider.class ) ) { - return (T) getAnyConnectionProvider(); + return unwrapType.cast( getAnyConnectionProvider() ); } else { throw new UnknownUnwrapTypeException( unwrapType ); diff --git a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/spi/ConnectionProvider.java b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/spi/ConnectionProvider.java index 14e7206285e3..169be3759411 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/spi/ConnectionProvider.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/spi/ConnectionProvider.java @@ -45,6 +45,27 @@ public interface ConnectionProvider extends Service, Wrapped { */ Connection getConnection() throws SQLException; + /** + * Obtains a connection for Hibernate use according to the underlying strategy of this provider, + * using the given credentials. + * + * @param user The database user + * @param password The database password + * @return The obtained JDBC connection + * + * @throws SQLException Indicates a problem opening a connection + * @throws org.hibernate.HibernateException Indicates a problem obtaining a connection. + * + * @since 7.3 + */ + @Incubating + default Connection getConnection(String user, String password) throws SQLException { + throw new UnsupportedOperationException( + "ConnectionProvider does not support contextual credentials: " + + getClass().getTypeName() + + " (use a different ConnectionProvider for credentials-based multitenancy)" ); + } + /** * Obtains a connection to a read-only replica for use according to the underlying * strategy of this provider. diff --git a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/env/internal/LobCreationLogging.java b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/env/internal/LobCreationLogging.java index ea0d146ea4f8..3e480762c326 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/env/internal/LobCreationLogging.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/env/internal/LobCreationLogging.java @@ -17,6 +17,7 @@ import org.jboss.logging.annotations.ValidIdRange; import java.lang.invoke.MethodHandles; +import java.util.Locale; import static org.jboss.logging.Logger.Level.DEBUG; @@ -34,7 +35,7 @@ public interface LobCreationLogging extends BasicLogger { String NAME = JdbcLogging.NAME + ".lob"; Logger LOB_LOGGER = Logger.getLogger( NAME ); - LobCreationLogging LOB_MESSAGE_LOGGER = Logger.getMessageLogger( MethodHandles.lookup(), LobCreationLogging.class, NAME ); + LobCreationLogging LOB_MESSAGE_LOGGER = Logger.getMessageLogger( MethodHandles.lookup(), LobCreationLogging.class, NAME, Locale.ROOT ); @LogMessage(level = DEBUG) @Message(value = "Disabling contextual LOB creation as %s is true", id = 10010001) @@ -53,10 +54,14 @@ public interface LobCreationLogging extends BasicLogger { void nonContextualLobCreationDialect(); @LogMessage(level = DEBUG) - @Message(value = "Disabling contextual LOB creation as createClob() method threw error : %s", id = 10010005) + @Message(value = "Disabling contextual LOB creation as createClob() method threw error: %s", id = 10010005) void contextualClobCreationFailed(Throwable t); @LogMessage(level = DEBUG) - @Message(value = "Disabling contextual NCLOB creation as createNClob() method threw error : %s", id = 10010006) + @Message(value = "Disabling contextual NCLOB creation as createNClob() method threw error: %s", id = 10010006) void contextualNClobCreationFailed(Throwable t); + + @LogMessage(level = DEBUG) + @Message(value = "Falling back to non-contextual LOB creation", id = 10010007) + void fallingBackToNonContextual(); } diff --git a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/env/internal/LobCreatorBuilderImpl.java b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/env/internal/LobCreatorBuilderImpl.java index f7e847ee9306..941f819cdd4f 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/env/internal/LobCreatorBuilderImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/env/internal/LobCreatorBuilderImpl.java @@ -15,7 +15,6 @@ import static org.hibernate.engine.jdbc.env.internal.LobCreationHelper.NONE; import static org.hibernate.engine.jdbc.env.internal.LobCreationHelper.getSupportedContextualLobTypes; -import static org.hibernate.engine.jdbc.env.internal.LobCreationLogging.LOB_LOGGER; import static org.hibernate.engine.jdbc.env.internal.LobCreationLogging.LOB_MESSAGE_LOGGER; /** @@ -79,7 +78,7 @@ else if ( supportedContextualLobTypes.contains( LobTypes.BLOB ) : new StandardLobCreator( lobCreationContext, useConnectionToCreateLob ); } else { - LOB_LOGGER.debug( "Unexpected condition resolving type of LobCreator to use. Falling back to NonContextualLobCreator" ); + LOB_MESSAGE_LOGGER.fallingBackToNonContextual(); return NonContextualLobCreator.INSTANCE; } } diff --git a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/env/internal/NormalizingIdentifierHelperImpl.java b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/env/internal/NormalizingIdentifierHelperImpl.java index f4c683fbfeb0..8ec3dcae03c0 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/env/internal/NormalizingIdentifierHelperImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/env/internal/NormalizingIdentifierHelperImpl.java @@ -89,6 +89,11 @@ public Identifier toIdentifier(String text, boolean quoted) { return normalizeQuoting( Identifier.toIdentifier( text, quoted ) ); } + @Override + public Identifier toIdentifier(String text, boolean quoted, boolean isExplicit) { + return normalizeQuoting( Identifier.toIdentifier( text, quoted, true, isExplicit ) ); + } + @Override public Identifier applyGlobalQuoting(String text) { return Identifier.toIdentifier( text, globallyQuoteIdentifiers && !globallyQuoteIdentifiersSkipColumnDefinitions, false ); diff --git a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/env/spi/IdentifierHelper.java b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/env/spi/IdentifierHelper.java index 23cbb6c231dc..d5d91969e22f 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/env/spi/IdentifierHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/env/spi/IdentifierHelper.java @@ -51,6 +51,21 @@ public interface IdentifierHelper { */ Identifier toIdentifier(String text, boolean quoted); + /** + * Generate an Identifier instance from its simple name as obtained from mapping + * information. Additionally, this form takes a boolean indicating whether to + * explicitly quote the Identifier. + *

    + * Note that Identifiers returned from here may be implicitly quoted based on + * 'globally quoted identifiers' or based on reserved words. + * + * @param text The text form of a name as obtained from mapping information. + * @param quoted Is the identifier to be quoted explicitly. + * @param isExplicit Whether the name is explicitly set + * @return The identifier form of the name. + */ + Identifier toIdentifier(String text, boolean quoted, boolean isExplicit); + /** * Intended only for use in handling quoting requirements for {@code column-definition} * as defined by {@link jakarta.persistence.Column#columnDefinition()}, diff --git a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/internal/BasicFormatterImpl.java b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/internal/BasicFormatterImpl.java index 735e7e9fbb68..fab9ae82b895 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/internal/BasicFormatterImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/internal/BasicFormatterImpl.java @@ -162,6 +162,7 @@ public String perform() { case "into": case "union": case "intersect": + case "except": case "offset": case "limit": case "fetch": diff --git a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/internal/JdbcCoordinatorImpl.java b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/internal/JdbcCoordinatorImpl.java index 7d70641bfa30..94aa6c24a945 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/internal/JdbcCoordinatorImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/internal/JdbcCoordinatorImpl.java @@ -293,10 +293,10 @@ public void afterStatementExecution() { } if ( connectionReleaseMode == AFTER_STATEMENT ) { if ( ! releasesEnabled ) { - JDBC_LOGGER.trace( "Skipping aggressive release due to manual disabling" ); + JDBC_LOGGER.skippingAggressiveRelease( "manually disabled" ); } else if ( hasRegisteredResources() ) { - JDBC_LOGGER.trace( "Skipping aggressive release due to registered resources" ); + JDBC_LOGGER.skippingAggressiveRelease( "registered resources" ); } else { getLogicalConnection().afterStatement(); @@ -304,6 +304,21 @@ else if ( hasRegisteredResources() ) { } } + /** + * Notification that a {@link SQLException} has occurred + * while executing a JDBC {@linkplain java.sql.Statement} + * or {@link java.sql.PreparedStatement}. This gives us a + * chance to mark the current transaction as rollback-only + * on those databases where exceptions always cause the + * transaction to be marked for rollback (PostgreSQL). + */ + @Override + public void afterFailedStatementExecution(SQLException sqlException) { + if ( jdbcServices.getDialect().causesRollback( sqlException ) ) { + getLogicalConnection().markRollbackOnly(); + } + } + @Override public void afterTransaction() { transactionTimeOutInstant = -1; @@ -332,6 +347,7 @@ public T coordinateWork(WorkExecutorVisitable work) { return result; } catch ( SQLException e ) { + afterFailedStatementExecution( e ); throw sqlExceptionHelper().convert( e, "Error executing work" ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/internal/ResultSetReturnImpl.java b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/internal/ResultSetReturnImpl.java index e37887a7805c..3589efc39798 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/internal/ResultSetReturnImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/internal/ResultSetReturnImpl.java @@ -55,12 +55,13 @@ public ResultSet extract(PreparedStatement statement, String sql) { finally { eventMonitor.completeJdbcPreparedStatementExecutionEvent( executionEvent, sql ); jdbcExecuteStatementEnd(); - endSlowQueryLogging(sql, executeStartNanos); + endSlowQueryLogging( sql, executeStartNanos ); } postExtract( resultSet, statement ); return resultSet; } catch (SQLException e) { + jdbcCoordinator.afterFailedStatementExecution( e ); throw sqlExceptionHelper.convert( e, "could not extract ResultSet", sql ); } } @@ -106,6 +107,7 @@ public ResultSet extract(Statement statement, String sql) { return resultSet; } catch (SQLException e) { + jdbcCoordinator.afterFailedStatementExecution( e ); throw sqlExceptionHelper.convert( e, "could not extract ResultSet", sql ); } } @@ -136,6 +138,7 @@ public ResultSet execute(PreparedStatement statement, String sql) { return resultSet; } catch (SQLException e) { + jdbcCoordinator.afterFailedStatementExecution( e ); throw sqlExceptionHelper.convert( e, "could not execute statement", sql ); } } @@ -166,6 +169,7 @@ public ResultSet execute(Statement statement, String sql) { return resultSet; } catch (SQLException e) { + jdbcCoordinator.afterFailedStatementExecution( e ); throw sqlExceptionHelper.convert( e, "could not execute statement", sql ); } } @@ -181,6 +185,7 @@ public int executeUpdate(PreparedStatement statement, String sql) { return statement.executeUpdate(); } catch (SQLException e) { + jdbcCoordinator.afterFailedStatementExecution( e ); throw sqlExceptionHelper.convert( e, "could not execute statement", sql ); } finally { @@ -201,6 +206,7 @@ public int executeUpdate(Statement statement, String sql) { return statement.executeUpdate( sql ); } catch (SQLException e) { + jdbcCoordinator.afterFailedStatementExecution( e ); throw sqlExceptionHelper.convert( e, "could not execute statement", sql ); } finally { diff --git a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/mutation/internal/AbstractMutationExecutor.java b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/mutation/internal/AbstractMutationExecutor.java index b73fc23a67c8..7ed849d6e4bc 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/mutation/internal/AbstractMutationExecutor.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/mutation/internal/AbstractMutationExecutor.java @@ -17,7 +17,6 @@ import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.generator.values.GeneratedValues; import org.hibernate.persister.entity.mutation.EntityTableMapping; -import org.hibernate.sql.model.TableMapping; import org.hibernate.sql.model.ValuesAnalysis; import static org.hibernate.engine.jdbc.mutation.internal.ModelMutationHelper.checkResults; @@ -63,7 +62,7 @@ public final GeneratedValues execute( OperationResultChecker resultChecker, SharedSessionContractImplementor session, Batch.StaleStateMapper staleStateMapper) { - final GeneratedValues generatedValues = performNonBatchedOperations( + final var generatedValues = performNonBatchedOperations( modelReference, valuesAnalysis, inclusionChecker, @@ -112,7 +111,7 @@ protected void performNonBatchedMutation( return; } - final TableMapping tableDetails = statementDetails.getMutatingTableDetails(); + final var tableDetails = statementDetails.getMutatingTableDetails(); if ( inclusionChecker != null && !inclusionChecker.include( tableDetails ) ) { if ( MODEL_MUTATION_LOGGER.isTraceEnabled() ) { MODEL_MUTATION_LOGGER.skippingSecondaryInsert( tableDetails.getTableName() ); @@ -140,9 +139,10 @@ protected void performNonBatchedMutation( try { valueBindings.beforeStatement( statementDetails ); - final int affectedRowCount = session.getJdbcCoordinator() - .getResultSetReturn() - .executeUpdate( statementDetails.getStatement(), statementDetails.getSqlString() ); + final int affectedRowCount = + session.getJdbcCoordinator() + .getResultSetReturn() + .executeUpdate( statementDetails.getStatement(), statementDetails.getSqlString() ); if ( affectedRowCount == 0 && tableDetails.isOptional() ) { // the optional table did not have a row diff --git a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/mutation/internal/JdbcValueBindingsImpl.java b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/mutation/internal/JdbcValueBindingsImpl.java index 5aa57b50cae4..eb52c677a0c6 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/mutation/internal/JdbcValueBindingsImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/mutation/internal/JdbcValueBindingsImpl.java @@ -53,29 +53,32 @@ public void bindValue( String tableName, String columnName, ParameterUsage usage) { - final JdbcValueDescriptor jdbcValueDescriptor = jdbcValueDescriptorAccess.resolveValueDescriptor( tableName, columnName, usage ); + final var jdbcValueDescriptor = + jdbcValueDescriptorAccess.resolveValueDescriptor( tableName, columnName, usage ); if ( jdbcValueDescriptor == null ) { throw new UnknownParameterException( mutationType, mutationTarget, tableName, columnName, usage ); } - - resolveBindingGroup( jdbcValueDescriptorAccess.resolvePhysicalTableName( tableName ) ).bindValue( columnName, value, jdbcValueDescriptor ); + resolveBindingGroup( jdbcValueDescriptorAccess.resolvePhysicalTableName( tableName ) ) + .bindValue( columnName, value, jdbcValueDescriptor ); } private BindingGroup resolveBindingGroup(String tableName) { - final BindingGroup existing = bindingGroupMap.get( tableName ); + final var existing = bindingGroupMap.get( tableName ); if ( existing != null ) { assert tableName.equals( existing.getTableName() ); return existing; } - - final BindingGroup created = new BindingGroup( tableName ); - bindingGroupMap.put( tableName, created ); - return created; + else { + final var created = new BindingGroup( tableName ); + bindingGroupMap.put( tableName, created ); + return created; + } } @Override public void beforeStatement(PreparedStatementDetails statementDetails) { - final BindingGroup bindingGroup = bindingGroupMap.get( statementDetails.getMutatingTableDetails().getTableName() ); + final var bindingGroup = + bindingGroupMap.get( statementDetails.getMutatingTableDetails().getTableName() ); if ( bindingGroup == null ) { statementDetails.resolveStatement(); } @@ -106,12 +109,10 @@ public void beforeStatement(PreparedStatementDetails statementDetails) { @Override public void afterStatement(TableMapping mutatingTable) { - final BindingGroup bindingGroup = bindingGroupMap.remove( mutatingTable.getTableName() ); - if ( bindingGroup == null ) { - return; + final var bindingGroup = bindingGroupMap.remove( mutatingTable.getTableName() ); + if ( bindingGroup != null ) { + bindingGroup.clear(); } - - bindingGroup.clear(); } /** diff --git a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/mutation/internal/ModelMutationHelper.java b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/mutation/internal/ModelMutationHelper.java index 80bbad199ea1..7f60fc9c37a7 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/mutation/internal/ModelMutationHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/mutation/internal/ModelMutationHelper.java @@ -15,8 +15,6 @@ import org.hibernate.engine.jdbc.mutation.OperationResultChecker; 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.MutationStatementPreparer; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.generator.values.GeneratedValuesMutationDelegate; @@ -24,7 +22,6 @@ import org.hibernate.sql.model.MutationTarget; import org.hibernate.sql.model.MutationType; import org.hibernate.sql.model.PreparableMutationOperation; -import org.hibernate.stat.spi.StatisticsImplementor; import static org.hibernate.engine.jdbc.mutation.internal.PreparedStatementGroupNone.GROUP_OF_NONE; @@ -63,14 +60,16 @@ public static boolean identifiedResultsCheck( batchPosition, statementDetails.getSqlString() ); + return true; } catch (StaleStateException e) { if ( !statementDetails.getMutatingTableDetails().isOptional() && affectedRowCount == 0 ) { - final StatisticsImplementor statistics = sessionFactory.getStatistics(); + final String fullPath = mutationTarget.getNavigableRole().getFullPath(); + final var statistics = sessionFactory.getStatistics(); if ( statistics.isStatisticsEnabled() ) { - statistics.optimisticFailure( mutationTarget.getNavigableRole().getFullPath() ); + statistics.optimisticFailure( fullPath ); } - throw new StaleObjectStateException( mutationTarget.getNavigableRole().getFullPath(), id, e ); + throw new StaleObjectStateException( fullPath, id, e ); } return false; } @@ -88,8 +87,6 @@ public static boolean identifiedResultsCheck( catch (Throwable t) { return false; } - - return true; } public static PreparedStatementGroup toPreparedStatementGroup( @@ -126,7 +123,7 @@ public static PreparedStatement delegateStatementPreparation( PreparableMutationOperation jdbcMutation, GeneratedValuesMutationDelegate delegate, SharedSessionContractImplementor session) { - final PreparedStatement statement = delegate.prepareStatement( jdbcMutation.getSqlString(), session ); + final var statement = delegate.prepareStatement( jdbcMutation.getSqlString(), session ); session.getJdbcCoordinator().getLogicalConnection().getResourceRegistry().register( null, statement ); return statement; } @@ -134,9 +131,9 @@ public static PreparedStatement delegateStatementPreparation( public static PreparedStatement standardStatementPreparation( PreparableMutationOperation jdbcMutation, SharedSessionContractImplementor session) { - final JdbcCoordinator jdbcCoordinator = session.getJdbcCoordinator(); - final MutationStatementPreparer statementPreparer = jdbcCoordinator.getMutationStatementPreparer(); - final PreparedStatement statement = statementPreparer.prepareStatement( jdbcMutation.getSqlString(), jdbcMutation.isCallable() ); + final var jdbcCoordinator = session.getJdbcCoordinator(); + final var statementPreparer = jdbcCoordinator.getMutationStatementPreparer(); + final var statement = statementPreparer.prepareStatement( jdbcMutation.getSqlString(), jdbcMutation.isCallable() ); session.getJdbcCoordinator().getLogicalConnection().getResourceRegistry().register( null, statement ); return statement; } 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 e4616aaffd2b..fa5c9785eb8b 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 @@ -10,7 +10,6 @@ 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; @@ -67,7 +66,7 @@ public TableMapping getMutatingTableDetails() { @Override public void releaseStatement(SharedSessionContractImplementor session) { if ( statement != null ) { - final JdbcCoordinator jdbcCoordinator = session.getJdbcCoordinator(); + final var jdbcCoordinator = session.getJdbcCoordinator(); jdbcCoordinator.getLogicalConnection().getResourceRegistry().release( statement ); statement = null; toRelease = false; diff --git a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/spi/JdbcCoordinator.java b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/spi/JdbcCoordinator.java index 98fae920aa91..348ea19fd811 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/spi/JdbcCoordinator.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/spi/JdbcCoordinator.java @@ -9,6 +9,7 @@ import java.io.Serializable; import java.sql.Connection; import java.sql.ResultSet; +import java.sql.SQLException; import java.sql.Statement; import java.util.function.Supplier; @@ -114,12 +115,19 @@ Batch getBatch( void afterTransaction(); /** - * Used to signify that a statement has completed execution which may - * indicate that this logical connection need to perform an + * Notification that a statement has completed execution, which + * might indicate that the logical connection should perform an * aggressive release of its physical connection. */ void afterStatementExecution(); + /** + * Notification that execution of a statement has failed, which + * might mean that the database has marked the transaction for + * rollback. + */ + void afterFailedStatementExecution(SQLException sqlException); + /** * Perform the requested work handling exceptions, coordinating and handling return processing. * diff --git a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/spi/SQLExceptionLogging.java b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/spi/SQLExceptionLogging.java index 717a4bfaa2a1..c17f1e62b6e9 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/spi/SQLExceptionLogging.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/spi/SQLExceptionLogging.java @@ -14,6 +14,7 @@ import java.lang.invoke.MethodHandles; import java.sql.SQLException; +import java.util.Locale; import static org.jboss.logging.Logger.Level.DEBUG; import static org.jboss.logging.Logger.Level.WARN; @@ -30,8 +31,8 @@ public interface SQLExceptionLogging extends BasicLogger { String ERROR_NAME = SubSystemLogging.BASE + ".jdbc.error"; String WARN_NAME = SubSystemLogging.BASE + ".jdbc.warn"; - SQLExceptionLogging ERROR_LOG = Logger.getMessageLogger( MethodHandles.lookup(), SQLExceptionLogging.class, ERROR_NAME ); - SQLExceptionLogging WARNING_LOG = Logger.getMessageLogger( MethodHandles.lookup(), SQLExceptionLogging.class, WARN_NAME ); + SQLExceptionLogging ERROR_LOG = Logger.getMessageLogger( MethodHandles.lookup(), SQLExceptionLogging.class, ERROR_NAME, Locale.ROOT ); + SQLExceptionLogging WARNING_LOG = Logger.getMessageLogger( MethodHandles.lookup(), SQLExceptionLogging.class, WARN_NAME, Locale.ROOT ); @LogMessage(level = WARN) @Message(value = "ErrorCode: %s, SQLState: %s", id = 247) diff --git a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/spi/SchemaNameResolver.java b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/spi/SchemaNameResolver.java index e83cfe19c44d..b64e6e129bf8 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/spi/SchemaNameResolver.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/spi/SchemaNameResolver.java @@ -10,14 +10,13 @@ /** * Contract for resolving the schema of a {@link Connection}. * - * @apiNote No used. + * @apiNote Not used. * @deprecated Use {@linkplain org.hibernate.engine.jdbc.env.spi.SchemaNameResolver} instead. * * @author Steve Ebersole */ @Deprecated(since = "7.0", forRemoval = true) public interface SchemaNameResolver { - /** /** * Given a JDBC {@link Connection}, resolve the name of the schema (if one) to which it connects. * diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/AbstractDelegatingSessionBuilder.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/AbstractDelegatingSessionBuilder.java index ed18b0f97fab..e0cdab723477 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/spi/AbstractDelegatingSessionBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/AbstractDelegatingSessionBuilder.java @@ -5,6 +5,7 @@ package org.hibernate.engine.spi; import java.sql.Connection; +import java.time.Instant; import java.util.TimeZone; import java.util.function.UnaryOperator; @@ -184,4 +185,16 @@ public SessionBuilderImplementor subselectFetchEnabled(boolean subselectFetchEna delegate.subselectFetchEnabled( subselectFetchEnabled ); return this; } + + @Override + public SessionBuilderImplementor asOf(Instant instant) { + delegate.asOf( instant ); + return this; + } + + @Override + public SessionBuilderImplementor atTransaction(Object transactionId) { + delegate.atTransaction( transactionId ); + return this; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/AbstractDelegatingSharedSessionBuilder.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/AbstractDelegatingSharedSessionBuilder.java index 08e8e7cb53ed..7d62cc2a0fa8 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/spi/AbstractDelegatingSharedSessionBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/AbstractDelegatingSharedSessionBuilder.java @@ -5,6 +5,7 @@ package org.hibernate.engine.spi; import java.sql.Connection; +import java.time.Instant; import java.util.TimeZone; import java.util.function.UnaryOperator; @@ -46,6 +47,11 @@ public Session openSession() { return delegate.openSession(); } + @Override + public Session open() { + return delegate.open(); + } + @Override public SharedSessionBuilder interceptor() { delegate.interceptor(); @@ -231,4 +237,16 @@ public SharedSessionBuilder subselectFetchEnabled(boolean subselectFetchEnabled) delegate.subselectFetchEnabled( subselectFetchEnabled ); return this; } + + @Override + public SharedSessionBuilder asOf(Instant instant) { + delegate.asOf( instant ); + return this; + } + + @Override + public SharedSessionBuilder atTransaction(Object transactionId) { + delegate.atTransaction( transactionId ); + return this; + } } 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 ed1689a7d5ef..2149933e48f4 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 @@ -100,7 +100,7 @@ public class ActionQueue implements TransactionCompletionCallbacks { private transient boolean isTransactionCoordinatorShared; - private TransactionCompletionCallbacksImplementor transactionCompletionCallbacks;; + private TransactionCompletionCallbacksImplementor transactionCompletionCallbacks; // Extract this as a constant to perform efficient iterations: // method values() otherwise allocates a new array on each invocation. @@ -959,7 +959,8 @@ public static ActionQueue deserialize(ObjectInputStream ois, EventSource session * Scheduling serially means, that there is an order which doesn't violate the FK constraint dependencies. * The inserts of insert groups which can't be scheduled, are going to be inserted in the original order. */ - private static class InsertActionSorter implements ExecutableList.Sorter { + // Used by Hibernate Reactive + public static class InsertActionSorter implements ExecutableList.Sorter { /** * Singleton access */ diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/CollectionEntry.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/CollectionEntry.java index f6981b79b4f3..38b7c78c1689 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/spi/CollectionEntry.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/CollectionEntry.java @@ -17,6 +17,7 @@ import org.checkerframework.checker.nullness.qual.Nullable; +import static java.util.Collections.emptyList; import static org.hibernate.internal.CoreMessageLogger.CORE_LOGGER; import static org.hibernate.internal.util.NullnessUtil.castNonNull; import static org.hibernate.pretty.MessageHelper.collectionInfoString; @@ -35,6 +36,7 @@ public final class CollectionEntry implements Serializable { private @Nullable Serializable snapshot; // allow the CollectionSnapshot to be serialized private @Nullable String role; + private boolean readOnly; // "loaded" means the reference that is consistent // with the current database state @@ -65,11 +67,9 @@ public CollectionEntry(CollectionPersister persister, PersistentCollection co // new collections that get found + wrapped // during flush shouldn't be ignored ignore = false; - // a newly wrapped collection is NOT dirty // (or we get unnecessary version updates) collection.clearDirty(); - snapshot = persister.isMutable() ? collection.getSnapshot( persister ) : null; role = persister.getRole(); collection.setSnapshot( loadedKey, role, snapshot ); @@ -82,18 +82,15 @@ public CollectionEntry( final PersistentCollection collection, final CollectionPersister loadedPersister, final Object loadedKey, - final boolean ignore ) { + final boolean ignore, + final boolean readOnly) { this.ignore = ignore; - + this.readOnly = readOnly; //collection.clearDirty() - this.loadedKey = loadedKey; - this.loadedPersister = loadedPersister; this.role = loadedPersister == null ? null : loadedPersister.getRole(); - collection.setSnapshot( loadedKey, role, null ); - //postInitialize() will be called after initialization } @@ -104,12 +101,10 @@ public CollectionEntry(CollectionPersister loadedPersister, Object loadedKey) { // detached collection wrappers that get found + reattached // during flush shouldn't be ignored ignore = false; - //collection.clearDirty() - this.loadedKey = loadedKey; this.loadedPersister = loadedPersister; - this.role = ( loadedPersister == null ? null : loadedPersister.getRole() ); + this.role = loadedPersister == null ? null : loadedPersister.getRole(); } /** @@ -119,11 +114,11 @@ public CollectionEntry(PersistentCollection collection, SessionFactoryImpleme // detached collections that get found + reattached // during flush shouldn't be ignored ignore = false; - loadedKey = collection.getKey(); role = collection.getRole(); - loadedPersister = factory.getMappingMetamodel().getCollectionDescriptor( castNonNull( role ) ); - + loadedPersister = + factory.getMappingMetamodel() + .getCollectionDescriptor( castNonNull( role ) ); snapshot = collection.getStoredSnapshot(); } @@ -137,10 +132,12 @@ private CollectionEntry( @Nullable String role, Serializable snapshot, Object loadedKey, + boolean readOnly, @Nullable SessionFactoryImplementor factory) { this.role = role; this.snapshot = snapshot; this.loadedKey = loadedKey; + this.readOnly = readOnly; if ( role != null ) { afterDeserialize( factory ); } @@ -151,57 +148,66 @@ private CollectionEntry( * of the collection elements, if necessary */ private void dirty(PersistentCollection collection) { - - final var loadedPersister = getLoadedPersister(); - final boolean forceDirty = - collection.wasInitialized() - && !collection.isDirty() //optimization - && loadedPersister != null - && loadedPersister.isMutable() //optimization - && ( collection.isDirectlyAccessible() || loadedPersister.getElementType().isMutable() ) //optimization - && !collection.equalsSnapshot( loadedPersister ); - if ( forceDirty ) { + if ( forceDirty( collection ) ) { collection.dirty(); } + } + private boolean forceDirty(PersistentCollection collection) { + final var loadedPersister = this.loadedPersister; + return loadedPersister != null + && collection.wasInitialized() + && !collection.isDirty() //optimization + && isModifiable() //optimization + && isElementMutable( collection, loadedPersister ) //optimization + && !collection.equalsSnapshot( loadedPersister ); + } + + private static boolean isElementMutable( + PersistentCollection collection, + CollectionPersister loadedPersister) { + return collection.isDirectlyAccessible() + || loadedPersister.getElementType().isMutable(); + } + + private boolean isModifiable() { + return !readOnly + && loadedPersister != null + && loadedPersister.isMutable(); } public void preFlush(PersistentCollection collection) { - if ( loadedKey == null && collection.getKey() != null ) { + if ( loadedKey == null ) { loadedKey = collection.getKey(); } - final var loadedPersister = getLoadedPersister(); - final boolean nonMutableChange = - collection.isDirty() - && loadedPersister != null - && !loadedPersister.isMutable(); - if ( nonMutableChange ) { + final var loadedPersister = this.loadedPersister; + if ( collection.isDirty() + && loadedPersister != null + && !isModifiable() ) { throw new HibernateException( "Immutable collection was modified: " + - collectionInfoString( castNonNull( loadedPersister ).getRole(), getLoadedKey() ) ); + collectionInfoString( loadedPersister.getRole(), getLoadedKey() ) ); } dirty( collection ); - if ( CORE_LOGGER.isTraceEnabled() && collection.isDirty() && loadedPersister != null ) { + if ( CORE_LOGGER.isTraceEnabled() + && loadedPersister != null + && collection.isDirty() ) { CORE_LOGGER.collectionDirty( collectionInfoString( loadedPersister.getRole(), getLoadedKey() ) ); } - setReached( false ); - setProcessed( false ); - - setDoupdate( false ); - setDoremove( false ); - setDorecreate( false ); + reached = false; + processed = false; + doupdate = false; + doremove = false; + dorecreate = false; } public void postInitialize(PersistentCollection collection, SharedSessionContractImplementor session) { - final var loadedPersister = getLoadedPersister(); - snapshot = - loadedPersister != null && loadedPersister.isMutable() - ? collection.getSnapshot( loadedPersister ) - : null; + final var loadedPersister = this.loadedPersister; + snapshot = loadedPersister != null && isModifiable() ? collection.getSnapshot( loadedPersister ) : null; collection.setSnapshot( loadedKey, role, snapshot ); if ( loadedPersister != null && session.getLoadQueryInfluencers().effectivelyBatchLoadable( loadedPersister ) ) { @@ -214,13 +220,11 @@ public void postInitialize(PersistentCollection collection, SharedSessionCont * Called after a successful flush */ public void postFlush(PersistentCollection collection) { - if ( isIgnore() ) { - ignore = false; - } - else if ( !isProcessed() ) { + if ( !ignore && !processed ) { throw new HibernateException( "Collection '" + collection.getRole() + "' was not processed by flush" + " (this is likely due to unsafe use of the session, for example, current use in multiple threads, or updates during entity lifecycle callbacks)"); } + ignore = false; collection.setSnapshot( loadedKey, role, snapshot ); } @@ -228,18 +232,16 @@ else if ( !isProcessed() ) { * Called after execution of an action */ public void afterAction(PersistentCollection collection) { - loadedKey = getCurrentKey(); - setLoadedPersister( getCurrentPersister() ); - + loadedKey = currentKey; + loadedPersister = currentPersister; + role = currentPersister == null ? null : currentPersister.getRole(); if ( collection.wasInitialized() - && ( isDoremove() || isDorecreate() || isDoupdate() ) ) { + && ( doremove || dorecreate || doupdate ) ) { // update the snapshot - snapshot = - loadedPersister != null && loadedPersister.isMutable() - ? collection.getSnapshot( castNonNull( loadedPersister ) ) - : null; + snapshot = isModifiable() + ? collection.getSnapshot( castNonNull( loadedPersister ) ) + : null; } - collection.postAction(); } @@ -255,6 +257,10 @@ public void afterAction(PersistentCollection collection) { return snapshot; } + public boolean isReadOnly() { + return readOnly; + } + private boolean fromMerge; /** @@ -266,7 +272,6 @@ public void afterAction(PersistentCollection collection) { */ public void resetStoredSnapshot(PersistentCollection collection, Serializable storedSnapshot) { CORE_LOGGER.resetStoredSnapshot( storedSnapshot, this ); - if ( !fromMerge ) { snapshot = storedSnapshot; collection.setSnapshot( loadedKey, role, snapshot ); @@ -274,9 +279,23 @@ public void resetStoredSnapshot(PersistentCollection collection, Serializable } } - private void setLoadedPersister(@Nullable CollectionPersister persister) { - loadedPersister = persister; - setRole( persister == null ? null : persister.getRole() ); + public void setReadOnly(boolean readOnly, PersistentCollection collection) { + if ( this.readOnly != readOnly ) { + this.readOnly = readOnly; + if ( readOnly ) { + snapshot = null; + collection.setSnapshot( loadedKey, role, null ); + } + else { + final var loadedPersister = this.loadedPersister; + if ( collection.wasInitialized() + && loadedPersister != null + && loadedPersister.isMutable() ) { + snapshot = collection.getSnapshot( loadedPersister ); + collection.setSnapshot( loadedKey, role, snapshot ); + } + } + } } void afterDeserialize(@Nullable SessionFactoryImplementor factory) { @@ -286,7 +305,7 @@ void afterDeserialize(@Nullable SessionFactoryImplementor factory) { } public boolean wasDereferenced() { - return getLoadedKey() == null; + return loadedKey == null; } public boolean isReached() { @@ -370,13 +389,13 @@ public void setRole(@Nullable String role) { @Override public String toString() { - final StringBuilder result = + final var result = new StringBuilder( "CollectionEntry" ) .append( collectionInfoString( role, loadedKey ) ); - final CollectionPersister persister = currentPersister; - if ( persister != null ) { + final var currentPersister = this.currentPersister; + if ( currentPersister != null ) { result.append( "->" ) - .append( collectionInfoString( persister.getRole(), currentKey ) ); + .append( collectionInfoString( currentPersister.getRole(), currentKey ) ); } return result.toString(); } @@ -385,21 +404,24 @@ public String toString() { * Get the collection orphans (entities which were removed from the collection) */ public Collection getOrphans(String entityName, PersistentCollection collection) { - if ( snapshot == null ) { - throw new AssertionFailure( "no collection snapshot for orphan delete" ); + if ( readOnly ) { + return emptyList(); + } + else { + if ( snapshot == null ) { + throw new AssertionFailure( "no collection snapshot for orphan delete" ); + } + return collection.getOrphans( snapshot, entityName ); } - return collection.getOrphans( snapshot, entityName ); } public boolean isSnapshotEmpty(PersistentCollection collection) { //TODO: does this really need to be here? // does the collection already have // it's own up-to-date snapshot? - final var loadedPersister = getLoadedPersister(); - final Serializable snapshot = getSnapshot(); return collection.wasInitialized() - && ( loadedPersister == null || loadedPersister.isMutable() ) - && ( snapshot == null || collection.isSnapshotEmpty(snapshot) ); + && ( loadedPersister == null || isModifiable() ) + && ( snapshot == null || collection.isSnapshotEmpty( snapshot ) ); } @@ -414,6 +436,7 @@ public void serialize(ObjectOutputStream oos) throws IOException { oos.writeObject( role ); oos.writeObject( snapshot ); oos.writeObject( loadedKey ); + oos.writeBoolean( readOnly ); } /** @@ -431,6 +454,7 @@ public static CollectionEntry deserialize(ObjectInputStream ois, SessionImplemen (String) ois.readObject(), (Serializable) ois.readObject(), ois.readObject(), + ois.readBoolean(), session == null ? null : session.getFactory() ); } diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/EntityEntry.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/EntityEntry.java index fbd27332f059..5de223af4a40 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/spi/EntityEntry.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/EntityEntry.java @@ -132,7 +132,7 @@ public interface EntityEntry { boolean isReadOnly(); - void setReadOnly(boolean readOnly, Object entity); + boolean setReadOnly(boolean readOnly, Object entity); /** * Has a bit set for every attribute position that is potentially lazy. 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 d7d41fa8f5d9..2e7277842826 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 @@ -59,6 +59,7 @@ public class LoadQueryInfluencers implements Serializable { private final EffectiveEntityGraph effectiveEntityGraph; private Boolean readOnly; + private Object temporalIdentifier; public LoadQueryInfluencers(SessionFactoryImplementor sessionFactory) { this.sessionFactory = sessionFactory; @@ -72,6 +73,7 @@ public LoadQueryInfluencers(SessionFactoryImplementor sessionFactory, SessionCre batchSize = options.getDefaultBatchFetchSize(); subselectFetchEnabled = options.isSubselectFetchEnabled(); effectiveEntityGraph = new EffectiveEntityGraph(); + temporalIdentifier = options.getTemporalIdentifier(); for ( var filterDefinition : sessionFactory.getAutoEnabledFilters() ) { final var filter = new FilterImpl( filterDefinition ); if ( enabledFilters == null ) { @@ -96,6 +98,14 @@ public SessionFactoryImplementor getSessionFactory() { return sessionFactory; } + public Object getTemporalIdentifier() { + return temporalIdentifier; + } + + public void setTemporalIdentifier(Object temporalIdentifier) { + this.temporalIdentifier = temporalIdentifier; + } + // internal fetch profile support ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/Managed.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/Managed.java index 88923c564f23..d9d5730b56c9 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/spi/Managed.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/Managed.java @@ -5,16 +5,20 @@ package org.hibernate.engine.spi; /** - * Contract for classes (specifically, entities and components/embeddables) that are "managed". Developers can - * choose to either have their classes manually implement these interfaces or Hibernate can enhance their classes - * to implement these interfaces via built-time or run-time enhancement. + * Contract for classes (specifically, entities and components/embeddables) that are "managed". + * Developers can choose to either have their classes manually implement these interfaces, or + * Hibernate can enhance their classes to implement these interfaces via built-time or run-time + * enhancement. *

    - * The term managed here is used to describe both:

  • * * @@ -368,10 +367,10 @@ private static QualifiedName sequenceName( IdentifierHelper identifierHelper) { if ( isNotEmpty( explicitSequenceName ) ) { // we have an explicit name, use it - return explicitSequenceName.contains(".") + return explicitSequenceName.contains( "." ) ? QualifiedNameParser.INSTANCE.parse( explicitSequenceName ) : new QualifiedNameParser.NameParts( catalog, schema, - identifierHelper.toIdentifier( explicitSequenceName ) ); + identifierHelper.toIdentifier( explicitSequenceName, false, true ) ); } else { // otherwise, determine an implicit name to use diff --git a/hibernate-core/src/main/java/org/hibernate/id/enhanced/StandardNamingStrategy.java b/hibernate-core/src/main/java/org/hibernate/id/enhanced/StandardNamingStrategy.java index 75b71a18ea03..689423a07cce 100644 --- a/hibernate-core/src/main/java/org/hibernate/id/enhanced/StandardNamingStrategy.java +++ b/hibernate-core/src/main/java/org/hibernate/id/enhanced/StandardNamingStrategy.java @@ -74,36 +74,32 @@ public QualifiedName determineSequenceName( Identifier schemaName, Map configValues, ServiceRegistry serviceRegistry) { - final String rootTableName = getString( TABLE, configValues ); - final String implicitName = implicitSequenceName( rootTableName, configValues ); - return qualifiedSequenceName( catalogName, schemaName, serviceRegistry, implicitName ); - + return qualifiedSequenceName( catalogName, schemaName, serviceRegistry, + implicitSequenceName( getString( TABLE, configValues ), configValues ) ); } private static String implicitSequenceName(String rootTableName, Map configValues) { final String explicitSuffix = getString( CONFIG_SEQUENCE_PER_ENTITY_SUFFIX, configValues ); final String base = getString( IMPLICIT_NAME_BASE, configValues, rootTableName ); - - if ( isNotEmpty( explicitSuffix ) ) { + if ( isNotEmpty( explicitSuffix ) && isNotEmpty( base ) ) { // an "implicit name suffix" was specified - if ( isNotEmpty( base ) ) { - return isQuoted( base ) - ? "`" + unQuote( base ) + explicitSuffix + "`" - : base + explicitSuffix; - } - } - - final String annotationGeneratorName = getString( GENERATOR_NAME, configValues ); - if ( isNotEmpty( annotationGeneratorName ) ) { - return annotationGeneratorName; - } - else if ( isNotEmpty( base ) ) { return isQuoted( base ) - ? "`" + unQuote( base ) + DEF_SEQUENCE_SUFFIX + "`" - : base + DEF_SEQUENCE_SUFFIX; + ? "`" + unQuote( base ) + explicitSuffix + "`" + : base + explicitSuffix; } else { - throw new MappingException( "Unable to determine implicit sequence name; target table - " + rootTableName ); + final String annotationGeneratorName = getString( GENERATOR_NAME, configValues ); + if ( isNotEmpty( annotationGeneratorName ) ) { + return annotationGeneratorName; + } + else if ( isNotEmpty( base ) ) { + return isQuoted( base ) + ? "`" + unQuote( base ) + DEF_SEQUENCE_SUFFIX + "`" + : base + DEF_SEQUENCE_SUFFIX; + } + else { + throw new MappingException( "Unable to determine implicit sequence name for target table '" + rootTableName + "'" ); + } } } @@ -113,8 +109,8 @@ public QualifiedName determineTableName( Identifier schemaName, Map configValues, ServiceRegistry serviceRegistry) { - final String implicitName = implicitTableName( configValues ); - return qualifiedTableName( catalogName, schemaName, serviceRegistry, implicitName ); + return qualifiedTableName( catalogName, schemaName, serviceRegistry, + implicitTableName( configValues ) ); } private static String implicitTableName(Map configValues) { diff --git a/hibernate-core/src/main/java/org/hibernate/id/enhanced/TableGenerator.java b/hibernate-core/src/main/java/org/hibernate/id/enhanced/TableGenerator.java index e22f4f0ee6e4..eeadcec76016 100644 --- a/hibernate-core/src/main/java/org/hibernate/id/enhanced/TableGenerator.java +++ b/hibernate-core/src/main/java/org/hibernate/id/enhanced/TableGenerator.java @@ -72,7 +72,6 @@ * By default, we use a single row for all generators (the {@value #DEF_SEGMENT_VALUE} * segment). The configuration parameter {@value #CONFIG_PREFER_SEGMENT_PER_ENTITY} can * be used to change that to instead default to using a row for each entity name. - *

    *

    General configuration parameters
    * * diff --git a/hibernate-core/src/main/java/org/hibernate/id/enhanced/TableGeneratorLogger.java b/hibernate-core/src/main/java/org/hibernate/id/enhanced/TableGeneratorLogger.java index ec836d4599f9..096ad9edb6a6 100644 --- a/hibernate-core/src/main/java/org/hibernate/id/enhanced/TableGeneratorLogger.java +++ b/hibernate-core/src/main/java/org/hibernate/id/enhanced/TableGeneratorLogger.java @@ -18,6 +18,7 @@ import org.jboss.logging.annotations.ValidIdRange; import java.lang.invoke.MethodHandles; +import java.util.Locale; import static org.jboss.logging.Logger.Level.ERROR; import static org.jboss.logging.Logger.Level.INFO; @@ -41,7 +42,8 @@ public interface TableGeneratorLogger extends BasicLogger { TableGeneratorLogger TABLE_GENERATOR_LOGGER = Logger.getMessageLogger( MethodHandles.lookup(), TableGeneratorLogger.class, - NAME + NAME, + Locale.ROOT ); @LogMessage(level = ERROR) diff --git a/hibernate-core/src/main/java/org/hibernate/id/insert/AbstractReturningDelegate.java b/hibernate-core/src/main/java/org/hibernate/id/insert/AbstractReturningDelegate.java index 85bd2f068ba4..a68838352a6c 100644 --- a/hibernate-core/src/main/java/org/hibernate/id/insert/AbstractReturningDelegate.java +++ b/hibernate-core/src/main/java/org/hibernate/id/insert/AbstractReturningDelegate.java @@ -9,13 +9,13 @@ import org.hibernate.engine.jdbc.mutation.JdbcValueBindings; import org.hibernate.engine.jdbc.mutation.group.PreparedStatementDetails; -import org.hibernate.engine.jdbc.spi.JdbcCoordinator; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.generator.EventType; import org.hibernate.generator.values.AbstractGeneratedValuesMutationDelegate; import org.hibernate.generator.values.GeneratedValues; import org.hibernate.persister.entity.EntityPersister; -import org.hibernate.pretty.MessageHelper; + +import static org.hibernate.pretty.MessageHelper.infoString; /** * Abstract {@link org.hibernate.generator.values.GeneratedValuesMutationDelegate} implementation where @@ -42,14 +42,11 @@ public GeneratedValues performMutation( JdbcValueBindings valueBindings, Object entity, SharedSessionContractImplementor session) { - session.getJdbcServices().getSqlStatementLogger().logStatement( statementDetails.getSqlString() ); + final String sql = statementDetails.getSqlString(); + session.getJdbcServices().getSqlStatementLogger().logStatement( sql ); try { valueBindings.beforeStatement( statementDetails ); - return executeAndExtractReturning( - statementDetails.getSqlString(), - statementDetails.getStatement(), - session - ); + return executeAndExtractReturning( sql, statementDetails.getStatement(), session ); } finally { if ( statementDetails.getStatement() != null ) { @@ -63,35 +60,28 @@ public GeneratedValues performMutation( @Override public final GeneratedValues performInsertReturning(String sql, SharedSessionContractImplementor session, Binder binder) { session.getJdbcServices().getSqlStatementLogger().logStatement( sql ); - + // prepare and execute the insert + final var insert = prepareStatement( sql, session ); try { - // prepare and execute the insert - final var insert = prepareStatement( sql, session ); - try { - binder.bindValues( insert ); - return executeAndExtractReturning( sql, insert, session ); - } - finally { - releaseStatement( insert, session ); - } + binder.bindValues( insert ); + return executeAndExtractReturning( sql, insert, session ); } catch (SQLException sqle) { throw session.getJdbcServices().getSqlExceptionHelper().convert( sqle, - "could not insert: " + MessageHelper.infoString( persister ), + "Could not insert: " + infoString( persister ), sql ); } + finally { + final var jdbcCoordinator = session.getJdbcCoordinator(); + jdbcCoordinator.getLogicalConnection().getResourceRegistry().release( insert ); + jdbcCoordinator.afterStatementExecution(); + } } protected abstract GeneratedValues executeAndExtractReturning( String sql, PreparedStatement preparedStatement, SharedSessionContractImplementor session); - - protected void releaseStatement(PreparedStatement preparedStatement, SharedSessionContractImplementor session) { - final JdbcCoordinator jdbcCoordinator = session.getJdbcCoordinator(); - jdbcCoordinator.getLogicalConnection().getResourceRegistry().release( preparedStatement ); - jdbcCoordinator.afterStatementExecution(); - } } 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 a6dbb0f48b7f..c52733e3550a 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 @@ -70,14 +70,12 @@ public GeneratedValues performMutation( Object entity, SharedSessionContractImplementor session) { final var jdbcCoordinator = session.getJdbcCoordinator(); - final var jdbcServices = session.getJdbcServices(); - - jdbcServices.getSqlStatementLogger().logStatement( statementDetails.getSqlString() ); - + final String sql = statementDetails.getSqlString(); + session.getJdbcServices().getSqlStatementLogger().logStatement( sql ); try { jdbcValueBindings.beforeStatement( statementDetails ); jdbcCoordinator.getResultSetReturn() - .executeUpdate( statementDetails.resolveStatement(), statementDetails.getSqlString() ); + .executeUpdate( statementDetails.resolveStatement(), sql ); } finally { if ( statementDetails.getStatement() != null ) { @@ -85,91 +83,76 @@ public GeneratedValues performMutation( } jdbcValueBindings.afterStatement( statementDetails.getMutatingTableDetails() ); } - - // the insert is complete, select the generated id... - - final String idSelectSql = getSelectSQL(); - final var idSelect = jdbcCoordinator.getStatementPreparer().prepareStatement( idSelectSql ); - try { - bindParameters( entity, idSelect, session ); - - final ResultSet resultSet = jdbcCoordinator.getResultSetReturn().extract( idSelect, idSelectSql ); - try { - return extractReturningValues( resultSet, idSelect, session ); - } - catch (SQLException e) { - throw jdbcServices.getSqlExceptionHelper().convert( - e, - "Unable to execute post-insert id selection query", - idSelectSql - ); - } - finally { - jdbcCoordinator.getLogicalConnection().getResourceRegistry().release( idSelect ); - jdbcCoordinator.afterStatementExecution(); - } - } - catch (SQLException e) { - throw jdbcServices.getSqlExceptionHelper().convert( - e, - "Unable to bind parameters for post-insert id selection query", - idSelectSql - ); - } + return selectGeneratedId( session, entity ); } @Override public final GeneratedValues performInsertReturning(String sql, SharedSessionContractImplementor session, Binder binder) { final var jdbcCoordinator = session.getJdbcCoordinator(); - final var statementPreparer = jdbcCoordinator.getStatementPreparer(); + // prepare and execute the insert + final var insert = + jdbcCoordinator.getStatementPreparer() + .prepareStatement( sql, NO_GENERATED_KEYS ); try { - // prepare and execute the insert - final var insert = statementPreparer.prepareStatement( sql, NO_GENERATED_KEYS ); - try { - binder.bindValues( insert ); - jdbcCoordinator.getResultSetReturn().executeUpdate( insert, sql ); - } - finally { - jdbcCoordinator.getLogicalConnection().getResourceRegistry().release( insert ); - jdbcCoordinator.afterStatementExecution(); - } + binder.bindValues( insert ); + jdbcCoordinator.getResultSetReturn().executeUpdate( insert, sql ); } catch (SQLException sqle) { throw session.getJdbcServices().getSqlExceptionHelper().convert( sqle, - "could not insert: " + infoString( persister ), + "Could not insert: " + infoString( persister ), sql ); } + finally { + jdbcCoordinator.getLogicalConnection().getResourceRegistry().release( insert ); + jdbcCoordinator.afterStatementExecution(); + } + // the insert is complete, select the generated id + return selectGeneratedId( session, binder.getEntity() ); + } - final String selectSQL = getSelectSQL(); - + private GeneratedValues selectGeneratedId(SharedSessionContractImplementor session, Object entity) { + final String idSelectSql = getSelectSQL(); + var jdbcCoordinator = session.getJdbcCoordinator(); + final var idSelect = jdbcCoordinator.getStatementPreparer().prepareStatement( idSelectSql ); try { - //fetch the generated id in a separate query - final var idSelect = statementPreparer.prepareStatement( selectSQL ); + bindParameters( entity, session, idSelect, idSelectSql ); + final var resultSet = jdbcCoordinator.getResultSetReturn().extract( idSelect, idSelectSql ); try { - bindParameters( binder.getEntity(), idSelect, session ); - final ResultSet resultSet = jdbcCoordinator.getResultSetReturn().extract( idSelect, selectSQL ); - try { - return extractReturningValues( resultSet, idSelect, session ); - } - finally { - jdbcCoordinator.getLogicalConnection().getResourceRegistry().release( resultSet, idSelect ); - } + return extractReturningValues( resultSet, idSelect, session ); + } + catch (SQLException sqle) { + throw session.getJdbcServices().getSqlExceptionHelper().convert( + sqle, + "Could not retrieve generated id after insert: " + infoString( persister ), + idSelectSql + ); } finally { - jdbcCoordinator.getLogicalConnection().getResourceRegistry().release( idSelect ); - jdbcCoordinator.afterStatementExecution(); + jdbcCoordinator.getLogicalConnection().getResourceRegistry().release( resultSet, idSelect ); } + } + finally { + jdbcCoordinator.getLogicalConnection().getResourceRegistry().release( idSelect ); + jdbcCoordinator.afterStatementExecution(); + } + } + private void bindParameters( + Object entity, + SharedSessionContractImplementor session, + PreparedStatement idSelect, + String idSelectSql) { + try { + bindParameters( entity, idSelect, session ); } - catch (SQLException sqle) { + catch (SQLException e) { throw session.getJdbcServices().getSqlExceptionHelper().convert( - sqle, - "could not retrieve generated id after insert: " + infoString( persister ), - selectSQL + e, + "Unable to bind parameters for post-insert id selection query", + idSelectSql ); } } - } 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 eed0b456c69a..9d9afe5dc52a 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 @@ -5,16 +5,15 @@ package org.hibernate.id.insert; import java.sql.PreparedStatement; -import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; import java.util.Locale; +import java.util.function.Supplier; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.engine.jdbc.mutation.JdbcValueBindings; import org.hibernate.engine.jdbc.mutation.group.PreparedStatementDetails; -import org.hibernate.engine.jdbc.spi.JdbcCoordinator; -import org.hibernate.engine.jdbc.spi.JdbcServices; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.generator.EventType; @@ -89,49 +88,16 @@ public GeneratedValues performMutation( JdbcValueBindings jdbcValueBindings, Object entity, SharedSessionContractImplementor session) { - final JdbcServices jdbcServices = session.getJdbcServices(); - final JdbcCoordinator jdbcCoordinator = session.getJdbcCoordinator(); - final String sql = statementDetails.getSqlString(); - - jdbcServices.getSqlStatementLogger().logStatement( sql ); - + session.getJdbcServices().getSqlStatementLogger().logStatement( sql ); try { final var preparedStatement = statementDetails.resolveStatement(); jdbcValueBindings.beforeStatement( statementDetails ); - - jdbcCoordinator.getResultSetReturn().executeUpdate( preparedStatement, sql ); - - try { - final ResultSet resultSet = preparedStatement.getGeneratedKeys(); - try { - return getGeneratedValues( resultSet, preparedStatement, persister, getTiming(), session ); - } - catch (SQLException e) { - throw jdbcServices.getSqlExceptionHelper().convert( - e, - () -> String.format( - Locale.ROOT, - "Unable to extract generated key from generated-key for `%s`", - persister.getNavigableRole().getFullPath() - ), - sql - ); - } - finally { - if ( resultSet != null ) { - jdbcCoordinator.getLogicalConnection().getResourceRegistry() - .release( resultSet, preparedStatement ); - } - } - } - catch (SQLException e) { - throw jdbcServices.getSqlExceptionHelper().convert( - e, - "Unable to extract generated-keys ResultSet", - sql - ); - } + session.getJdbcCoordinator().getResultSetReturn().executeUpdate( preparedStatement, sql ); + return extractGeneratedValues( session, preparedStatement, sql, + () -> String.format( Locale.ROOT, + "Unable to extract generated key for '%s'", + persister.getNavigableRole().getFullPath() ) ); } finally { if ( statementDetails.getStatement() != null ) { @@ -146,22 +112,25 @@ public GeneratedValues executeAndExtractReturning( String sql, PreparedStatement preparedStatement, SharedSessionContractImplementor session) { - final JdbcCoordinator jdbcCoordinator = session.getJdbcCoordinator(); - final JdbcServices jdbcServices = session.getJdbcServices(); - - jdbcCoordinator.getResultSetReturn().executeUpdate( preparedStatement, sql ); + session.getJdbcCoordinator().getResultSetReturn().executeUpdate( preparedStatement, sql ); + return extractGeneratedValues( session, preparedStatement, sql, + () -> "Unable to extract generated keys from ResultSet" ); + } + private @Nullable GeneratedValues extractGeneratedValues( + SharedSessionContractImplementor session, + PreparedStatement preparedStatement, + String sql, + Supplier message) { + final var jdbcServices = session.getJdbcServices(); + final var jdbcCoordinator = session.getJdbcCoordinator(); try { - final ResultSet resultSet = preparedStatement.getGeneratedKeys(); + final var resultSet = preparedStatement.getGeneratedKeys(); try { return getGeneratedValues( resultSet, preparedStatement, persister, getTiming(), session ); } catch (SQLException e) { - throw jdbcServices.getSqlExceptionHelper().convert( - e, - "Unable to extract generated key(s) from generated-keys ResultSet", - sql - ); + throw jdbcServices.getSqlExceptionHelper().convert( e, message.get(), sql ); } finally { if ( resultSet != null ) { @@ -173,7 +142,7 @@ public GeneratedValues executeAndExtractReturning( catch (SQLException e) { throw jdbcServices.getSqlExceptionHelper().convert( e, - "Unable to extract generated-keys ResultSet", + "Unable to extract generated keys from ResultSet", sql ); } diff --git a/hibernate-core/src/main/java/org/hibernate/id/insert/InsertReturningDelegate.java b/hibernate-core/src/main/java/org/hibernate/id/insert/InsertReturningDelegate.java index 79a2a259244c..971c9b5c2867 100644 --- a/hibernate-core/src/main/java/org/hibernate/id/insert/InsertReturningDelegate.java +++ b/hibernate-core/src/main/java/org/hibernate/id/insert/InsertReturningDelegate.java @@ -5,7 +5,6 @@ package org.hibernate.id.insert; import java.sql.PreparedStatement; -import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; @@ -41,13 +40,9 @@ public class InsertReturningDelegate extends AbstractReturningDelegate { private final List generatedColumns; public InsertReturningDelegate(EntityPersister persister, EventType timing) { - super( - persister, - timing, - true, + super( persister, timing, true, persister.getFactory().getJdbcServices().getDialect() - .supportsInsertReturningRowId() - ); + .supportsInsertReturningRowId() ); tableReference = new MutatingTableReference( persister.getIdentifierTableMapping() ); final var resultBuilders = jdbcValuesMappingProducer.getResultBuilders(); generatedColumns = new ArrayList<>( resultBuilders.size() ); @@ -71,7 +66,7 @@ protected GeneratedValues executeAndExtractReturning( String sql, PreparedStatement preparedStatement, SharedSessionContractImplementor session) { - final ResultSet resultSet = + final var resultSet = session.getJdbcCoordinator().getResultSetReturn() .execute( preparedStatement, sql ); try { @@ -80,7 +75,7 @@ protected GeneratedValues executeAndExtractReturning( catch (SQLException sqle) { throw session.getJdbcServices().getSqlExceptionHelper().convert( sqle, - "Unable to extract generated key(s) from generated-keys ResultSet", + "Unable to extract generated keys from ResultSet", sql ); } diff --git a/hibernate-core/src/main/java/org/hibernate/id/insert/SybaseJConnGetGeneratedKeysDelegate.java b/hibernate-core/src/main/java/org/hibernate/id/insert/SybaseJConnGetGeneratedKeysDelegate.java index f1988411468d..ee0ab07f1c23 100644 --- a/hibernate-core/src/main/java/org/hibernate/id/insert/SybaseJConnGetGeneratedKeysDelegate.java +++ b/hibernate-core/src/main/java/org/hibernate/id/insert/SybaseJConnGetGeneratedKeysDelegate.java @@ -5,7 +5,6 @@ package org.hibernate.id.insert; import java.sql.PreparedStatement; -import java.sql.ResultSet; import java.sql.SQLException; import org.hibernate.engine.spi.SharedSessionContractImplementor; @@ -43,17 +42,16 @@ public GeneratedValues executeAndExtractReturning( String sql, PreparedStatement preparedStatement, SharedSessionContractImplementor session) { - final var jdbcCoordinator = session.getJdbcCoordinator(); - final var jdbcServices = session.getJdbcServices(); - - final ResultSet resultSet = jdbcCoordinator.getResultSetReturn().execute( preparedStatement, sql ); + final var resultSet = + session.getJdbcCoordinator().getResultSetReturn() + .execute( preparedStatement, sql ); try { return getGeneratedValues( resultSet, preparedStatement, persister, getTiming(), session ); } catch (SQLException e) { - throw jdbcServices.getSqlExceptionHelper().convert( + throw session.getJdbcServices().getSqlExceptionHelper().convert( e, - "Unable to extract generated-keys ResultSet", + "Unable to extract generated keys from ResultSet", sql ); } diff --git a/hibernate-core/src/main/java/org/hibernate/id/insert/UniqueKeySelectingDelegate.java b/hibernate-core/src/main/java/org/hibernate/id/insert/UniqueKeySelectingDelegate.java index 0739702c0394..cf86ab529e60 100644 --- a/hibernate-core/src/main/java/org/hibernate/id/insert/UniqueKeySelectingDelegate.java +++ b/hibernate-core/src/main/java/org/hibernate/id/insert/UniqueKeySelectingDelegate.java @@ -13,7 +13,6 @@ import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.generator.EventType; import org.hibernate.jdbc.Expectation; -import org.hibernate.metamodel.mapping.EntityRowIdMapping; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.sql.model.ast.builder.TableInsertBuilderStandard; import org.hibernate.sql.model.ast.builder.TableMutationBuilder; @@ -46,7 +45,7 @@ public UniqueKeySelectingDelegate( uniqueKeyTypes[i] = persister.getPropertyType( uniqueKeyPropertyNames[i] ); } - final EntityRowIdMapping rowIdMapping = persister.getRowIdMapping(); + final var rowIdMapping = persister.getRowIdMapping(); if ( !persister.isIdentifierAssignedByInsert() || persister.getInsertGeneratedProperties().size() > 1 || rowIdMapping != null ) { diff --git a/hibernate-core/src/main/java/org/hibernate/id/package-info.java b/hibernate-core/src/main/java/org/hibernate/id/package-info.java index 099124f62e7a..bdb21faf6d29 100644 --- a/hibernate-core/src/main/java/org/hibernate/id/package-info.java +++ b/hibernate-core/src/main/java/org/hibernate/id/package-info.java @@ -16,7 +16,7 @@ *
  • {@link org.hibernate.id.enhanced.TableGenerator} - {@code @GeneratedValue(strategy=TABLE)} *
  • {@link org.hibernate.id.uuid.UuidGenerator} - {@code @UuidGenerator} * - *

    + * * @apiNote The remaining id generators are kept around for backward compatibility * and as an implementation detail of the {@code hbm.xml} mapping format. * diff --git a/hibernate-core/src/main/java/org/hibernate/id/uuid/UuidGenerator.java b/hibernate-core/src/main/java/org/hibernate/id/uuid/UuidGenerator.java index c6f786a8664e..e66ef0ab5bed 100644 --- a/hibernate-core/src/main/java/org/hibernate/id/uuid/UuidGenerator.java +++ b/hibernate-core/src/main/java/org/hibernate/id/uuid/UuidGenerator.java @@ -30,7 +30,7 @@ /** * {@linkplain org.hibernate.generator.Generator} for producing {@link UUID} values. - *

    + *

    * Uses a {@linkplain UuidValueGenerator} and {@linkplain ValueTransformer} to * generate the values. * diff --git a/hibernate-core/src/main/java/org/hibernate/id/uuid/UuidVersion7Strategy.java b/hibernate-core/src/main/java/org/hibernate/id/uuid/UuidVersion7Strategy.java index 35ff129960b5..65170aba9361 100644 --- a/hibernate-core/src/main/java/org/hibernate/id/uuid/UuidVersion7Strategy.java +++ b/hibernate-core/src/main/java/org/hibernate/id/uuid/UuidVersion7Strategy.java @@ -54,6 +54,13 @@ public long millis() { return lastTimestamp.toEpochMilli(); } + /** + * Sub-miliseconds part of timestamp (micro- and nanoseconds) mapped to 12 bit integral value. + * Calculated as nanos / 1000000 * 4096 + * + * @param timestamp + * @return + */ private static long nanos(Instant timestamp) { return (long) ((timestamp.getNano() % 1_000_000L) * 0.004096); } @@ -64,9 +71,17 @@ public State getNextState() { return new State( now, randomSequence() ); } else { - final long nextSequence = lastSequence + Holder.numberGenerator.nextLong( 0xFFFF_FFFFL ); - return nextSequence > MAX_RANDOM_SEQUENCE - ? new State( lastTimestamp.plusNanos( 250 ), randomSequence() ) + final long nextSequence = randomSequence(); + /* + * If next random sequence is less or equal to last one sub-millisecond part + * should be incremented to preserve monotonicity of generated UUIDs. + * To do this smallest number of nanoseconds that will always increase + * sub-millisecond part mapped to 12 bits is + * 1_000_000 (nanons per milli) / 4096 (12 bits) = 244.14... + * So 245 is used as smallest integer larger than this value. + */ + return lastSequence >= nextSequence + ? new State( lastTimestamp.plusNanos( 245 ), nextSequence ) : new State( lastTimestamp, nextSequence ); } } @@ -77,7 +92,7 @@ private boolean lastTimestampEarlierThan(Instant now) { } private static long randomSequence() { - return Holder.numberGenerator.nextLong( MAX_RANDOM_SEQUENCE ); + return Holder.numberGenerator.nextLong( MAX_RANDOM_SEQUENCE + 1 ); } } @@ -118,7 +133,7 @@ public UUID generateUuid(final SharedSessionContractImplementor session) { | state.nanos() & 0xFFFL, // LSB bits 0-1 - variant = 4 0x8000_0000_0000_0000L - // LSB bits 2-15 - pseudorandom counter + // LSB bits 2-63 - pseudorandom counter | state.lastSequence ); } 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 2474372bc614..39b5248113af 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java @@ -102,6 +102,7 @@ import org.hibernate.resource.transaction.TransactionRequiredForJoinException; import org.hibernate.resource.transaction.backend.jta.internal.JtaTransactionCoordinatorImpl; import org.hibernate.resource.transaction.spi.TransactionCoordinator; +import org.hibernate.temporal.spi.TransactionIdentifierSupplier; import org.hibernate.type.format.FormatMapper; import org.hibernate.type.spi.TypeConfiguration; @@ -166,10 +167,14 @@ abstract class AbstractSharedSessionContract implements SharedSessionContractImp private final boolean readOnly; private final TimeZone jdbcTimeZone; + private transient TransactionIdentifierSupplier transactionIdSupplier; + // mutable state private CacheMode cacheMode; private Integer jdbcBatchSize; + private transient Object currentTransactionIdentifier; + private boolean criteriaCopyTreeEnabled; private boolean criteriaPlanCacheEnabled; @@ -210,6 +215,8 @@ abstract class AbstractSharedSessionContract implements SharedSessionContractImp final var statementInspector = interpret( options.getStatementInspector() ); + transactionIdSupplier = initializeTransactionIdSupplier( factory ); + if ( options instanceof SharedSessionCreationOptions sharedOptions && sharedOptions.isTransactionCoordinatorShared() ) { isTransactionCoordinatorShared = true; @@ -250,6 +257,13 @@ public void onParentClose() { } } + private static TransactionIdentifierSupplier initializeTransactionIdSupplier(SessionFactoryImplementor factory) { + final var transactionIdentifierService = factory.getTransactionIdentifierService(); + return transactionIdentifierService.useServerTimestamp( factory.getJdbcServices().getDialect() ) + ? null + : transactionIdentifierService.getIdentifierSupplier(); + } + final SessionFactoryOptions getSessionFactoryOptions() { return factoryOptions; } @@ -260,7 +274,8 @@ public SharedStatelessSessionBuilder statelessWithOptions() { @Override protected StatelessSessionImplementor createStatelessSession() { return new StatelessSessionImpl( factory, - new SessionCreationOptionsAdaptor( factory, this, AbstractSharedSessionContract.this ) ); + new SessionCreationOptionsAdaptor( factory, this, + AbstractSharedSessionContract.this ) ); } }; } @@ -482,10 +497,9 @@ public final Object getSessionToken() { @Override public String getTenantIdentifier() { - if ( tenantIdentifier == null ) { - return null; - } - return factory.getTenantIdentifierJavaType().toString( tenantIdentifier ); + return tenantIdentifier == null + ? null + : factory.getTenantIdentifierJavaType().toString( tenantIdentifier ); } @Override @@ -604,6 +618,39 @@ public boolean isTransactionInProgress() { && transactionCoordinator.isTransactionActive(); } + @Override + public Object getCurrentTransactionIdentifier() { + if ( currentTransactionIdentifier != null ) { + return currentTransactionIdentifier; + } + else if ( isTransactionInProgress() ) { + initializeCurrentTransactionIdentifier(); + return currentTransactionIdentifier; + } + else { + return generateCurrentTransactionIdentifier(); + } + } + + private Object generateCurrentTransactionIdentifier() { + return transactionIdSupplier == null + ? null + : transactionIdSupplier.generateTransactionIdentifier( this ); + } + + @Override + public void afterTransactionBegin() { + initializeCurrentTransactionIdentifier(); + } + + protected void initializeCurrentTransactionIdentifier() { + currentTransactionIdentifier = generateCurrentTransactionIdentifier(); + } + + protected void clearTransactionStartInstant() { + currentTransactionIdentifier = null; + } + @Override public void checkTransactionNeededForUpdateOperation(String exceptionMessage) { if ( !factoryOptions.isAllowOutOfTransactionUpdateOperations() @@ -653,6 +700,7 @@ public void beforeTransactionCompletion() { @Override public void afterTransactionCompletion(boolean successful, boolean delayed) { + clearTransactionStartInstant(); cacheTransactionSynchronization.transactionCompleted( successful ); } @@ -1771,6 +1819,8 @@ private void readObject(ObjectInputStream ois) throws IOException, ClassNotFound factory.transactionCoordinatorBuilder.buildTransactionCoordinator( jdbcCoordinator, this ); entityNameResolver = new CoordinatingEntityNameResolver( factory, interceptor ); + + transactionIdSupplier = initializeTransactionIdSupplier( factory ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/internal/CoreMessageLogger.java b/hibernate-core/src/main/java/org/hibernate/internal/CoreMessageLogger.java index 1213a3bb9bb0..3016f7179ddd 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/CoreMessageLogger.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/CoreMessageLogger.java @@ -10,6 +10,7 @@ import java.net.URL; import java.sql.SQLException; import java.sql.SQLWarning; +import java.util.Locale; import java.util.Properties; import org.hibernate.HibernateException; @@ -52,7 +53,7 @@ public interface CoreMessageLogger extends BasicLogger { String NAME = SubSystemLogging.BASE + ".core"; - CoreMessageLogger CORE_LOGGER = Logger.getMessageLogger( MethodHandles.lookup(), CoreMessageLogger.class, NAME ); + CoreMessageLogger CORE_LOGGER = Logger.getMessageLogger( MethodHandles.lookup(), CoreMessageLogger.class, NAME, Locale.ROOT ); @LogMessage(level = INFO) @Message(value = "Hibernate ORM core version %s", id = 1) @@ -234,6 +235,10 @@ void missingArguments( @Message(value = "Unable to mark for rollback on TransientObjectException: ", id = 338) void unableToMarkForRollbackOnTransientObjectException(@Cause Exception e); + @LogMessage(level = ERROR) + @Message(value = "Unable to mark for rollback on DetachedObjectException: ", id = 339) + void unableToMarkForRollbackOnDetachedObjectException(@Cause Exception e); + @LogMessage(level = ERROR) @Message(value = "Could not release a cache lock: %s", id = 353) void unableToReleaseCacheLock(CacheException ce); @@ -270,10 +275,6 @@ void missingArguments( @Message(value = "Warnings creating temp table: %s", id = 413) void warningsCreatingTempTable(SQLWarning warning); - @LogMessage(level = WARN) - @Message(value = "Write locks via update not supported for non-versioned entities [%s]", id = 416) - void writeLocksNotSupported(String entityName); - @LogMessage(level = WARN) @Message( value = """ diff --git a/hibernate-core/src/main/java/org/hibernate/internal/ExceptionConverterImpl.java b/hibernate-core/src/main/java/org/hibernate/internal/ExceptionConverterImpl.java index 40fbce66c837..e9fadea15895 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/ExceptionConverterImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/ExceptionConverterImpl.java @@ -8,6 +8,7 @@ import java.sql.SQLException; import org.hibernate.AssertionFailure; +import org.hibernate.DetachedObjectException; import org.hibernate.HibernateException; import org.hibernate.JDBCException; import org.hibernate.LockOptions; @@ -96,8 +97,13 @@ else if ( exception instanceof LockingStrategyException lockingStrategyException else if ( exception instanceof SnapshotIsolationException ) { return new OptimisticLockException( exception.getMessage(), exception ); } - else if ( exception instanceof org.hibernate.QueryTimeoutException ) { - final var converted = new QueryTimeoutException( exception.getMessage(), exception ); + else if ( exception instanceof org.hibernate.QueryTimeoutException queryTimeoutException ) { + final var converted = + isMarkedForRollback( queryTimeoutException ) + // per spec, we must throw this exception if the tx was aborted + ? new PersistenceException( exception.getMessage(), exception ) + // per spec, we only throw this exception if the tx is not aborted + : new QueryTimeoutException( exception.getMessage(), exception ); rollbackIfNecessary( converted ); return converted; } @@ -141,6 +147,16 @@ else if ( exception instanceof TransientObjectException ) { //Spec 3.2.3 Synchronization rules return new IllegalStateException( exception ); } + else if ( exception instanceof DetachedObjectException ) { + try { + session.markForRollbackOnly(); + } + catch (Exception ne) { + //we do not want the subsequent exception to swallow the original one + CORE_LOGGER.unableToMarkForRollbackOnDetachedObjectException( ne ); + } + throw new IllegalArgumentException( exception ); + } else if ( exception instanceof TransactionSerializationException ) { final var converted = new RollbackException( exception.getMessage(), exception ); rollbackIfNecessary( converted ); @@ -211,9 +227,12 @@ protected PersistenceException wrapLockException(LockingStrategyException except return new OptimisticLockException( message, lockException, entity ); } else if ( exception instanceof PessimisticEntityLockException lockException ) { - // assume lock timeout occurred if a timeout or NO WAIT was specified - return lockOptions != null && lockOptions.getTimeout().milliseconds() > -1 + // assume a lock timeout occurred if a timeout or NO WAIT was specified + return !isMarkedForRollback( lockException.getCause() ) + && hasTimeout( lockOptions ) + // per spec, we only throw this exception if the tx is not aborted ? new LockTimeoutException( message, lockException, entity ) + // per spec, we must throw this exception if the tx was aborted : new PessimisticLockException( message, lockException, entity ); } else { @@ -223,17 +242,33 @@ else if ( exception instanceof PessimisticEntityLockException lockException ) { protected PersistenceException wrapLockException(org.hibernate.PessimisticLockException exception, LockOptions lockOptions) { final String message = exception.getMessage(); + final boolean markedForRollback = isMarkedForRollback( exception ); if ( exception instanceof org.hibernate.exception.LockTimeoutException ) { - return new LockTimeoutException( message, exception ); + return markedForRollback + // per spec, we must throw this exception if the tx was aborted + ? new PessimisticLockException( message, exception ) + // per spec, we only throw this exception if the tx is not aborted + : new LockTimeoutException( message, exception ); } else { - // assume lock timeout occurred if a timeout or NO WAIT was specified - return lockOptions != null && lockOptions.getTimeout().milliseconds() > -1 + // assume a lock timeout occurred if a timeout or NO WAIT was specified + return !markedForRollback && hasTimeout( lockOptions ) + // per spec, we only throw this exception if the tx is not aborted ? new LockTimeoutException( message, exception ) + // per spec, we must throw this exception if the tx was aborted : new PessimisticLockException( message, exception ); } } + private static boolean hasTimeout(LockOptions lockOptions) { + return lockOptions != null + && lockOptions.getTimeout().milliseconds() > -1; + } + + private boolean isMarkedForRollback(JDBCException exception) { + return session.getJdbcServices().getDialect().causesRollback( exception.getSQLException() ); + } + private void rollbackIfNecessary(PersistenceException persistenceException) { if ( !isNonRollbackException( persistenceException ) ) { try { diff --git a/hibernate-core/src/main/java/org/hibernate/internal/FilterImpl.java b/hibernate-core/src/main/java/org/hibernate/internal/FilterImpl.java index ec35374aa238..bcb767f83f01 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/FilterImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/FilterImpl.java @@ -37,9 +37,11 @@ public class FilterImpl implements Filter, Serializable { private @Nullable TreeMap parameters; private final boolean autoEnabled; private final boolean applyToLoadByKey; + private transient boolean validated; void afterDeserialize(SessionFactoryImplementor factory) { definition = factory.getFilterDefinition( filterName ); + validated = false; validate(); } @@ -117,6 +119,7 @@ public Filter setParameter(String name, Object value) throws IllegalArgumentExce parameters = new TreeMap<>(); } parameters.put( name, argument ); + validated = false; return this; } @@ -148,6 +151,7 @@ public Filter setParameterList(String name, Collection values) throws Hiberna parameters = new TreeMap<>(); } parameters.put( name, values ); + validated = false; return this; } @@ -184,6 +188,9 @@ public Supplier getParameterResolver(String name) { * @throws HibernateException If the state is not currently valid. */ public void validate() throws HibernateException { + if ( validated ) { + return; + } // for each of the defined parameters, make sure its argument // has been set or a resolver has been implemented and specified for ( final String parameterName : definition.getParameterNames() ) { @@ -192,6 +199,7 @@ public void validate() throws HibernateException { + "' has neither an argument nor a resolver" ); } } + validated = true; } private boolean hasResolver(String parameterName) { diff --git a/hibernate-core/src/main/java/org/hibernate/internal/MultiIdentifierLoadAccessImpl.java b/hibernate-core/src/main/java/org/hibernate/internal/MultiIdentifierLoadAccessImpl.java index e2fdedb0f9d1..10b05f7fcc7f 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/MultiIdentifierLoadAccessImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/MultiIdentifierLoadAccessImpl.java @@ -7,11 +7,15 @@ import jakarta.persistence.EntityGraph; import jakarta.persistence.PessimisticLockScope; import jakarta.persistence.Timeout; +import org.hibernate.BatchSize; import org.hibernate.CacheMode; +import org.hibernate.KeyType; import org.hibernate.LockMode; import org.hibernate.LockOptions; import org.hibernate.MultiIdentifierLoadAccess; +import org.hibernate.NaturalIdSynchronization; import org.hibernate.OrderingMode; +import org.hibernate.ReadOnlyMode; import org.hibernate.RemovalsMode; import org.hibernate.SessionCheckMode; import org.hibernate.UnknownProfileException; @@ -19,19 +23,23 @@ import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.graph.GraphSemantic; import org.hibernate.graph.spi.RootGraphImplementor; +import org.hibernate.internal.find.FindMultipleByKeyOperation; import org.hibernate.loader.ast.spi.MultiIdLoadOptions; +import org.hibernate.loader.internal.LoadAccessContext; import org.hibernate.persister.entity.EntityPersister; import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.function.Supplier; import static java.util.Collections.emptyList; -/** - * @author Steve Ebersole - */ +/// Implementation of MultiIdentifierLoadAccess. +/// +/// @author Steve Ebersole +/// +/// @deprecated Use [FindMultipleByKeyOperation] instead. +@Deprecated class MultiIdentifierLoadAccessImpl implements MultiIdentifierLoadAccess, MultiIdLoadOptions { private final SharedSessionContractImplementor session; private final EntityPersister entityPersister; @@ -43,7 +51,7 @@ class MultiIdentifierLoadAccessImpl implements MultiIdentifierLoadAccess, private RootGraphImplementor rootGraph; private GraphSemantic graphSemantic; - private Integer batchSize; + private BatchSize batchSize; private SessionCheckMode sessionCheckMode = SessionCheckMode.DISABLED; private RemovalsMode removalsMode = RemovalsMode.REPLACE; protected OrderingMode orderingMode = OrderingMode.ORDERED; @@ -107,12 +115,12 @@ public MultiIdentifierLoadAccess with(EntityGraph graph, GraphSemantic sem @Override public Integer getBatchSize() { - return batchSize; + return batchSize.batchSize(); } @Override public MultiIdentifierLoadAccess withBatchSize(int batchSize) { - this.batchSize = batchSize < 1 ? null : batchSize; + this.batchSize = batchSize < 1 ? null : new BatchSize( batchSize ); return this; } @@ -164,45 +172,25 @@ public Boolean getReadOnly(SessionImplementor session) { @Override @SuppressWarnings( "unchecked" ) public List multiLoad(K... ids) { - return perform( () -> (List) entityPersister.multiLoad( ids, session, this ) ); - } - - public List perform(Supplier> executor) { - final var sessionCacheMode = session.getCacheMode(); - boolean cacheModeChanged = false; - if ( cacheMode != null ) { - // naive check for now... - // todo : account for "conceptually equal" - if ( cacheMode != sessionCacheMode ) { - session.setCacheMode( cacheMode ); - cacheModeChanged = true; - } - } + return buildOperation().performFind( List.of( ids ), graphSemantic, rootGraph, (LoadAccessContext) session ); + } - try { - final var influencers = session.getLoadQueryInfluencers(); - final var fetchProfiles = - influencers.adjustFetchProfiles( disabledFetchProfiles, enabledFetchProfiles ); - final var effectiveEntityGraph = - rootGraph == null - ? null - : influencers.applyEntityGraph( rootGraph, graphSemantic ); - try { - return executor.get(); - } - finally { - if ( effectiveEntityGraph != null ) { - effectiveEntityGraph.clear(); - } - influencers.setEnabledFetchProfileNames( fetchProfiles ); - } - } - finally { - if ( cacheModeChanged ) { - // change it back - session.setCacheMode( sessionCacheMode ); - } - } + private FindMultipleByKeyOperation buildOperation() { + return new FindMultipleByKeyOperation( + entityPersister, + KeyType.IDENTIFIER, + batchSize, + sessionCheckMode, + removalsMode, + orderingMode, + cacheMode, + lockOptions, + readOnly == Boolean.TRUE ? ReadOnlyMode.READ_ONLY : ReadOnlyMode.READ_WRITE, + enabledFetchProfiles, + disabledFetchProfiles, + // irrelevant for load-by-id + NaturalIdSynchronization.DISABLED + ); } @Override @@ -210,7 +198,7 @@ public List perform(Supplier> executor) { public List multiLoad(List ids) { return ids.isEmpty() ? emptyList() - : perform( () -> (List) entityPersister.multiLoad( ids.toArray(), session, this ) ); + : buildOperation().performFind( (List)ids, graphSemantic, rootGraph, (LoadAccessContext) session ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/internal/NaturalIdHelper.java b/hibernate-core/src/main/java/org/hibernate/internal/NaturalIdHelper.java index b607a9f59997..b9cde2b804f3 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/NaturalIdHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/NaturalIdHelper.java @@ -43,7 +43,7 @@ public static void performAnyNeededCrossReferenceSynchronizations( // first check if synchronization (this process) was disabled if ( synchronizationEnabled // only mutable natural-ids need this processing - && entityMappingType.getNaturalIdMapping().isMutable() + && entityMappingType.requireNaturalIdMapping().isMutable() // skip synchronization when not in a transaction && session.isTransactionInProgress() ) { final var persister = entityMappingType.getEntityPersister(); diff --git a/hibernate-core/src/main/java/org/hibernate/internal/NaturalIdMultiLoadAccessStandard.java b/hibernate-core/src/main/java/org/hibernate/internal/NaturalIdMultiLoadAccessStandard.java index bb84eb30e4e6..ce8b2adca103 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/NaturalIdMultiLoadAccessStandard.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/NaturalIdMultiLoadAccessStandard.java @@ -4,30 +4,38 @@ */ package org.hibernate.internal; -import java.util.List; - import jakarta.persistence.EntityGraph; - import jakarta.persistence.PessimisticLockScope; import jakarta.persistence.Timeout; +import org.hibernate.BatchSize; import org.hibernate.CacheMode; +import org.hibernate.KeyType; import org.hibernate.LockMode; import org.hibernate.LockOptions; +import org.hibernate.Locking; import org.hibernate.NaturalIdMultiLoadAccess; +import org.hibernate.NaturalIdSynchronization; import org.hibernate.OrderingMode; +import org.hibernate.ReadOnlyMode; import org.hibernate.RemovalsMode; +import org.hibernate.SessionCheckMode; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.graph.GraphSemantic; import org.hibernate.graph.spi.RootGraphImplementor; +import org.hibernate.internal.find.FindMultipleByKeyOperation; import org.hibernate.loader.ast.spi.MultiNaturalIdLoadOptions; +import org.hibernate.loader.internal.LoadAccessContext; import org.hibernate.persister.entity.EntityPersister; -import static org.hibernate.internal.NaturalIdHelper.performAnyNeededCrossReferenceSynchronizations; +import java.util.List; -/** - * @author Steve Ebersole - */ -class NaturalIdMultiLoadAccessStandard implements NaturalIdMultiLoadAccess, MultiNaturalIdLoadOptions { +/// Implementation of NaturalIdMultiLoadAccess. +/// +/// @deprecated Use [FindMultipleByKeyOperation] instead. +/// +/// @author Steve Ebersole +@Deprecated +public class NaturalIdMultiLoadAccessStandard implements NaturalIdMultiLoadAccess, MultiNaturalIdLoadOptions { private final EntityPersister entityDescriptor; private final SharedSessionContractImplementor session; @@ -41,7 +49,7 @@ class NaturalIdMultiLoadAccessStandard implements NaturalIdMultiLoadAccess private RemovalsMode removalsMode = RemovalsMode.REPLACE; private OrderingMode orderingMode = OrderingMode.ORDERED; - NaturalIdMultiLoadAccessStandard(EntityPersister entityDescriptor, SharedSessionContractImplementor session) { + public NaturalIdMultiLoadAccessStandard(EntityPersister entityDescriptor, SharedSessionContractImplementor session) { this.entityDescriptor = entityDescriptor; this.session = session; } @@ -56,6 +64,13 @@ public NaturalIdMultiLoadAccess with(LockMode lockMode, PessimisticLockScope return this; } + public void with(Locking.Scope scope) { + if ( lockOptions == null ) { + lockOptions = new LockOptions(); + } + lockOptions.setScope( scope ); + } + @Override public NaturalIdMultiLoadAccess with(Timeout timeout) { if ( lockOptions == null ) { @@ -96,72 +111,47 @@ public NaturalIdMultiLoadAccess enableReturnOfDeletedEntities(boolean enabled return this; } + public void with(RemovalsMode removalsMode) { + this.removalsMode = removalsMode; + } + @Override public NaturalIdMultiLoadAccess enableOrderedReturn(boolean enabled) { this.orderingMode = enabled ? OrderingMode.ORDERED : OrderingMode.UNORDERED; return this; } + public void with(OrderingMode orderingMode) { + this.orderingMode = orderingMode; + } + @Override - @SuppressWarnings( "unchecked" ) public List multiLoad(Object... ids) { - performAnyNeededCrossReferenceSynchronizations( true, entityDescriptor, session ); - - final CacheMode sessionCacheMode = session.getCacheMode(); - boolean cacheModeChanged = false; - - if ( cacheMode != null ) { - // naive check for now... - // todo : account for "conceptually equal" - if ( cacheMode != sessionCacheMode ) { - session.setCacheMode( cacheMode ); - cacheModeChanged = true; - } - } - - final var loadQueryInfluencers = session.getLoadQueryInfluencers(); - - try { - final var effectiveEntityGraph = loadQueryInfluencers.getEffectiveEntityGraph(); - final var initialGraphSemantic = effectiveEntityGraph.getSemantic(); - final var initialGraph = effectiveEntityGraph.getGraph(); - final boolean hadInitialGraph = initialGraphSemantic != null; - - if ( graphSemantic != null ) { - if ( rootGraph == null ) { - throw new IllegalArgumentException( "Graph semantic specified, but no RootGraph was supplied" ); - } - effectiveEntityGraph.applyGraph( rootGraph, graphSemantic ); - } - - try { - return (List) - entityDescriptor.getMultiNaturalIdLoader() - .multiLoad( ids, this, session ); - } - finally { - if ( graphSemantic != null ) { - if ( hadInitialGraph ) { - effectiveEntityGraph.applyGraph( initialGraph, initialGraphSemantic ); - } - else { - effectiveEntityGraph.clear(); - } - } - } - } - finally { - if ( cacheModeChanged ) { - // change it back - session.setCacheMode( sessionCacheMode ); - } - } - + return buildOperation() + .performFind( List.of( ids ), graphSemantic, rootGraph, (LoadAccessContext) session ); } @Override public List multiLoad(List ids) { - return multiLoad( ids.toArray( new Object[ 0 ] ) ); + return buildOperation() + .performFind( ids, graphSemantic, rootGraph, (LoadAccessContext) session ); + } + + private FindMultipleByKeyOperation buildOperation() { + return new FindMultipleByKeyOperation( + entityDescriptor, + KeyType.NATURAL, + batchSize == null ? null : new BatchSize( batchSize ), + SessionCheckMode.ENABLED, + removalsMode, + orderingMode, + cacheMode, + lockOptions, + session.isDefaultReadOnly() ? ReadOnlyMode.READ_ONLY : ReadOnlyMode.READ_WRITE, + null, + null, + NaturalIdSynchronization.ENABLED + ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/internal/NonContextualJdbcConnectionAccess.java b/hibernate-core/src/main/java/org/hibernate/internal/NonContextualJdbcConnectionAccess.java index 0361ba625d0b..37c26c0d0d70 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/NonContextualJdbcConnectionAccess.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/NonContextualJdbcConnectionAccess.java @@ -9,7 +9,9 @@ import java.sql.SQLException; import java.util.Objects; +import org.hibernate.HibernateException; import org.hibernate.SessionEventListener; +import org.hibernate.context.spi.TenantCredentialsMapper; import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider; import org.hibernate.engine.jdbc.connections.spi.JdbcConnectionAccess; import org.hibernate.engine.spi.SharedSessionContractImplementor; @@ -42,6 +44,19 @@ public Connection obtainConnection() throws SQLException { final var connectionAcquisitionEvent = eventMonitor.beginJdbcConnectionAcquisitionEvent(); try { listener.jdbcConnectionAcquisitionStart(); + final Object tenantIdentifier = session.getTenantIdentifierValue(); + if ( tenantIdentifier != null ) { + final var tenantCredentialsMapper = getTenantCredentialsMapper(); + if ( tenantCredentialsMapper != null ) { + if ( readOnly ) { + throw new HibernateException( "Credentials-based multitenancy not supported with read-only replicas" ); + } + return connectionProvider.getConnection( + tenantCredentialsMapper.user( tenantIdentifier ), + tenantCredentialsMapper.password( tenantIdentifier ) + ); + } + } return readOnly ? connectionProvider.getReadOnlyConnection() : connectionProvider.getConnection(); @@ -52,6 +67,11 @@ public Connection obtainConnection() throws SQLException { } } + private TenantCredentialsMapper getTenantCredentialsMapper() { + return session.getSessionFactory().getSessionFactoryOptions() + .getTenantCredentialsMapper(); + } + @Override public void releaseConnection(Connection connection) throws SQLException { final var eventMonitor = session.getEventMonitor(); 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 4c29bcfc4df2..37cf5956688a 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/SessionFactoryImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/SessionFactoryImpl.java @@ -91,6 +91,7 @@ import org.hibernate.resource.beans.spi.ManagedBeanRegistry; import org.hibernate.resource.transaction.spi.TransactionCoordinatorBuilder; import org.hibernate.service.ServiceRegistry; +import org.hibernate.temporal.spi.TransactionIdentifierService; import org.hibernate.service.spi.ServiceRegistryImplementor; import org.hibernate.service.spi.SessionFactoryServiceRegistry; import org.hibernate.service.spi.SessionFactoryServiceRegistryFactory; @@ -205,6 +206,7 @@ public class SessionFactoryImpl implements SessionFactoryImplementor { final transient EntityCopyObserverFactory entityCopyObserverFactory; final transient ParameterMarkerStrategy parameterMarkerStrategy; final transient JdbcValuesMappingProducerProvider jdbcValuesMappingProducerProvider; + final transient TransactionIdentifierService transactionIdentifierService; public SessionFactoryImpl( final MetadataImplementor bootMetamodel, @@ -256,6 +258,8 @@ public SessionFactoryImpl( classLoaderService = serviceRegistry.requireService( ClassLoaderService.class ); jdbcValuesMappingProducerProvider = serviceRegistry.requireService( JdbcValuesMappingProducerProvider.class ); + transactionIdentifierService = serviceRegistry.requireService( TransactionIdentifierService.class ); + final var integratorObserver = new IntegratorObserver(); observerChain.addObserver( integratorObserver ); try { @@ -1094,6 +1098,11 @@ boolean connectionProviderHandlesConnectionSchema() { : connectionProvider.handlesConnectionSchema(); } + @Override + public TransactionIdentifierService getTransactionIdentifierService() { + return transactionIdentifierService; + } + // Serialization handling ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /** diff --git a/hibernate-core/src/main/java/org/hibernate/internal/SessionFactoryLogging.java b/hibernate-core/src/main/java/org/hibernate/internal/SessionFactoryLogging.java index 5697463fdfe2..309f4adb160c 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/SessionFactoryLogging.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/SessionFactoryLogging.java @@ -16,6 +16,7 @@ import org.jboss.logging.annotations.ValidIdRange; import java.lang.invoke.MethodHandles; +import java.util.Locale; import java.util.Map; import static org.jboss.logging.Logger.Level.DEBUG; @@ -35,7 +36,7 @@ public interface SessionFactoryLogging extends BasicLogger { String NAME = SubSystemLogging.BASE + ".factory"; - SessionFactoryLogging SESSION_FACTORY_LOGGER = Logger.getMessageLogger( MethodHandles.lookup(), SessionFactoryLogging.class, NAME ); + SessionFactoryLogging SESSION_FACTORY_LOGGER = Logger.getMessageLogger( MethodHandles.lookup(), SessionFactoryLogging.class, NAME, Locale.ROOT ); // ---- SessionFactoryImpl related --------------------------------------------------------------- diff --git a/hibernate-core/src/main/java/org/hibernate/internal/SessionFactoryRegistryMessageLogger.java b/hibernate-core/src/main/java/org/hibernate/internal/SessionFactoryRegistryMessageLogger.java index 96835db000ea..682030b90656 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/SessionFactoryRegistryMessageLogger.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/SessionFactoryRegistryMessageLogger.java @@ -17,6 +17,7 @@ import javax.naming.NamingException; import java.lang.invoke.MethodHandles; +import java.util.Locale; import static org.jboss.logging.Logger.Level.DEBUG; import static org.jboss.logging.Logger.Level.ERROR; @@ -36,7 +37,7 @@ public interface SessionFactoryRegistryMessageLogger extends BasicLogger { String LOGGER_NAME = SubSystemLogging.BASE + ".factoryRegistry"; SessionFactoryRegistryMessageLogger REGISTRY_LOGGER = - getMessageLogger( MethodHandles.lookup(), SessionFactoryRegistryMessageLogger.class, LOGGER_NAME ); + getMessageLogger( MethodHandles.lookup(), SessionFactoryRegistryMessageLogger.class, LOGGER_NAME, Locale.ROOT ); @LogMessage(level = TRACE) @Message("Initializing SessionFactoryRegistry @%s") diff --git a/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java b/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java index a37ec581e42c..dd6c6a6904fc 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java @@ -40,9 +40,12 @@ import org.hibernate.event.service.spi.EventListenerGroups; import org.hibernate.event.spi.*; import org.hibernate.event.spi.LoadEventListener.LoadType; +import org.hibernate.exception.GenericJDBCException; +import org.hibernate.exception.JDBCConnectionException; import org.hibernate.graph.GraphSemantic; -import org.hibernate.graph.RootGraph; import org.hibernate.graph.spi.RootGraphImplementor; +import org.hibernate.internal.find.FindByKeyOperation; +import org.hibernate.internal.find.FindMultipleByKeyOperation; import org.hibernate.internal.util.ExceptionHelper; import org.hibernate.jpa.internal.LegacySpecHelper; import org.hibernate.jpa.internal.util.ConfigurationHelper; @@ -86,7 +89,6 @@ import static java.lang.Boolean.parseBoolean; import static java.lang.Integer.parseInt; import static java.lang.System.currentTimeMillis; -import static java.util.Collections.unmodifiableMap; import static org.hibernate.CacheMode.fromJpaModes; import static org.hibernate.Timeouts.WAIT_FOREVER_MILLI; import static org.hibernate.cfg.AvailableSettings.CRITERIA_COPY_TREE; @@ -158,8 +160,8 @@ public class SessionImpl implements Serializable, SharedSessionContractImplementor, JdbcSessionOwner, SessionImplementor, EventSource, TransactionCoordinatorBuilder.Options, WrapperOptions, LoadAccessContext { - // Defaults to null which means the properties are the default - // as defined in FastSessionServices#defaultSessionProperties + // Defaults to null, meaning the properties + // are the default properties of the factory. private Map properties; private transient ActionQueue actionQueue; @@ -191,8 +193,6 @@ public SessionImpl(SessionFactoryImpl factory, SessionCreationOptions options) { actionQueue = createActionQueue(); eventListenerGroups = factory.getEventListenerGroups(); - flushMode = options.getInitialSessionFlushMode(); - autoClear = options.shouldAutoClear(); autoClose = options.shouldAutoClose(); @@ -202,13 +202,10 @@ public SessionImpl(SessionFactoryImpl factory, SessionCreationOptions options) { loadQueryInfluencers = new LoadQueryInfluencers( factory, options ); - // NOTE : pulse() already handles auto-join-ability correctly + // NOTE: pulse() already handles auto-join-ability correctly getTransactionCoordinator().pulse(); - // do not override explicitly set flush mode ( SessionBuilder#flushMode() ) - if ( getHibernateFlushMode() == null ) { - setHibernateFlushMode( getInitialFlushMode() ); - } + flushMode = getInitialFlushMode( options ); setUpMultitenancy( factory, loadQueryInfluencers ); @@ -247,10 +244,16 @@ private static void setUpTransactionCompletionProcesses( } } - private FlushMode getInitialFlushMode() { - return properties == null - ? getSessionFactoryOptions().getInitialSessionFlushMode() - : ConfigurationHelper.getFlushMode( getSessionProperty( HINT_FLUSH_MODE ), FlushMode.AUTO ); + private FlushMode getInitialFlushMode(SessionCreationOptions options) { + final var initialSessionFlushMode = options.getInitialSessionFlushMode(); + if ( initialSessionFlushMode != null ) { + return initialSessionFlushMode; + } + else { + return properties == null + ? getSessionFactoryOptions().getInitialSessionFlushMode() + : ConfigurationHelper.getFlushMode( properties.get( HINT_FLUSH_MODE ), FlushMode.AUTO ); + } } protected PersistenceContext createPersistenceContext(SessionCreationOptions options) { @@ -351,7 +354,6 @@ public void clear() { private void internalClear() { persistenceContext.clear(); actionQueue.clear(); - eventListenerGroups.eventListenerGroup_CLEAR .fireLazyEventOnEachListener( this::createClearEvent, ClearEventListener::onClear ); } @@ -392,7 +394,7 @@ public void closeWithoutOpenChecks() { else { // In the JPA bootstrap, if the session is closed // before the transaction commits, we just mark the - // session as closed, and set waitingForAutoClose. + // session as closed and set waitingForAutoClose. // This method will be called a second time from // afterTransactionCompletion when the transaction // commits, and the session will be closed for real. @@ -405,10 +407,12 @@ public void closeWithoutOpenChecks() { } } finally { - // E.g. when we are in the JTA context the session can get closed while the transaction is still active - // and JTA will call the AfterCompletion itself. Hence, we don't want to clear out the action queue callbacks at this point: - if ( !getTransactionCoordinator().isTransactionActive() && actionQueue.hasAfterTransactionActions() ) { - SESSION_LOGGER.warn( "Closing session with unprocessed clean up bulk operations, forcing their execution" ); + // E.g. When we are in the JTA context, the session can get closed while the + // transaction is still active and JTA will call the AfterCompletion itself. + // Hence, we don't want to clear out the action queue callbacks at this point: + if ( !getTransactionCoordinator().isTransactionActive() + && actionQueue.hasAfterTransactionActions() ) { + SESSION_LOGGER.closingSessionWithUnprocessedBulkOperations(); actionQueue.executePendingBulkOperationCleanUpActions(); } final var statistics = getSessionFactory().getStatistics(); @@ -489,8 +493,8 @@ else if ( isClosed() ) { return false; } else { - // JPA technically requires that this be a PersistentUnityTransactionType#JTA to work, - // but we do not assert that here: + // JPA requires PersistentUnitTransactionType.JTA, + // for this, but we do not assert that here: return isAutoCloseSessionEnabled(); // && getTransactionCoordinator().getTransactionCoordinatorBuilder().isJta(); } @@ -557,7 +561,9 @@ public Object getEntityUsingInterceptor(EntityKey key) { // logically, is PersistentContext the "thing" to which an interceptor gets attached? final Object result = persistenceContext.getEntity( key ); if ( result == null ) { - final Object newObject = getInterceptor().getEntity( key.getEntityName(), key.getIdentifier() ); + final Object newObject = + getInterceptor() + .getEntity( key.getEntityName(), key.getIdentifier() ); if ( newObject != null ) { lock( newObject, LockMode.NONE ); } @@ -624,7 +630,6 @@ public void lock(Object object, LockMode lockMode, LockOption... lockOptions) { private void fireLock(final LockEvent lockEvent) { checkOpen(); - checkEntityManaged( lockEvent.getEntityName(), lockEvent.getObject() ); try { pulseTransactionCoordinator(); checkTransactionNeededForLock( lockEvent.getLockMode() ); @@ -642,7 +647,11 @@ private void fireLock(final LockEvent lockEvent) { private void convertIfJpaBootstrap(RuntimeException exception, LockOptions lockOptions) { if ( !isJpaBootstrap() && exception instanceof HibernateException ) { - throw exception; + throw exception instanceof DetachedObjectException + // convert to IllegalArgumentException for backward compatibility + // TODO: drop this conversion in Hibernate 8 + ? new IllegalArgumentException( exception ) + : exception; } else if ( exception instanceof MappingException ) { // I believe this is now obsolete everywhere we do it, @@ -870,8 +879,8 @@ public void removeOrphanBeforeUpdates(String entityName, Object child) { private void logRemoveOrphanBeforeUpdates(String timing, String entityName, Object entity) { if ( SESSION_LOGGER.isTraceEnabled() ) { final var entityEntry = persistenceContext.getEntry( entity ); - final String entityInfo = entityEntry == null ? entityName : infoString( entityName, entityEntry.getId() ); - SESSION_LOGGER.removeOrphanBeforeUpdates( timing, entityInfo ); + SESSION_LOGGER.removeOrphanBeforeUpdates( timing, + entityEntry == null ? entityName : infoString( entityName, entityEntry.getId() ) ); } } @@ -928,80 +937,46 @@ public void load(Object object, Object id) { fireLoad( new LoadEvent( id, object, this, getReadOnlyFromLoadQueryInfluencers() ), LoadEventListener.RELOAD ); } - private void setMultiIdentifierLoadAccessOptions(FindOption[] options, MultiIdentifierLoadAccess loadAccess) { - CacheStoreMode storeMode = getCacheStoreMode(); - CacheRetrieveMode retrieveMode = getCacheRetrieveMode(); - LockOptions lockOptions = copySessionLockOptions(); - int batchSize = -1; - for ( FindOption option : options ) { - if ( option instanceof CacheStoreMode cacheStoreMode ) { - storeMode = cacheStoreMode; - } - else if ( option instanceof CacheRetrieveMode cacheRetrieveMode ) { - retrieveMode = cacheRetrieveMode; - } - else if ( option instanceof CacheMode cacheMode ) { - storeMode = cacheMode.getJpaStoreMode(); - retrieveMode = cacheMode.getJpaRetrieveMode(); - } - else if ( option instanceof LockModeType lockModeType ) { - lockOptions.setLockMode( LockModeTypeHelper.getLockMode( lockModeType ) ); - } - else if ( option instanceof LockMode lockMode ) { - lockOptions.setLockMode( lockMode ); - } - else if ( option instanceof LockOptions lockOpts ) { - lockOptions = lockOpts; - } - else if ( option instanceof PessimisticLockScope pessimisticLockScope ) { - lockOptions.setLockScope( pessimisticLockScope ); - } - else if ( option instanceof Timeout timeout ) { - lockOptions.setTimeOut( timeout.milliseconds() ); - } - else if ( option instanceof EnabledFetchProfile enabledFetchProfile ) { - loadAccess.enableFetchProfile( enabledFetchProfile.profileName() ); - } - else if ( option instanceof ReadOnlyMode ) { - loadAccess.withReadOnly( option == ReadOnlyMode.READ_ONLY ); - } - else if ( option instanceof BatchSize batchSizeOption ) { - batchSize = batchSizeOption.batchSize(); - } - else if ( option instanceof SessionCheckMode ) { - loadAccess.enableSessionCheck( option == SessionCheckMode.ENABLED ); - } - else if ( option instanceof OrderingMode ) { - loadAccess.enableOrderedReturn( option == OrderingMode.ORDERED ); - } - else if ( option instanceof RemovalsMode ) { - loadAccess.enableReturnOfDeletedEntities( option == RemovalsMode.INCLUDE ); - } - } - loadAccess.with( lockOptions ) - .with( interpretCacheMode( storeMode, retrieveMode ) ) - .withBatchSize( batchSize ); + @Override + public List findMultiple(Class entityType, List keys, FindOption... options) { + //noinspection unchecked + return findMultiple( + requireEntityPersister( entityType ), + loadQueryInfluencers.getEffectiveEntityGraph().getSemantic(), + (RootGraphImplementor) loadQueryInfluencers.getEffectiveEntityGraph().getGraph(), + (List) keys, + options + ); } - @Override - public List findMultiple(Class entityType, List ids, FindOption... options) { - final var loadAccess = byMultipleIds( entityType ); - setMultiIdentifierLoadAccessOptions( options, loadAccess ); - return loadAccess.multiLoad( ids ); + private List findMultiple( + EntityPersister entityDescriptor, + GraphSemantic graphSemantic, + RootGraphImplementor rootGraph, + List keys, + FindOption... options) { + final var operation = new FindMultipleByKeyOperation( + entityDescriptor, + lockOptions, + getCacheMode(), + isDefaultReadOnly(), + getFactory(), + options + ); + return operation.performFind( keys, graphSemantic, rootGraph, this ); } @Override - public List findMultiple(EntityGraph entityGraph, List ids, FindOption... options) { - final var rootGraph = (RootGraph) entityGraph; + public List findMultiple(EntityGraph entityGraph, List keys, FindOption... options) { + final var rootGraph = (RootGraphImplementor) entityGraph; final var type = rootGraph.getGraphedType(); - final MultiIdentifierLoadAccess loadAccess = - switch ( type.getRepresentationMode() ) { - case MAP -> byMultipleIds( type.getTypeName() ); - case POJO -> byMultipleIds( type.getJavaType() ); - }; - loadAccess.withLoadGraph( rootGraph ); - setMultiIdentifierLoadAccessOptions( options, loadAccess ); - return loadAccess.multiLoad( ids ); + final var entityDescriptor = switch ( type.getRepresentationMode() ) { + case POJO -> requireEntityPersister( type.getJavaType() ); + case MAP -> requireEntityPersister( type.getTypeName() ); + }; + + //noinspection unchecked + return findMultiple( entityDescriptor, GraphSemantic.LOAD, rootGraph, (List) keys, options ); } @Override @@ -1128,6 +1103,7 @@ protected LoadEvent makeLoadEvent(String entityName, Object id, Boolean readOnly event.setReadOnly( readOnly ); event.setLockOptions( lockOptions ); event.setAssociationFetch( false ); + event.validate(); return event; } } @@ -1308,7 +1284,6 @@ public void refresh(String entityName, Object object, RefreshContext refreshedAl private void fireRefresh(final RefreshEvent refreshEvent) { checkOpen(); - checkEntityManaged( refreshEvent.getEntityName(), refreshEvent.getObject() ); try { pulseTransactionCoordinator(); checkTransactionNeededForLock( refreshEvent.getLockMode() ); @@ -1327,7 +1302,6 @@ private void fireRefresh(final RefreshEvent refreshEvent) { private void fireRefresh(final RefreshContext refreshedAlready, final RefreshEvent refreshEvent) { // called from cascades checkOpenOrWaitingForAutoClose(); - checkEntityManaged( refreshEvent.getEntityName(), refreshEvent.getObject() ); try { pulseTransactionCoordinator(); eventListenerGroups.eventListenerGroup_REFRESH @@ -1339,12 +1313,6 @@ private void fireRefresh(final RefreshContext refreshedAlready, final RefreshEve } } - private void checkEntityManaged(String entityName, Object entity) { - if ( !isManaged( entity ) ) { - throw new IllegalArgumentException( "Given entity is not associated with the persistence context" ); - } - } - @Override public boolean isManaged(Object entity) { try { @@ -1552,7 +1520,8 @@ public void forceFlush(EntityEntry entityEntry) { @Override public void forceFlush(EntityKey key) { if ( SESSION_LOGGER.isTraceEnabled() ) { - SESSION_LOGGER.flushingToForceDeletion( infoString( key.getPersister(), key.getIdentifier(), getFactory() ) ); + SESSION_LOGGER.flushingToForceDeletion( + infoString( key.getPersister(), key.getIdentifier(), getFactory() ) ); } if ( persistenceContext.getCascadeLevel() > 0 ) { @@ -2102,6 +2071,7 @@ public void startTransactionBoundary() { @Override public void afterTransactionBegin() { checkOpenOrWaitingForAutoClose(); + super.afterTransactionBegin(); afterTransactionBeginEvents(); } @@ -2123,7 +2093,7 @@ private boolean isTransactionFlushable() { return true; } else { - final TransactionStatus status = currentTransaction.getStatus(); + final var status = currentTransaction.getStatus(); return status == TransactionStatus.ACTIVE || status == TransactionStatus.COMMITTING; } @@ -2189,13 +2159,13 @@ private T find(Class entityClass, Object primaryKey, LockOptions lockOpti .load( primaryKey ); } catch ( FetchNotFoundException e ) { - // This may happen if the entity has an associations mapped with + // This may happen if the entity has an association mapped with // @NotFound(action = NotFoundAction.EXCEPTION) and this associated // entity is not found throw e; } catch ( EntityFilterException e ) { - // This may happen if the entity has an associations which is + // This may happen if the entity has an association which is // filtered by a FilterDef and this associated entity is not found throw e; } @@ -2222,8 +2192,11 @@ private T find(Class entityClass, Object primaryKey, LockOptions lockOpti throw getExceptionConverter().convert( new IllegalArgumentException( e.getMessage(), e ) ); } catch ( JDBCException e ) { - if ( accessTransaction().isActive() && accessTransaction().getRollbackOnly() ) { - // Assume situation HHH-12472 running on WildFly + final Transaction transaction = accessTransaction(); + if ( transaction.isActive() && transaction.getRollbackOnly() + && (e instanceof GenericJDBCException || e instanceof JDBCConnectionException) ) { + // Assume situation HHH-12472 running on WildFly, + // but only if the exception is generic to avoid swallowing locking exceptions (HHH-20260) // Just log the exception and return null SESSION_LOGGER.jdbcExceptionThrownWithTransactionRolledBack( e ); return null; @@ -2251,81 +2224,24 @@ protected static void logIgnoringEntityNotFound(Class entityClass, Object } } - private void setLoadAccessOptions(FindOption[] options, IdentifierLoadAccessImpl loadAccess) { - CacheStoreMode storeMode = getCacheStoreMode(); - CacheRetrieveMode retrieveMode = getCacheRetrieveMode(); - LockOptions lockOptions = copySessionLockOptions(); - for ( FindOption option : options ) { - if ( option instanceof CacheStoreMode cacheStoreMode ) { - storeMode = cacheStoreMode; - } - else if ( option instanceof CacheRetrieveMode cacheRetrieveMode ) { - retrieveMode = cacheRetrieveMode; - } - else if ( option instanceof CacheMode cacheMode ) { - storeMode = cacheMode.getJpaStoreMode(); - retrieveMode = cacheMode.getJpaRetrieveMode(); - } - else if ( option instanceof LockModeType lockModeType ) { - lockOptions.setLockMode( LockModeTypeHelper.getLockMode( lockModeType ) ); - } - else if ( option instanceof LockMode lockMode ) { - lockOptions.setLockMode( lockMode ); - } - else if ( option instanceof LockOptions lockOpts ) { - lockOptions = lockOpts; - } - else if ( option instanceof Locking.Scope lockScope ) { - lockOptions.setScope( lockScope ); - } - else if ( option instanceof PessimisticLockScope pessimisticLockScope ) { - lockOptions.setScope( Locking.Scope.fromJpaScope( pessimisticLockScope ) ); - } - else if ( option instanceof Locking.FollowOn followOn ) { - lockOptions.setFollowOnStrategy( followOn ); - } - else if ( option instanceof Timeout timeout ) { - lockOptions.setTimeout( timeout ); - } - else if ( option instanceof EnabledFetchProfile enabledFetchProfile ) { - loadAccess.enableFetchProfile( enabledFetchProfile.profileName() ); - } - else if ( option instanceof ReadOnlyMode ) { - loadAccess.withReadOnly( option == ReadOnlyMode.READ_ONLY ); - } - else if ( option instanceof FindMultipleOption findMultipleOption ) { - throw new IllegalArgumentException( "Option '" + findMultipleOption + "' can only be used in 'findMultiple()'" ); - } - } - if ( lockOptions.getLockMode().isPessimistic() ) { - if ( lockOptions.getTimeOut() == WAIT_FOREVER_MILLI ) { - final Object factoryHint = getFactory().getProperties().get( HINT_SPEC_LOCK_TIMEOUT ); - if ( factoryHint != null ) { - lockOptions.setTimeOut( Timeouts.fromHint( factoryHint ) ); - } - } - } - loadAccess.with( lockOptions ).with( interpretCacheMode( storeMode, retrieveMode ) ); - } - @Override - public T find(Class entityClass, Object primaryKey, FindOption... options) { - final IdentifierLoadAccessImpl loadAccess = byId( entityClass ); - setLoadAccessOptions( options, loadAccess ); - return loadAccess.load( primaryKey ); + public T find(Class entityClass, Object key, FindOption... options) { + //noinspection unchecked + return (T) byKey( requireEntityPersister( entityClass ), options ).performFind( key, this ); } @Override - public T find(EntityGraph entityGraph, Object primaryKey, FindOption... options) { - final var graph = (RootGraph) entityGraph; + public T find(EntityGraph entityGraph, Object key, FindOption... options) { + final var graph = (RootGraphImplementor) entityGraph; final var type = graph.getGraphedType(); - final IdentifierLoadAccessImpl loadAccess = - switch ( type.getRepresentationMode() ) { - case MAP -> byId( type.getTypeName() ); - case POJO -> byId( type.getJavaType() ); - }; - setLoadAccessOptions( options, loadAccess ); - return loadAccess.withLoadGraph( graph ).load( primaryKey ); + + final EntityPersister entityDescriptor = switch ( type.getRepresentationMode() ) { + case POJO -> requireEntityPersister( type.getJavaType() ); + case MAP -> requireEntityPersister( type.getTypeName() ); + }; + + //noinspection unchecked + return (T) byKey( entityDescriptor, GraphSemantic.LOAD, graph, options ).performFind( key, this ); } // Hibernate Reactive may need to use this @@ -2388,16 +2304,34 @@ private void checkTransactionNeededForUpdateOperation() { } @Override - public Object find(String entityName, Object primaryKey) { - final IdentifierLoadAccessImpl loadAccess = byId( entityName ); - return loadAccess.load( primaryKey ); + public Object find(String entityName, Object key) { + return byKey( requireEntityPersister( entityName ) ).performFind( key, this ); } @Override - public Object find(String entityName, Object primaryKey, FindOption... options) { - final IdentifierLoadAccessImpl loadAccess = byId( entityName ); - setLoadAccessOptions( options, loadAccess ); - return loadAccess.load( primaryKey ); + public Object find(String entityName, Object key, FindOption... options) { + return byKey( requireEntityPersister( entityName ), options ).performFind( key, this ); + } + + private FindByKeyOperation byKey(EntityPersister entityDescriptor, FindOption... options) { + return byKey( entityDescriptor, null, null, options ); + } + + private FindByKeyOperation byKey( + EntityPersister entityDescriptor, + GraphSemantic graphSemantic, + RootGraphImplementor rootGraph, + FindOption... options) { + return new FindByKeyOperation<>( + entityDescriptor, + graphSemantic, + rootGraph, + lockOptions, + getCacheMode(), + isReadOnly(), + getFactory(), + options + ); } @Override @@ -2577,25 +2511,22 @@ public LockModeType getLockMode(Object entity) { @Override public void setProperty(String propertyName, Object value) { checkOpen(); - - if ( !( value instanceof Serializable ) ) { - SESSION_LOGGER.nonSerializableProperty( propertyName ); - return; - } - if ( propertyName == null ) { SESSION_LOGGER.nullPropertyKey(); - return; } - - // store property for future reference: - if ( properties == null ) { - properties = computeCurrentProperties(); + else if ( !(value instanceof Serializable) ) { + SESSION_LOGGER.nonSerializableProperty( propertyName ); + } + else { + // store property for future reference + if ( properties == null ) { + properties = getInitialProperties(); + } + properties.put( propertyName, value ); + // now actually update the setting if + // it's one that affects this Session + interpretProperty( propertyName, value ); } - properties.put( propertyName, value ); - - // now actually update the setting, if it's one which affects this Session - interpretProperty( propertyName, value ); } private void interpretProperty(String propertyName, Object value) { @@ -2658,7 +2589,7 @@ private void interpretProperty(String propertyName, Object value) { } } - private Map computeCurrentProperties() { + private Map getInitialProperties() { final var map = new HashMap<>( getDefaultProperties() ); //The FLUSH_MODE is always set at Session creation time, //so it needs special treatment to not eagerly initialize this Map: @@ -2668,10 +2599,15 @@ private Map computeCurrentProperties() { @Override public Map getProperties() { - if ( properties == null ) { - properties = computeCurrentProperties(); - } - return unmodifiableMap( properties ); + // EntityManager Javadoc implies that the + // returned map should be a mutable copy, + // not an unmodifiable map. There's no + // good reason to cache the initial + // properties, since we have to copy them + // each time this method is called. + return properties == null + ? getInitialProperties() + : new HashMap<>( properties ); } @Override @@ -2780,23 +2716,24 @@ public Collection getManagedEntities(String entityName) { public Collection getManagedEntities(Class entityType) { return persistenceContext.getEntityHoldersByKey().entrySet().stream() .filter( entry -> entry.getKey().getPersister().getMappedClass().equals( entityType ) ) - .map( entry -> (E) entry.getValue().getManagedObject() ) + .map( entry -> entityType.cast( entry.getValue().getManagedObject() ) ) .toList(); } @Override public Collection getManagedEntities(EntityType entityType) { - final String entityName = ( (EntityDomainType) entityType ).getHibernateEntityName(); + final var entityDomainType = (EntityDomainType) entityType; + final String entityName = entityDomainType.getHibernateEntityName(); return persistenceContext.getEntityHoldersByKey().entrySet().stream() .filter( entry -> entry.getKey().getEntityName().equals( entityName ) ) - .map( entry -> (E) entry.getValue().getManagedObject() ) + .map( entry -> entityType.getJavaType().cast( entry.getValue().getManagedObject() ) ) .toList(); } /** - * Used by JDK serialization... + * Used by JDK serialization * - * @param oos The output stream to which we are being written... + * @param oos The output stream to which we are being written * * @throws IOException Indicates a general IO stream exception */ @@ -2815,9 +2752,9 @@ private void writeObject(ObjectOutputStream oos) throws IOException { } /** - * Used by JDK serialization... + * Used by JDK serialization * - * @param ois The input stream from which we are being read... + * @param ois The input stream from which we are being read * * @throws IOException Indicates a general IO stream exception * @throws ClassNotFoundException Indicates a class resolution issue @@ -2837,7 +2774,7 @@ private void readObject(ObjectInputStream ois) throws IOException, ClassNotFound // LoadQueryInfluencers#getEnabledFilters() tries to validate each enabled // filter, which will fail when called before FilterImpl#afterDeserialize( factory ); - // Instead lookup the filter by name and then call FilterImpl#afterDeserialize( factory ). + // Instead, look up the filter by name and then call FilterImpl#afterDeserialize( factory ). for ( String filterName : loadQueryInfluencers.getEnabledFilterNames() ) { ( (FilterImpl) loadQueryInfluencers.getEnabledFilter( filterName ) ) .afterDeserialize( getFactory() ); diff --git a/hibernate-core/src/main/java/org/hibernate/internal/SessionLogging.java b/hibernate-core/src/main/java/org/hibernate/internal/SessionLogging.java index cddf971d6999..f11920add427 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/SessionLogging.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/SessionLogging.java @@ -16,6 +16,7 @@ import org.jboss.logging.annotations.ValidIdRange; import java.lang.invoke.MethodHandles; +import java.util.Locale; import java.util.UUID; import static org.jboss.logging.Logger.Level.DEBUG; @@ -36,7 +37,7 @@ public interface SessionLogging extends BasicLogger { String NAME = SubSystemLogging.BASE + ".session"; - SessionLogging SESSION_LOGGER = Logger.getMessageLogger( MethodHandles.lookup(), SessionLogging.class, NAME ); + SessionLogging SESSION_LOGGER = Logger.getMessageLogger( MethodHandles.lookup(), SessionLogging.class, NAME, Locale.ROOT ); @LogMessage(level = DEBUG) @Message("Session creation specified 'autoJoinTransactions', " @@ -145,6 +146,10 @@ public interface SessionLogging extends BasicLogger { @Message(id = 90010107, value = "Exception in interceptor afterTransactionCompletion()") void exceptionInAfterTransactionCompletionInterceptor(@Cause Throwable e); + @LogMessage(level = WARN) + @Message(id = 90010108, value = "Closing session with unprocessed clean up bulk operations, forcing their execution") + void closingSessionWithUnprocessedBulkOperations(); + // StatelessSession-specific @LogMessage(level = TRACE) 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 f38757327a72..6c9635c3b5d0 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/StatelessSessionImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/StatelessSessionImpl.java @@ -33,7 +33,6 @@ import org.hibernate.engine.spi.TransactionCompletionCallbacks; import org.hibernate.engine.spi.TransactionCompletionCallbacksImplementor; import org.hibernate.engine.transaction.jta.platform.spi.JtaPlatform; -import org.hibernate.event.monitor.spi.DiagnosticEvent; import org.hibernate.event.service.spi.EventListenerGroups; import org.hibernate.event.spi.PostCollectionRecreateEvent; import org.hibernate.event.spi.PostCollectionRecreateEventListener; @@ -150,10 +149,11 @@ public StatelessSessionImpl(SessionFactoryImpl factory, SessionCreationOptions o } temporaryPersistenceContext = createPersistenceContext( this ); influencers = new LoadQueryInfluencers( getFactory() ); + influencers.setTemporalIdentifier( options.getTemporalIdentifier() ); eventListenerGroups = factory.getEventListenerGroups(); setUpMultitenancy( factory, influencers ); - // a nonzero batch size forces use of write-behind - // therefore ignore the value of hibernate.jdbc.batch_size + // A nonzero batch size forces the use of write-behind + // Therefore, ignore the value of hibernate.jdbc.batch_size setJdbcBatchSize( 0 ); } @@ -187,6 +187,7 @@ public void insertMultiple(List entities) { for ( Object entity : entities ) { insert( null, entity ); } + getJdbcCoordinator().executeBatch(); } finally { setJdbcBatchSize( batchSize ); @@ -294,7 +295,7 @@ private void recreateCollections(Object entity, Object id, EntityPersister persi forEachOwnedCollection( entity, id, persister, (descriptor, collection) -> { final String role = descriptor.getRole(); - firePreRecreate( collection, id, entityName, entity ); + firePreRecreate( descriptor, collection, id, entityName, entity ); final var event = eventMonitor.beginCollectionRecreateEvent(); boolean success = false; try { @@ -307,7 +308,7 @@ private void recreateCollections(Object entity, Object id, EntityPersister persi if ( statistics.isStatisticsEnabled() ) { statistics.recreateCollection( role ); } - firePostRecreate( collection, id, entityName, entity ); + firePostRecreate( descriptor, collection, id, entityName, entity ); } ); } } @@ -327,6 +328,7 @@ public void deleteMultiple(List entities) { for ( Object entity : entities ) { delete( null, entity ); } + getJdbcCoordinator().executeBatch(); } finally { setJdbcBatchSize( batchSize ); @@ -371,8 +373,8 @@ private void removeCollections(Object entity, Object id, EntityPersister persist forEachOwnedCollection( entity, id, persister, (descriptor, collection) -> { final String role = descriptor.getRole(); - firePreRemove( collection, id, entityName, entity ); - final DiagnosticEvent event = eventMonitor.beginCollectionRemoveEvent(); + firePreRemove( descriptor, collection, id, entityName, entity ); + final var event = eventMonitor.beginCollectionRemoveEvent(); boolean success = false; try { descriptor.remove( id, this ); @@ -381,7 +383,7 @@ private void removeCollections(Object entity, Object id, EntityPersister persist finally { eventMonitor.completeCollectionRemoveEvent( event, id, role, success, this ); } - firePostRemove( collection, id, entityName, entity ); + firePostRemove( descriptor, collection, id, entityName, entity ); if ( statistics.isStatisticsEnabled() ) { statistics.removeCollection( role ); } @@ -405,6 +407,7 @@ public void updateMultiple(List entities) { for ( Object entity : entities ) { update( null, entity ); } + getJdbcCoordinator().executeBatch(); } finally { setJdbcBatchSize( batchSize ); @@ -459,8 +462,8 @@ private void removeAndRecreateCollections(Object entity, Object id, EntityPersis forEachOwnedCollection( entity, id, persister, (descriptor, collection) -> { final String role = descriptor.getRole(); - firePreUpdate( collection, id, entityName, entity ); - final DiagnosticEvent event = eventMonitor.beginCollectionRemoveEvent(); + firePreUpdate( descriptor, collection, id, entityName, entity ); + final var event = eventMonitor.beginCollectionRemoveEvent(); boolean success = false; try { // TODO: can we do better here? @@ -471,7 +474,7 @@ private void removeAndRecreateCollections(Object entity, Object id, EntityPersis finally { eventMonitor.completeCollectionRemoveEvent( event, id, role, success, this ); } - firePostUpdate( collection, id, entityName, entity ); + firePostUpdate( descriptor, collection, id, entityName, entity ); if ( statistics.isStatisticsEnabled() ) { statistics.updateCollection( role ); } @@ -492,6 +495,7 @@ public void upsertMultiple(List entities) { for ( Object entity : entities ) { upsert( null, entity ); } + getJdbcCoordinator().executeBatch(); } finally { setJdbcBatchSize( batchSize ); @@ -668,44 +672,74 @@ protected void firePostDelete(Object entity, Object id, EntityPersister persiste } // Hibernate Reactive may need to call this - protected void firePreRecreate(PersistentCollection collection, Object id, String entityName, Object owner) { + protected void firePreRecreate( + CollectionPersister collectionPersister, + PersistentCollection collection, + Object id, + String entityName, + Object owner) { eventListenerGroups.eventListenerGroup_PRE_COLLECTION_RECREATE.fireLazyEventOnEachListener( - () -> new PreCollectionRecreateEvent( collection, id, entityName, owner ), + () -> new PreCollectionRecreateEvent( collectionPersister, collection, id, entityName, owner ), PreCollectionRecreateEventListener::onPreRecreateCollection ); } // Hibernate Reactive may need to call this - protected void firePreUpdate(PersistentCollection collection, Object id, String entityName, Object owner) { + protected void firePreUpdate( + CollectionPersister collectionPersister, + PersistentCollection collection, + Object id, + String entityName, + Object owner) { eventListenerGroups.eventListenerGroup_PRE_COLLECTION_UPDATE.fireLazyEventOnEachListener( - () -> new PreCollectionUpdateEvent( collection, id, entityName, owner ), + () -> new PreCollectionUpdateEvent( collectionPersister, collection, id, entityName, owner ), PreCollectionUpdateEventListener::onPreUpdateCollection ); } // Hibernate Reactive may need to call this - protected void firePreRemove(PersistentCollection collection, Object id, String entityName, Object owner) { + protected void firePreRemove( + CollectionPersister collectionPersister, + PersistentCollection collection, + Object id, + String entityName, + Object owner) { eventListenerGroups.eventListenerGroup_PRE_COLLECTION_REMOVE.fireLazyEventOnEachListener( - () -> new PreCollectionRemoveEvent( collection, id, entityName, owner ), + () -> new PreCollectionRemoveEvent( collectionPersister, collection, id, entityName, owner ), PreCollectionRemoveEventListener::onPreRemoveCollection ); } // Hibernate Reactive may need to call this - protected void firePostRecreate(PersistentCollection collection, Object id, String entityName, Object owner) { + protected void firePostRecreate( + CollectionPersister collectionPersister, + PersistentCollection collection, + Object id, + String entityName, + Object owner) { eventListenerGroups.eventListenerGroup_POST_COLLECTION_RECREATE.fireLazyEventOnEachListener( - () -> new PostCollectionRecreateEvent( collection, id, entityName, owner ), + () -> new PostCollectionRecreateEvent( collectionPersister, collection, id, entityName, owner ), PostCollectionRecreateEventListener::onPostRecreateCollection ); } // Hibernate Reactive may need to call this - protected void firePostUpdate(PersistentCollection collection, Object id, String entityName, Object owner) { + protected void firePostUpdate( + CollectionPersister collectionPersister, + PersistentCollection collection, + Object id, + String entityName, + Object owner) { eventListenerGroups.eventListenerGroup_POST_COLLECTION_UPDATE.fireLazyEventOnEachListener( - () -> new PostCollectionUpdateEvent( collection, id, entityName, owner ), + () -> new PostCollectionUpdateEvent( collectionPersister, collection, id, entityName, owner ), PostCollectionUpdateEventListener::onPostUpdateCollection ); } // Hibernate Reactive may need to call this - protected void firePostRemove(PersistentCollection collection, Object id, String entityName, Object owner) { + protected void firePostRemove( + CollectionPersister collectionPersister, + PersistentCollection collection, + Object id, + String entityName, + Object owner) { eventListenerGroups.eventListenerGroup_POST_COLLECTION_REMOVE.fireLazyEventOnEachListener( - () -> new PostCollectionRemoveEvent( collection, id, entityName, owner ), + () -> new PostCollectionRemoveEvent( collectionPersister, collection, id, entityName, owner ), PostCollectionRemoveEventListener::onPostRemoveCollection ); } @@ -961,6 +995,8 @@ public void refresh(String entityName, Object entity, LockMode lockMode) { final Object result = getLoadQueryInfluencers() + // TODO: This is wrong. StatelessSession is not supposed + // to be affected by cascade = REFRESH. Fix in H8. .fromInternalFetchProfile( CascadingFetchProfile.REFRESH, () -> persister.load( id, entity, getNullSafeLockMode( lockMode ), this ) ); UnresolvableObjectException.throwIfNull( result, id, persister.getEntityName() ); @@ -1032,25 +1068,26 @@ public Object internalLoad( final var persister = requireEntityPersister( entityName ); final var entityKey = generateEntityKey( id, persister ); - // first, try to load it from the temp PC associated to this SS + // First, try to load it from the temporary PersistenceContext final var persistenceContext = getPersistenceContext(); final var holder = persistenceContext.getEntityHolder( entityKey ); if ( holder != null && holder.getEntity() != null ) { - // we found it in the temp PC. Should indicate we are in the midst of processing a result set - // containing eager fetches via join fetch + // We found it in the temporary persistence context. + // Should indicate we are in the midst of processing a + // result set containing eager fetches via join fetch. return holder.getEntity(); } if ( !eager ) { - // caller did not request forceful eager loading, see if we can create - // some form of proxy + // The caller did not request forceful eager loading; + // see if we can create some form of proxy. // first, check to see if we can use "bytecode proxies" - final var enhancementMetadata = persister.getBytecodeEnhancementMetadata(); if ( enhancementMetadata.isEnhancedForLazyLoading() ) { - // if the entity defines a HibernateProxy factory, see if there is an - // existing proxy associated with the PC - and if so, use it + // If the entity defines a HibernateProxy factory, + // see if there is an existing proxy associated with + // the persistence context - and if so, use it if ( persister.getRepresentationStrategy().getProxyFactory() != null ) { final Object proxy = holder == null ? null : holder.getProxy(); @@ -1063,9 +1100,11 @@ public Object internalLoad( return persistenceContext.narrowProxy( proxy, persister, entityKey, null ); } - // specialized handling for entities with subclasses with a HibernateProxy factory + // Specialized handling for entities with subclasses with + // a HibernateProxy factory. if ( persister.hasSubclasses() ) { - // entities with subclasses that define a ProxyFactory can create a HibernateProxy. + // Entities with subclasses that define a ProxyFactory + // can create a HibernateProxy. SESSION_LOGGER.creatingHibernateProxyToHonorLaziness(); return createProxy( entityKey ); } @@ -1074,8 +1113,8 @@ public Object internalLoad( else if ( !persister.hasSubclasses() ) { return enhancementMetadata.createEnhancedProxy( entityKey, false, this ); } - // If we get here, then the entity class has subclasses and there is no HibernateProxy factory. - // The entity will get loaded below. + // If we get here, then the entity class has subclasses and there + // is no HibernateProxy factory. The entity will be loaded below. } else { if ( persister.hasProxy() ) { @@ -1087,14 +1126,14 @@ else if ( !persister.hasSubclasses() ) { } } - // otherwise immediately materialize it + // Otherwise, immediately materialize it. return internalLoadGet( entityName, id, persistenceContext ); } // For Hibernate Reactive protected Object internalLoadGet(String entityName, Object id, PersistenceContext persistenceContext) { - // IMPLEMENTATION NOTE: increment/decrement the load count before/after getting the value - // to ensure that #get does not clear the PersistenceContext. + // Increment/decrement the load count before/after getting the value + // to ensure that #get does not clear the PersistenceContext. persistenceContext.beforeLoad(); try { return get( entityName, id ); @@ -1353,6 +1392,7 @@ public boolean autoFlushIfRequired(Set querySpaces, boolean skipPreFlush @Override public void afterTransactionBegin() { + super.afterTransactionBegin(); afterTransactionBeginEvents(); } @@ -1367,6 +1407,7 @@ public void beforeTransactionCompletion() { public void afterTransactionCompletion(boolean successful, boolean delayed) { transactionCompletionCallbacks.afterTransactionCompletion( successful ); afterTransactionCompletionEvents( successful ); + clearTransactionStartInstant(); if ( shouldAutoClose() && !isClosed() ) { managedClose(); } diff --git a/hibernate-core/src/main/java/org/hibernate/internal/find/FindByKeyOperation.java b/hibernate-core/src/main/java/org/hibernate/internal/find/FindByKeyOperation.java new file mode 100644 index 000000000000..20ec4286566e --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/internal/find/FindByKeyOperation.java @@ -0,0 +1,344 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.internal.find; + +import jakarta.persistence.CacheRetrieveMode; +import jakarta.persistence.CacheStoreMode; +import jakarta.persistence.FindOption; +import jakarta.persistence.LockModeType; +import jakarta.persistence.PessimisticLockScope; +import jakarta.persistence.Timeout; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.hibernate.CacheMode; +import org.hibernate.EnabledFetchProfile; +import org.hibernate.KeyType; +import org.hibernate.FindMultipleOption; +import org.hibernate.LockMode; +import org.hibernate.LockOptions; +import org.hibernate.Locking; +import org.hibernate.NaturalIdSynchronization; +import org.hibernate.ObjectNotFoundException; +import org.hibernate.ReadOnlyMode; +import org.hibernate.Timeouts; +import org.hibernate.bytecode.enhance.spi.interceptor.EnhancementAsProxyLazinessInterceptor; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SessionImplementor; +import org.hibernate.engine.spi.Status; +import org.hibernate.event.spi.LoadEventListener; +import org.hibernate.graph.GraphSemantic; +import org.hibernate.graph.spi.RootGraphImplementor; +import org.hibernate.jpa.internal.util.LockModeTypeHelper; +import org.hibernate.loader.ast.spi.NaturalIdLoader; +import org.hibernate.loader.internal.LoadAccessContext; +import org.hibernate.metamodel.mapping.NaturalIdMapping; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.proxy.HibernateProxy; + +import java.util.HashSet; +import java.util.Set; +import java.util.function.Supplier; + +import static org.hibernate.Timeouts.WAIT_FOREVER; +import static org.hibernate.engine.spi.NaturalIdResolutions.INVALID_NATURAL_ID_REFERENCE; +import static org.hibernate.internal.NaturalIdHelper.performAnyNeededCrossReferenceSynchronizations; +import static org.hibernate.jpa.SpecHints.HINT_SPEC_LOCK_TIMEOUT; +import static org.hibernate.proxy.HibernateProxy.extractLazyInitializer; + +/// Support for loading a single entity by key (either [id][KeyType#IDENTIFIER] or [natural-id][KeyType#NATURAL]). +/// +/// @see org.hibernate.Session#find +/// @see KeyType +/// +/// @author Steve Ebersole +public class FindByKeyOperation implements NaturalIdLoader.Options { + private final EntityPersister entityDescriptor; + + private KeyType keyType = KeyType.IDENTIFIER; + + private CacheStoreMode cacheStoreMode; + private CacheRetrieveMode cacheRetrieveMode; + + private LockMode lockMode; + private Locking.Scope lockScope; + private Locking.FollowOn lockFollowOn; + private Timeout lockTimeout = WAIT_FOREVER; + + private ReadOnlyMode readOnlyMode; + + private RootGraphImplementor rootGraph; + private GraphSemantic graphSemantic; + + private Set enabledFetchProfiles; + + private NaturalIdSynchronization naturalIdSynchronization; + + public FindByKeyOperation( + @NonNull EntityPersister entityDescriptor, + @Nullable GraphSemantic graphSemantic, + @Nullable RootGraphImplementor rootGraph, + @Nullable LockOptions defaultLockOptions, + @Nullable CacheMode defaultCacheMode, + boolean defaultReadOnly, + @NonNull SessionFactoryImplementor sessionFactory, + FindOption... findOptions) { + this.entityDescriptor = entityDescriptor; + + this.graphSemantic = graphSemantic; + this.rootGraph = rootGraph; + + if ( defaultCacheMode != null ) { + cacheStoreMode = defaultCacheMode.getJpaStoreMode(); + cacheRetrieveMode = defaultCacheMode.getJpaRetrieveMode(); + } + + if ( defaultLockOptions != null ) { + lockMode = defaultLockOptions.getLockMode(); + lockScope = defaultLockOptions.getScope(); + lockTimeout = defaultLockOptions.getTimeout(); + lockFollowOn = defaultLockOptions.getFollowOnStrategy(); + } + if ( lockTimeout == WAIT_FOREVER ) { + final Object factoryTimeoutHint = sessionFactory.getProperties().get( HINT_SPEC_LOCK_TIMEOUT ); + if ( factoryTimeoutHint != null ) { + lockTimeout = Timeouts.fromHintTimeout( factoryTimeoutHint ); + } + } + + readOnlyMode = defaultReadOnly ? ReadOnlyMode.READ_ONLY : ReadOnlyMode.READ_WRITE; + + for ( FindOption option : findOptions ) { + if ( option instanceof KeyType keyType ) { + this.keyType = keyType; + } + else if ( option instanceof CacheStoreMode cacheStoreMode ) { + this.cacheStoreMode = cacheStoreMode; + } + else if ( option instanceof CacheRetrieveMode cacheRetrieveMode ) { + this.cacheRetrieveMode = cacheRetrieveMode; + } + else if ( option instanceof CacheMode cacheMode ) { + this.cacheStoreMode = cacheMode.getJpaStoreMode(); + this.cacheRetrieveMode = cacheMode.getJpaRetrieveMode(); + } + else if ( option instanceof LockModeType lockModeType ) { + this.lockMode = LockModeTypeHelper.getLockMode( lockModeType ); + } + else if ( option instanceof LockMode lockMode ) { + this.lockMode = lockMode; + } + else if ( option instanceof Locking.Scope lockScope ) { + this.lockScope = lockScope; + } + else if ( option instanceof PessimisticLockScope pessimisticLockScope ) { + this.lockScope = Locking.Scope.fromJpaScope( pessimisticLockScope ); + } + else if ( option instanceof Locking.FollowOn followOn ) { + this.lockFollowOn = followOn; + } + else if ( option instanceof Timeout timeout ) { + this.lockTimeout = timeout; + } + else if ( option instanceof ReadOnlyMode readOnlyMode) { + this.readOnlyMode = readOnlyMode; + } + else if ( option instanceof EnabledFetchProfile enabledFetchProfile ) { + this.enabledFetchProfile( enabledFetchProfile.profileName() ); + } + else if ( option instanceof GraphSemantic semantic ) { + if ( rootGraph == null ) { + throw new IllegalArgumentException( "GraphSemantic option was specified, but no Graph was supplied" ); + } + this.graphSemantic = semantic; + } + else if ( option instanceof NaturalIdSynchronization naturalIdSynchronization ) { + this.naturalIdSynchronization = naturalIdSynchronization; + } + else if ( option instanceof FindMultipleOption findMultipleOption ) { + throw new IllegalArgumentException( "Option '" + findMultipleOption + "' can only be used in 'findMultiple()'" ); + } + } + } + + private void enabledFetchProfile(String profileName) { + if ( enabledFetchProfiles == null ) { + enabledFetchProfiles = new HashSet<>(); + } + enabledFetchProfiles.add( profileName ); + } + + public T performFind(Object key, LoadAccessContext loadAccessContext) { + if ( keyType == KeyType.NATURAL ) { + return findByNaturalId( key, loadAccessContext ); + } + else { + return findById( key, loadAccessContext ); + } + + } + + private T findByNaturalId(Object key, LoadAccessContext loadAccessContext) { + final NaturalIdMapping naturalIdMapping = entityDescriptor.requireNaturalIdMapping(); + final SessionImplementor session = loadAccessContext.getSession(); + + performAnyNeededCrossReferenceSynchronizations( + naturalIdSynchronization != NaturalIdSynchronization.DISABLED, + entityDescriptor, + session + ); + + final var normalizedKey = naturalIdMapping.normalizeInput( key ); + + final Object cachedResolution = getCachedNaturalIdResolution( normalizedKey, loadAccessContext ); + if ( cachedResolution == INVALID_NATURAL_ID_REFERENCE ) { + return null; + } + + if ( cachedResolution != null ) { + return findById( cachedResolution, loadAccessContext ); + } + + return withOptions( loadAccessContext, () -> { + @SuppressWarnings("unchecked") + final T loaded = (T) entityDescriptor.getNaturalIdLoader() + .load( normalizedKey, this, session ); + if ( loaded != null ) { + final var persistenceContext = session.getPersistenceContextInternal(); + final var lazyInitializer = HibernateProxy.extractLazyInitializer( loaded ); + final var entity = lazyInitializer != null ? lazyInitializer.getImplementation() : loaded; + final var entry = persistenceContext.getEntry( entity ); + assert entry != null; + if ( entry.getStatus() == Status.DELETED ) { + return null; + } + } + return loaded; + } ); + } + + private T withOptions(LoadAccessContext loadAccessContext, Supplier action) { + final var session = loadAccessContext.getSession(); + final var influencers = session.getLoadQueryInfluencers(); + final var fetchProfiles = influencers.adjustFetchProfiles( null, enabledFetchProfiles ); + final var effectiveEntityGraph = rootGraph == null + ? null + : influencers.applyEntityGraph( rootGraph, graphSemantic ); + + final var readOnly = session.isDefaultReadOnly(); + session.setDefaultReadOnly( readOnlyMode == ReadOnlyMode.READ_ONLY ); + + final var cacheMode = session.getCacheMode(); + session.setCacheMode( CacheMode.fromJpaModes( cacheRetrieveMode, cacheStoreMode ) ); + + try { + return action.get(); + } + finally { + loadAccessContext.delayedAfterCompletion(); + if ( effectiveEntityGraph != null ) { + effectiveEntityGraph.clear(); + } + influencers.setEnabledFetchProfileNames( fetchProfiles ); + session.setDefaultReadOnly( readOnly ); + session.setCacheMode( cacheMode ); + } + } + + private Object getCachedNaturalIdResolution( + Object normalizedNaturalIdValue, + LoadAccessContext loadAccessContext) { + loadAccessContext.checkOpenOrWaitingForAutoClose(); + loadAccessContext.pulseTransactionCoordinator(); + + return loadAccessContext + .getSession() + .getPersistenceContextInternal() + .getNaturalIdResolutions() + .findCachedIdByNaturalId( normalizedNaturalIdValue, entityDescriptor ); + } + + private T findById(Object key, LoadAccessContext loadAccessContext) { + return withOptions( loadAccessContext, () -> { + final var session = loadAccessContext.getSession(); + Object result; + try { + result = loadAccessContext.load( + LoadEventListener.GET, + coerceId( key, session.getFactory() ), + entityDescriptor.getEntityName(), + makeLockOptions(), + readOnlyMode == ReadOnlyMode.READ_ONLY + ); + } + catch (ObjectNotFoundException notFoundException) { + // if session cache contains proxy for nonexisting object + result = null; + } + initializeIfNecessary( result ); + //noinspection unchecked + return (T) result; + } ); + } + + private LockOptions makeLockOptions() { + return Helper.makeLockOptions( lockMode, lockScope, lockTimeout, lockFollowOn ); + } + + // Used by Hibernate Reactive + protected Object coerceId(Object id, SessionFactoryImplementor factory) { + if ( factory.getSessionFactoryOptions().getJpaCompliance().isLoadByIdComplianceEnabled() ) { + return id; + } + + try { + return entityDescriptor.getIdentifierMapping().getJavaType().coerce( id ); + } + catch ( Exception e ) { + throw new IllegalArgumentException( "Argument '" + id + + "' could not be converted to the identifier type of entity '" + + entityDescriptor.getEntityName() + "'" + + " [" + e.getMessage() + "]", e ); + } + } + + private void initializeIfNecessary(Object result) { + if ( result != null ) { + final var lazyInitializer = extractLazyInitializer( result ); + if ( lazyInitializer != null ) { + if ( lazyInitializer.isUninitialized() ) { + lazyInitializer.initialize(); + } + } + else { + final var enhancementMetadata = entityDescriptor.getBytecodeEnhancementMetadata(); + if ( enhancementMetadata.isEnhancedForLazyLoading() + && enhancementMetadata.extractLazyInterceptor( result ) + instanceof EnhancementAsProxyLazinessInterceptor lazinessInterceptor ) { + lazinessInterceptor.forceInitialize( result, null ); + } + } + } + } + + @Override + public LockMode getLockMode() { + return lockMode; + } + + @Override + public Timeout getLockTimeout() { + return lockTimeout; + } + + @Override + public Locking.Scope getLockScope() { + return lockScope; + } + + @Override + public Locking.FollowOn getLockFollowOn() { + return lockFollowOn; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/internal/find/FindMultipleByKeyOperation.java b/hibernate-core/src/main/java/org/hibernate/internal/find/FindMultipleByKeyOperation.java new file mode 100644 index 000000000000..76c073b66d8a --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/internal/find/FindMultipleByKeyOperation.java @@ -0,0 +1,344 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.internal.find; + +import jakarta.persistence.CacheRetrieveMode; +import jakarta.persistence.CacheStoreMode; +import jakarta.persistence.FindOption; +import jakarta.persistence.LockModeType; +import jakarta.persistence.PessimisticLockScope; +import jakarta.persistence.Timeout; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.hibernate.BatchSize; +import org.hibernate.CacheMode; +import org.hibernate.EnabledFetchProfile; +import org.hibernate.KeyType; +import org.hibernate.LockMode; +import org.hibernate.LockOptions; +import org.hibernate.Locking; +import org.hibernate.NaturalIdSynchronization; +import org.hibernate.OrderingMode; +import org.hibernate.ReadOnlyMode; +import org.hibernate.RemovalsMode; +import org.hibernate.SessionCheckMode; +import org.hibernate.Timeouts; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SessionImplementor; +import org.hibernate.graph.GraphSemantic; +import org.hibernate.graph.spi.RootGraphImplementor; +import org.hibernate.jpa.internal.util.LockModeTypeHelper; +import org.hibernate.loader.ast.spi.MultiIdLoadOptions; +import org.hibernate.loader.ast.spi.MultiNaturalIdLoadOptions; +import org.hibernate.loader.internal.LoadAccessContext; +import org.hibernate.persister.entity.EntityPersister; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Supplier; + +import static org.hibernate.Timeouts.WAIT_FOREVER; +import static org.hibernate.internal.NaturalIdHelper.performAnyNeededCrossReferenceSynchronizations; +import static org.hibernate.jpa.SpecHints.HINT_SPEC_LOCK_TIMEOUT; + +/// Support for loading multiple entities (of a type) by key (either [id][KeyType#IDENTIFIER] or [natural-id][KeyType#NATURAL]). +/// +/// @see org.hibernate.Session#findMultiple +/// @see KeyType +/// +/// @author Steve Ebersole +public class FindMultipleByKeyOperation implements MultiIdLoadOptions, MultiNaturalIdLoadOptions { + private final EntityPersister entityDescriptor; + + private KeyType keyType = KeyType.IDENTIFIER; + + private BatchSize batchSize; + private SessionCheckMode sessionCheckMode = SessionCheckMode.DISABLED; + private RemovalsMode removalsMode = RemovalsMode.REPLACE; + private OrderingMode orderingMode = OrderingMode.ORDERED; + + private CacheStoreMode cacheStoreMode; + private CacheRetrieveMode cacheRetrieveMode; + + private LockMode lockMode; + private Locking.Scope lockScope; + private Locking.FollowOn lockFollowOn; + private Timeout lockTimeout = WAIT_FOREVER; + + private ReadOnlyMode readOnlyMode; + + private Set enabledFetchProfiles; + private Set disabledFetchProfiles; + + private NaturalIdSynchronization naturalIdSynchronization; + + @SuppressWarnings("PatternVariableHidesField") + public FindMultipleByKeyOperation( + @NonNull EntityPersister entityDescriptor, + @Nullable LockOptions defaultLockOptions, + @Nullable CacheMode defaultCacheMode, + boolean defaultReadOnly, + @NonNull SessionFactoryImplementor sessionFactory, + FindOption... findOptions) { + this.entityDescriptor = entityDescriptor; + + if ( defaultCacheMode != null ) { + cacheStoreMode = defaultCacheMode.getJpaStoreMode(); + cacheRetrieveMode = defaultCacheMode.getJpaRetrieveMode(); + } + + if ( defaultLockOptions != null ) { + lockMode = defaultLockOptions.getLockMode(); + lockScope = defaultLockOptions.getScope(); + lockTimeout = defaultLockOptions.getTimeout(); + lockFollowOn = defaultLockOptions.getFollowOnStrategy(); + } + if ( lockTimeout == WAIT_FOREVER ) { + final Object factoryTimeoutHint = sessionFactory.getProperties().get( HINT_SPEC_LOCK_TIMEOUT ); + if ( factoryTimeoutHint != null ) { + lockTimeout = Timeouts.fromHintTimeout( factoryTimeoutHint ); + } + } + + readOnlyMode = defaultReadOnly ? ReadOnlyMode.READ_ONLY : ReadOnlyMode.READ_WRITE; + + for ( FindOption option : findOptions ) { + if ( option instanceof KeyType keyType ) { + this.keyType = keyType; + } + else if ( option instanceof BatchSize batchSize ) { + this.batchSize = batchSize; + } + else if ( option instanceof SessionCheckMode sessionCheckMode ) { + this.sessionCheckMode = sessionCheckMode; + } + else if ( option instanceof RemovalsMode removalsMode ) { + this.removalsMode = removalsMode; + } + else if ( option instanceof OrderingMode orderingMode ) { + this.orderingMode = orderingMode; + } + else if ( option instanceof CacheStoreMode cacheStoreMode ) { + this.cacheStoreMode = cacheStoreMode; + } + else if ( option instanceof CacheRetrieveMode cacheRetrieveMode ) { + this.cacheRetrieveMode = cacheRetrieveMode; + } + else if ( option instanceof CacheMode cacheMode ) { + this.cacheStoreMode = cacheMode.getJpaStoreMode(); + this.cacheRetrieveMode = cacheMode.getJpaRetrieveMode(); + } + else if ( option instanceof LockModeType lockModeType ) { + this.lockMode = LockModeTypeHelper.getLockMode( lockModeType ); + } + else if ( option instanceof LockMode lockMode ) { + this.lockMode = lockMode; + } + else if ( option instanceof Locking.Scope lockScope ) { + this.lockScope = lockScope; + } + else if ( option instanceof PessimisticLockScope pessimisticLockScope ) { + this.lockScope = Locking.Scope.fromJpaScope( pessimisticLockScope ); + } + else if ( option instanceof Locking.FollowOn followOn ) { + this.lockFollowOn = followOn; + } + else if ( option instanceof Timeout timeout ) { + this.lockTimeout = timeout; + } + else if ( option instanceof ReadOnlyMode readOnlyMode) { + this.readOnlyMode = readOnlyMode; + } + else if ( option instanceof EnabledFetchProfile enabledFetchProfile ) { + this.enabledFetchProfile( enabledFetchProfile.profileName() ); + } + else if ( option instanceof NaturalIdSynchronization naturalIdSynchronization ) { + this.naturalIdSynchronization = naturalIdSynchronization; + } + } + } + + private void enabledFetchProfile(String profileName) { + if ( enabledFetchProfiles == null ) { + enabledFetchProfiles = new HashSet<>(); + } + enabledFetchProfiles.add( profileName ); + } + + public List performFind( + List keys, + @Nullable GraphSemantic graphSemantic, + @Nullable RootGraphImplementor rootGraph, + LoadAccessContext loadAccessContext) { + // todo (natural-id-class) : these impls are temporary + // longer term, move the logic here as much of it can be shared + return keyType == KeyType.NATURAL + ? findByNaturalIds( keys, graphSemantic, rootGraph, loadAccessContext ) + : findByIds( keys, graphSemantic, rootGraph, loadAccessContext ); + } + + private List findByNaturalIds(List keys, GraphSemantic graphSemantic, RootGraphImplementor rootGraph, LoadAccessContext loadAccessContext) { + final var naturalIdMapping = entityDescriptor.requireNaturalIdMapping(); + final var session = loadAccessContext.getSession(); + + performAnyNeededCrossReferenceSynchronizations( + naturalIdSynchronization != NaturalIdSynchronization.DISABLED, + entityDescriptor, + session + ); + + return withOptions( loadAccessContext, graphSemantic, rootGraph, () -> { + // normalize the incoming natural-id values and get them in array form as needed + // by MultiNaturalIdLoader + final int size = keys.size(); + final var naturalIds = new Object[size]; + for ( int i = 0; i < size; i++ ) { + naturalIds[i] = naturalIdMapping.normalizeInput( keys.get( i ) ); + } + //noinspection unchecked + return (List) + entityDescriptor.getMultiNaturalIdLoader() + .multiLoad( naturalIds, this, session ); + } ); + } + + private List withOptions( + LoadAccessContext loadAccessContext, + GraphSemantic graphSemantic, + RootGraphImplementor rootGraph, + Supplier> action) { + final var session = loadAccessContext.getSession(); + final var influencers = session.getLoadQueryInfluencers(); + final var fetchProfiles = + influencers.adjustFetchProfiles( disabledFetchProfiles, enabledFetchProfiles ); + final var effectiveEntityGraph = + rootGraph == null + ? null + : influencers.applyEntityGraph( rootGraph, graphSemantic ); + + final var readOnly = session.isDefaultReadOnly(); + session.setDefaultReadOnly( readOnlyMode == ReadOnlyMode.READ_ONLY ); + + final var cacheMode = session.getCacheMode(); + session.setCacheMode( CacheMode.fromJpaModes( cacheRetrieveMode, cacheStoreMode ) ); + + try { + return action.get(); + } + finally { + loadAccessContext.delayedAfterCompletion(); + if ( effectiveEntityGraph != null ) { + effectiveEntityGraph.clear(); + } + influencers.setEnabledFetchProfileNames( fetchProfiles ); + session.setDefaultReadOnly( readOnly ); + session.setCacheMode( cacheMode ); + } + } + +// private Object getCachedNaturalIdResolution( +// Object normalizedNaturalIdValue, +// LoadAccessContext loadAccessContext) { +// loadAccessContext.checkOpenOrWaitingForAutoClose(); +// loadAccessContext.pulseTransactionCoordinator(); +// +// return loadAccessContext +// .getSession() +// .getPersistenceContextInternal() +// .getNaturalIdResolutions() +// .findCachedIdByNaturalId( normalizedNaturalIdValue, entityDescriptor ); +// } + + private List findByIds(List keys, GraphSemantic graphSemantic, RootGraphImplementor rootGraph, LoadAccessContext loadAccessContext) { + final var ids = keys.toArray( new Object[0] ); + //noinspection unchecked + return withOptions( loadAccessContext, graphSemantic, rootGraph, + () -> (List) entityDescriptor.multiLoad( ids, loadAccessContext.getSession(), this ) ); + } + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // MultiIdLoadOptions & MultiNaturalIdLoadOptions + + @Override + public SessionCheckMode getSessionCheckMode() { + return sessionCheckMode; + } + + @Override + public boolean isSecondLevelCacheCheckingEnabled() { + return cacheRetrieveMode == CacheRetrieveMode.USE; + } + + @Override + public Boolean getReadOnly(SessionImplementor session) { + return readOnlyMode == null ? null : readOnlyMode == ReadOnlyMode.READ_ONLY; + } + + @Override + public RemovalsMode getRemovalsMode() { + return removalsMode; + } + + @Override + public OrderingMode getOrderingMode() { + return orderingMode; + } + + @Override + public LockOptions getLockOptions() { + return Helper.makeLockOptions( lockMode, lockScope, lockTimeout, lockFollowOn ); + } + + @Override + public Integer getBatchSize() { + return batchSize == null ? null : batchSize.batchSize(); + } + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + /// Temporarily defined full constructor in support of + /// [org.hibernate.MultiIdentifierLoadAccess] and [org.hibernate.MultiIdentifierLoadAccess]. + /// + /// @deprecated [org.hibernate.MultiIdentifierLoadAccess] and [org.hibernate.MultiIdentifierLoadAccess] + /// are both also deprecated. + @Deprecated + public FindMultipleByKeyOperation( + EntityPersister entityDescriptor, + KeyType keyType, + BatchSize batchSize, + SessionCheckMode sessionCheckMode, + RemovalsMode removalsMode, + OrderingMode orderingMode, + CacheMode cacheMode, + LockOptions lockOptions, + ReadOnlyMode readOnlyMode, + Set enabledFetchProfiles, + Set disabledFetchProfiles, + NaturalIdSynchronization naturalIdSynchronization) { + if ( cacheMode == null ) { + cacheMode = CacheMode.NORMAL; + } + if ( lockOptions == null ) { + lockOptions = LockOptions.NONE; + } + this.entityDescriptor = entityDescriptor; + this.keyType = keyType; + this.batchSize = batchSize; + this.sessionCheckMode = sessionCheckMode; + this.removalsMode = removalsMode; + this.orderingMode = orderingMode; + this.cacheStoreMode = cacheMode.getJpaStoreMode(); + this.cacheRetrieveMode = cacheMode.getJpaRetrieveMode(); + this.lockMode = lockOptions.getLockMode(); + this.lockScope = lockOptions.getScope(); + this.lockFollowOn = lockOptions.getFollowOnStrategy(); + this.lockTimeout = lockOptions.getTimeout(); + this.readOnlyMode = readOnlyMode; + this.enabledFetchProfiles = enabledFetchProfiles; + this.disabledFetchProfiles = disabledFetchProfiles; + this.naturalIdSynchronization = naturalIdSynchronization; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/internal/find/Helper.java b/hibernate-core/src/main/java/org/hibernate/internal/find/Helper.java new file mode 100644 index 000000000000..7a43743efbf5 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/internal/find/Helper.java @@ -0,0 +1,31 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.internal.find; + +import jakarta.persistence.Timeout; +import org.hibernate.LockMode; +import org.hibernate.LockOptions; +import org.hibernate.Locking; +import org.hibernate.Timeouts; + +/** + * @author Steve Ebersole + */ +public class Helper { + public static LockOptions makeLockOptions(LockMode lockMode, Locking.Scope lockScope, Timeout lockTimeout, Locking.FollowOn lockFollowOn) { + if ( lockMode == null || lockMode == LockMode.NONE ) { + return LockOptions.NONE; + } + if ( lockMode == LockMode.READ ) { + return LockOptions.READ; + } + + final var lockOptions = new LockOptions( lockMode ); + lockOptions.setScope( lockScope != null ? lockScope : Locking.Scope.ROOT_ONLY ); + lockOptions.setTimeout( lockTimeout != null ? lockTimeout : Timeouts.WAIT_FOREVER ); + lockOptions.setFollowOnStrategy( lockFollowOn != null ? lockFollowOn : Locking.FollowOn.ALLOW ); + return lockOptions; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/internal/log/ConnectionAccessLogger.java b/hibernate-core/src/main/java/org/hibernate/internal/log/ConnectionAccessLogger.java index ee17c0cd9c45..e44fac29867a 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/log/ConnectionAccessLogger.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/log/ConnectionAccessLogger.java @@ -14,6 +14,7 @@ import org.jboss.logging.annotations.ValidIdRange; import java.lang.invoke.MethodHandles; +import java.util.Locale; import static org.jboss.logging.Logger.Level.TRACE; @@ -36,7 +37,8 @@ public interface ConnectionAccessLogger extends BasicLogger { ConnectionAccessLogger INSTANCE = Logger.getMessageLogger( MethodHandles.lookup(), ConnectionAccessLogger.class, - LOGGER_NAME + LOGGER_NAME, + Locale.ROOT ); diff --git a/hibernate-core/src/main/java/org/hibernate/internal/log/ConnectionInfoLogger.java b/hibernate-core/src/main/java/org/hibernate/internal/log/ConnectionInfoLogger.java index 441dca267c0f..79ed53876bfd 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/log/ConnectionInfoLogger.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/log/ConnectionInfoLogger.java @@ -6,6 +6,7 @@ import java.lang.invoke.MethodHandles; import java.sql.SQLException; +import java.util.Locale; import org.hibernate.Internal; import org.hibernate.cfg.JdbcSettings; @@ -38,7 +39,7 @@ public interface ConnectionInfoLogger extends BasicLogger { /** * Static access to the logging instance */ - ConnectionInfoLogger CONNECTION_INFO_LOGGER = Logger.getMessageLogger( MethodHandles.lookup(), ConnectionInfoLogger.class, LOGGER_NAME ); + ConnectionInfoLogger CONNECTION_INFO_LOGGER = Logger.getMessageLogger( MethodHandles.lookup(), ConnectionInfoLogger.class, LOGGER_NAME, Locale.ROOT ); @LogMessage(level = WARN) @Message(value = "Using built-in connection pool (not intended for production use)", id = 10001002) @@ -123,4 +124,8 @@ public interface ConnectionInfoLogger extends BasicLogger { @LogMessage(level = ERROR) @Message(value = "Connection leak detected: there are %s unclosed connections", id = 10001023) void connectionLeakDetected(int allocationCount); + + @LogMessage(level = WARN) + @Message(value = "Could not set login timeout", id = 10001024) + void couldNotSetLoginTimeout(@Cause SQLException e); } diff --git a/hibernate-core/src/main/java/org/hibernate/internal/log/DeprecationLogger.java b/hibernate-core/src/main/java/org/hibernate/internal/log/DeprecationLogger.java index e097de473ad6..a95f19ad10f9 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/log/DeprecationLogger.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/log/DeprecationLogger.java @@ -7,6 +7,7 @@ import java.lang.annotation.Annotation; import java.lang.invoke.MethodHandles; import java.util.List; +import java.util.Locale; import org.hibernate.Internal; import org.hibernate.boot.jaxb.SourceType; @@ -19,6 +20,7 @@ import org.jboss.logging.annotations.MessageLogger; import org.jboss.logging.annotations.ValidIdRange; +import static org.jboss.logging.Logger.Level.DEBUG; import static org.jboss.logging.Logger.Level.WARN; /** @@ -36,7 +38,7 @@ public interface DeprecationLogger extends BasicLogger { String CATEGORY = SubSystemLogging.BASE + ".deprecation"; - DeprecationLogger DEPRECATION_LOGGER = Logger.getMessageLogger( MethodHandles.lookup(), DeprecationLogger.class, CATEGORY ); + DeprecationLogger DEPRECATION_LOGGER = Logger.getMessageLogger( MethodHandles.lookup(), DeprecationLogger.class, CATEGORY, Locale.ROOT ); @LogMessage(level = WARN) @Message( @@ -248,4 +250,33 @@ void recognizedObsoleteHibernateNamespace( value = "DEPRECATED: use [%s] instead with custom [%s] implementation" ) void deprecatedUuidGenerator(String name, String name2); + + @LogMessage(level = WARN) + @Message( + id = 90000041, + value = "Marking named native queries as callable is deprecated; use instead" + ) + void callableNamedNativeQuery(); + + @LogMessage(level = WARN) + @Message( + id = 90000042, + value = "Implicit/explicit polymorphism no longer supported" + ) + void explicitPolymorphism(); + + @LogMessage(level = DEBUG) + @Message( + id = 90000043, + value = "Custom CollectionPersister implementations are no longer supported - %s (%s)" + ) + void customCollectionPersister(String role, String name); + + @LogMessage(level = WARN) + @Message( + id = 90000044, + value = "Deprecated syntax when using @NamedEntityGraph: 'Type: attr1, attr2' is deprecated. " + + "Specify the root entity using the 'root' attribute instead of prefixing the graph with the entity type." + ) + void deprecatedNamedEntityGraphTextThatContainTypeIndicator(); } diff --git a/hibernate-core/src/main/java/org/hibernate/internal/log/IncubationLogger.java b/hibernate-core/src/main/java/org/hibernate/internal/log/IncubationLogger.java index 99e2852fe729..535a47fd1d17 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/log/IncubationLogger.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/log/IncubationLogger.java @@ -12,6 +12,7 @@ import org.jboss.logging.annotations.ValidIdRange; import java.lang.invoke.MethodHandles; +import java.util.Locale; import static org.jboss.logging.Logger.Level.INFO; @@ -24,7 +25,7 @@ public interface IncubationLogger { String CATEGORY = SubSystemLogging.BASE + ".incubating"; - IncubationLogger INCUBATION_LOGGER = Logger.getMessageLogger( MethodHandles.lookup(), IncubationLogger.class, CATEGORY ); + IncubationLogger INCUBATION_LOGGER = Logger.getMessageLogger( MethodHandles.lookup(), IncubationLogger.class, CATEGORY, Locale.ROOT ); @LogMessage(level = INFO) @Message( diff --git a/hibernate-core/src/main/java/org/hibernate/internal/log/StatisticsLogger.java b/hibernate-core/src/main/java/org/hibernate/internal/log/StatisticsLogger.java index e7c12ac90ad5..9d15bd818108 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/log/StatisticsLogger.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/log/StatisticsLogger.java @@ -11,6 +11,7 @@ import org.jboss.logging.annotations.MessageLogger; import java.lang.invoke.MethodHandles; +import java.util.Locale; import static org.jboss.logging.Logger.Level.DEBUG; import static org.jboss.logging.Logger.Level.INFO; @@ -24,7 +25,7 @@ public interface StatisticsLogger extends BasicLogger { String LOGGER_NAME = "org.hibernate.statistics"; - StatisticsLogger STATISTICS_LOGGER = Logger.getMessageLogger( MethodHandles.lookup(), StatisticsLogger.class, LOGGER_NAME ); + StatisticsLogger STATISTICS_LOGGER = Logger.getMessageLogger( MethodHandles.lookup(), StatisticsLogger.class, LOGGER_NAME, Locale.ROOT ); @LogMessage(level = TRACE) @Message(value = "Statistics initialized", id = 460) diff --git a/hibernate-core/src/main/java/org/hibernate/internal/log/UrlMessageBundle.java b/hibernate-core/src/main/java/org/hibernate/internal/log/UrlMessageBundle.java index b1e270f329f2..805fb776ec0a 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/log/UrlMessageBundle.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/log/UrlMessageBundle.java @@ -7,6 +7,7 @@ import java.lang.invoke.MethodHandles; import java.net.URISyntaxException; import java.net.URL; +import java.util.Locale; import org.hibernate.Internal; import org.jboss.logging.Logger; @@ -36,7 +37,7 @@ public interface UrlMessageBundle { String LOGGER_NAME = SubSystemLogging.BASE + ".url"; Logger URL_LOGGER = Logger.getLogger( LOGGER_NAME ); - UrlMessageBundle URL_MESSAGE_LOGGER = Logger.getMessageLogger( MethodHandles.lookup(), UrlMessageBundle.class, LOGGER_NAME ); + UrlMessageBundle URL_MESSAGE_LOGGER = Logger.getMessageLogger( MethodHandles.lookup(), UrlMessageBundle.class, LOGGER_NAME, Locale.ROOT ); /** * Logs a warning about a malformed URL, caused by a {@link URISyntaxException} diff --git a/hibernate-core/src/main/java/org/hibernate/internal/util/GenericAssignability.java b/hibernate-core/src/main/java/org/hibernate/internal/util/GenericAssignability.java new file mode 100644 index 000000000000..1da0cb05cce2 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/internal/util/GenericAssignability.java @@ -0,0 +1,204 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.internal.util; + +import java.lang.reflect.*; + +/** + * @author Gavin King + */ +public final class GenericAssignability { + + public static boolean isAssignableFrom(Type to, Type from) { + return isAssignable( from, to ); + } + + public static boolean isAssignable(Type from, Type to) { + if ( from.equals( to ) ) { + return true; + } + + // Wildcards (target side) + if ( to instanceof WildcardType wt ) { + return isAssignableToWildcard( from, wt ); + } + + // Type variables (target side) + if ( to instanceof TypeVariable tv ) { + return isAssignableToTypeVariable( from, tv ); + } + + // Generic arrays + if ( to instanceof GenericArrayType gaTo ) { + return isAssignableToGenericArray( from, gaTo ); + } + + // From-side wildcard + if ( from instanceof WildcardType wf ) { + return isAssignableFromWildcard( wf, to ); + } + + // Parameterized types + if ( from instanceof ParameterizedType pf && + to instanceof ParameterizedType pt ) { + return isAssignableParameterized( pf, pt ); + } + + // Raw class or array class + final var fromRaw = rawClass( from ); + final var toRaw = rawClass( to ); + + if ( fromRaw != null && toRaw != null ) { + return toRaw.isAssignableFrom( fromRaw ) + || isAssignableViaInheritance( from, toRaw ); + } + + return false; + } + + private static boolean isAssignableToGenericArray(Type from, GenericArrayType to) { + final var toComponent = to.getGenericComponentType(); + if ( from instanceof GenericArrayType gaFrom ) { + return isAssignable( gaFrom.getGenericComponentType(), toComponent ); + } + else if ( from instanceof Class c && c.isArray() ) { + return isAssignable( c.getComponentType(), toComponent ); + } + else { + return false; + } + } + + private static boolean isAssignableParameterized(ParameterizedType from, ParameterizedType to) { + + final var fromRaw = (Class) from.getRawType(); + final var toRaw = (Class) to.getRawType(); + + if ( toRaw.isAssignableFrom( fromRaw ) ) { + final var superType = findGenericSuperType( from, toRaw ); + if ( !( superType instanceof ParameterizedType ps ) ) { + return false; + } + else { + final var fromArgs = ps.getActualTypeArguments(); + final var toArgs = to.getActualTypeArguments(); + if ( fromArgs.length != toArgs.length ) { + return false; + } + for ( int i = 0; i < fromArgs.length; i++ ) { + if ( !isTypeArgumentAssignable( fromArgs[i], toArgs[i] ) ) { + return false; + } + } + return true; + } + } + else { + return false; + } + + } + + private static boolean isTypeArgumentAssignable(Type from, Type to) { + return to instanceof WildcardType wt + ? isAssignableToWildcard( from, wt ) + : isAssignable( from, to ); + } + + private static boolean isAssignableViaInheritance(Type from, Class toRaw) { + + final var raw = rawClass( from ); + if ( raw == null ) { + return false; + } + + // Check interfaces + for ( var iface : raw.getGenericInterfaces() ) { + if ( isAssignable( iface, toRaw ) ) { + return true; + } + } + + // Check superclass + final var superclass = raw.getGenericSuperclass(); + if ( superclass != null ) { + return isAssignable( superclass, toRaw ); + } + + return false; + } + + private static Type findGenericSuperType(Type from, Class target) { + final var raw = rawClass( from ); + if ( raw == null ) { + return null; + } + + if ( raw == target ) { + return from; + } + + for ( var iface : raw.getGenericInterfaces() ) { + final var found = findGenericSuperType( iface, target ); + if ( found != null ) { + return found; + } + } + + final var superclass = raw.getGenericSuperclass(); + if ( superclass != null ) { + return findGenericSuperType( superclass, target ); + } + + return null; + } + + private static boolean isAssignableToWildcard(Type from, WildcardType to) { + for ( var lower : to.getLowerBounds() ) { + if ( !isAssignable( lower, from ) ) { + return false; + } + } + for ( var upper : to.getUpperBounds() ) { + if ( !isAssignable( from, upper ) ) { + return false; + } + } + return true; + } + + private static boolean isAssignableFromWildcard(WildcardType from, Type to) { + for ( var upper : from.getUpperBounds() ) { + if ( isAssignable( upper, to ) ) { + return true; + } + } + return false; + } + + private static boolean isAssignableToTypeVariable(Type from, TypeVariable tv) { + for ( var bound : tv.getBounds() ) { + if ( !isAssignable( from, bound ) ) { + return false; + } + } + return true; + } + + private static Class rawClass(Type type) { + if ( type instanceof Class c ) { + return c; + } + else if ( type instanceof ParameterizedType pt ) { + return (Class) pt.getRawType(); + } + else if ( type instanceof GenericArrayType ga ) { + return rawClass( ga.getGenericComponentType() ).arrayType(); + } + else { + return null; + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/internal/util/GenericsHelper.java b/hibernate-core/src/main/java/org/hibernate/internal/util/GenericsHelper.java index da448b313a6e..b66b9f6f6731 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/util/GenericsHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/util/GenericsHelper.java @@ -4,38 +4,213 @@ */ package org.hibernate.internal.util; +import org.hibernate.models.spi.MemberDetails; + +import java.lang.reflect.Field; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.Member; +import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.lang.reflect.TypeVariable; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; +import java.util.HashMap; +import java.util.Map; +import java.util.StringJoiner; + +/** + * @author Gavin King + */ +public final class GenericsHelper { + + /** + * The type of the member inherited by the subclass from the supertype, + * as viewed from within the subclass. + * @param memberDetails The member, represented in the subclass + * @return The type of the member as it would be seen in the subclass + */ + public static Type actualMemberType(MemberDetails memberDetails) { + return actualInheritedMemberType( + memberDetails.getDeclaringType().toJavaClass(), + memberDetails.toJavaMember() + ); + } + + /** + * The type of the member inherited by the subclass from the supertype, + * as viewed from within the subclass. + * @param subclass The inheriting subclass + * @param superMember The member declared in the supertype + * @return The type of the member as it would be seen in the subclass + */ + public static Type actualInheritedMemberType(Class subclass, Member superMember) { + return substituteTypeVariables( getMemberType( superMember ), + collectTypeArguments( subclass, superMember ) ); + } + + private static Map, Type> collectTypeArguments( + Class subclass, Member superMember) { + final var superclass = superMember.getDeclaringClass(); + final var typeArguments = typeArguments( superclass, subclass ); + final var typeParameters = superclass.getTypeParameters(); + final Map, Type> typeMap = + new HashMap<>( typeParameters.length ); + for ( int i = 0; i < typeParameters.length; i++ ) { + typeMap.put( typeParameters[i], typeArguments[i] ); + } + return typeMap; + } + + private static Type getMemberType(Member superMember) { + if ( superMember instanceof Field field ) { + return field.getGenericType(); + } + else if ( superMember instanceof Method method ) { + return method.getGenericReturnType(); + } + else { + throw new IllegalArgumentException( "Unsupported member: " + superMember ); + } + } + + private static Type substituteTypeVariables(Type type, Map, Type> typeMap) { + + if ( type instanceof TypeVariable typeVariable ) { + final var substituted = typeMap.get( typeVariable ); + return substituted == null + ? Object.class + : substituteTypeVariables( substituted, typeMap ); + } + else if ( type instanceof ParameterizedType parameterizedType ) { + final var args = parameterizedType.getActualTypeArguments(); + final var resolved = new Type[args.length]; + for ( int i = 0; i < args.length; i++ ) { + resolved[i] = substituteTypeVariables( args[i], typeMap ); + } + return new SimpleParameterizedType( + (Class) parameterizedType.getRawType(), + resolved, + parameterizedType.getOwnerType() + ); + } + else if ( type instanceof GenericArrayType genericArrayType ) { + final var elementType = + substituteTypeVariables( genericArrayType.getGenericComponentType(), typeMap ); + return new GenericArrayType() { + @Override + public Type getGenericComponentType() { + return elementType; + } + + @Override + public String toString() { + return elementType.getTypeName() + "[]"; + } + }; + } + else { + return type; + } + } + + /** + * The erased type of the given type. + * @param type A type, possibly with type arguments + * @return The erased type + */ + public static Class erasedType(Type type) { + if ( type instanceof Class clazz ) { + return clazz; + } + else if ( type instanceof ParameterizedType parameterizedType ) { + return erasedType( parameterizedType.getRawType() ); + } + else if ( type instanceof TypeVariable typeVariable ) { + return erasedType( typeVariable.getBounds()[0] ); + } + else if ( type instanceof GenericArrayType genericArrayType ) { + return genericArrayType.getGenericComponentType() instanceof Class elementClass + ? elementClass.arrayType() + : Object[].class; + } + else { + throw new IllegalArgumentException( "Cannot erase type: " + type ); + } + } -public class GenericsHelper { + /** + * Get the type argument of the instantiation of the given generic + * type constructor which is a supertype of the given type expression. + * @param genericType A generic type constructor + * @param implementingType A type expression + * @return The type arguments assigned to parameters of the generic type constructor + */ + public static Type[] typeArguments(Class genericType, Type implementingType) { + if ( genericType.getTypeParameters().length == 0 ) { + return EMPTY_TYPE_ARRAY; + } + else { + final var instantiation = + supertypeInstantiation( genericType, implementingType ); + if ( instantiation == null ) { + throw new IllegalArgumentException( + implementingType.getTypeName() + + " is is not a subtype of " + + genericType.getName() ); + } + return instantiation.getActualTypeArguments(); + } + } - public static ParameterizedType extractParameterizedType(Type base, Class genericType) { + private static final Type[] EMPTY_TYPE_ARRAY = new Type[0]; + + /** + * A supertype of the given type which is an instantiation of the + * given generic type constructor. + * @param genericType A generic type constructor + * @param base A type which is assignable to some instantiation of + * the given generic type constructor + * @return An instantiation of the generic type constructor which + * is a supertype of the given type, or null if none exists + */ + public static ParameterizedType supertypeInstantiation(Class genericType, Type base) { if ( base == null ) { return null; } - final Class clazz = extractClass( base ); + final var clazz = erasedType( base ); if ( clazz == null ) { return null; } - final List types = new ArrayList<>(); - types.add( clazz.getGenericSuperclass() ); - types.addAll( Arrays.asList( clazz.getGenericInterfaces() ) ); + if ( clazz == genericType + && base instanceof ParameterizedType result ) { + return result; + } - for ( Type type : types ) { - type = resolveType( type, base ); - if ( type instanceof ParameterizedType parameterizedType ) { - if ( genericType.equals( parameterizedType.getRawType() ) ) { - return parameterizedType; - } + final var superclass = clazz.getGenericSuperclass(); + if ( superclass != null ) { + final var type = substituteTypeArguments( superclass, base ); + if ( type instanceof ParameterizedType parameterizedType + && genericType.equals( parameterizedType.getRawType() ) ) { + return parameterizedType; + } + + final var parameterizedType = + supertypeInstantiation( genericType, type ); + if ( parameterizedType != null ) { + return parameterizedType; + } + } + + for ( var iface : clazz.getGenericInterfaces() ) { + final var type = substituteTypeArguments( iface, base ); + if ( type instanceof ParameterizedType parameterizedType + && genericType.equals( parameterizedType.getRawType() ) ) { + return parameterizedType; } - final ParameterizedType parameterizedType = extractParameterizedType( type, genericType ); + final var parameterizedType = + supertypeInstantiation( genericType, type ); if ( parameterizedType != null ) { return parameterizedType; } @@ -44,66 +219,75 @@ public static ParameterizedType extractParameterizedType(Type base, Class gen return null; } - private static Type resolveTypeVariable(TypeVariable typeVariable, ParameterizedType context) { - final Class clazz = extractClass( context.getRawType() ); + private static Type replaceTypeVariableWithArgument( + TypeVariable typeVariable, ParameterizedType context) { + final var clazz = erasedType( context.getRawType() ); if ( clazz == null ) { return null; } - final TypeVariable[] typeParameters = clazz.getTypeParameters(); + final var typeArguments = context.getActualTypeArguments(); + final var typeParameters = clazz.getTypeParameters(); for ( int idx = 0; idx < typeParameters.length; idx++ ) { if ( typeVariable.getName().equals( typeParameters[idx].getName() ) ) { - return resolveType( context.getActualTypeArguments()[idx], context ); + return substituteTypeArguments( typeArguments[idx], context ); } } return typeVariable; } - public static Class extractClass(Type type) { - if ( type instanceof Class clazz ) { - return clazz; + private static Type substituteTypeArguments(Type target, Type context) { + if ( target instanceof ParameterizedType parameterizedType ) { + return replaceTypeVariablesWithArguments( parameterizedType, context ); } - else if ( type instanceof ParameterizedType parameterizedType ) { - return extractClass( parameterizedType.getRawType() ); + else if ( target instanceof TypeVariable typeVariable + && context instanceof ParameterizedType parameterizedContext ) { + return replaceTypeVariableWithArgument( typeVariable, parameterizedContext ); + } + else { + return target; } - return null; } - private static Type resolveType(Type target, Type context) { - if ( target instanceof ParameterizedType parameterizedType ) { - return resolveParameterizedType( parameterizedType, context ); + private static ParameterizedType replaceTypeVariablesWithArguments( + ParameterizedType parameterizedType, Type context) { + final var typeArguments = parameterizedType.getActualTypeArguments(); + final var resolvedTypeArguments = new Type[typeArguments.length]; + for ( int idx = 0; idx < typeArguments.length; idx++ ) { + resolvedTypeArguments[idx] = substituteTypeArguments( typeArguments[idx], context ); } - else if ( target instanceof TypeVariable typeVariable ) { - return resolveTypeVariable( typeVariable, (ParameterizedType) context ); - } - return target; + return new SimpleParameterizedType( + erasedType( parameterizedType ), + resolvedTypeArguments, + parameterizedType.getOwnerType() + ); } - private static ParameterizedType resolveParameterizedType(final ParameterizedType parameterizedType, Type context) { - final Type[] actualTypeArguments = parameterizedType.getActualTypeArguments(); - - final Type[] resolvedTypeArguments = new Type[actualTypeArguments.length]; - for ( int idx = 0; idx < actualTypeArguments.length; idx++ ) { - resolvedTypeArguments[idx] = resolveType( actualTypeArguments[idx], context ); + private record SimpleParameterizedType(Class raw, Type[] args, Type owner) + implements ParameterizedType { + @Override + public Type[] getActualTypeArguments() { + return args; } - return new ParameterizedType() { - @Override - public Type[] getActualTypeArguments() { - return resolvedTypeArguments; - } + @Override + public Type getRawType() { + return raw; + } - @Override - public Type getRawType() { - return parameterizedType.getRawType(); - } + @Override + public Type getOwnerType() { + return owner; + } - @Override - public Type getOwnerType() { - return parameterizedType.getOwnerType(); + @Override + public String toString() { + final var joiner = new StringJoiner( ", ", "<", ">" ); + for ( var type : args ) { + joiner.add( type.getTypeName() ); } - - }; + return raw.getName() + joiner; + } } } diff --git a/hibernate-core/src/main/java/org/hibernate/internal/util/MarkerObject.java b/hibernate-core/src/main/java/org/hibernate/internal/util/MarkerObject.java deleted file mode 100644 index 38f605615ce2..000000000000 --- a/hibernate-core/src/main/java/org/hibernate/internal/util/MarkerObject.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * Copyright Red Hat Inc. and Hibernate Authors - */ -package org.hibernate.internal.util; - -import java.io.Serializable; - -/** - * @deprecated This is a legacy of very ancient versions of Hibernate. - * - * @author Gavin King - */ -@Deprecated -public class MarkerObject implements Serializable { - private final String name; - - public MarkerObject(String name) { - this.name = name; - } - - @Override - public String toString() { - return name; - } -} diff --git a/hibernate-core/src/main/java/org/hibernate/internal/util/Optional.java b/hibernate-core/src/main/java/org/hibernate/internal/util/Optional.java new file mode 100644 index 000000000000..ccc017bf72e6 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/internal/util/Optional.java @@ -0,0 +1,79 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.internal.util; + +import java.io.Serializable; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * A slightly less-broken version of {@link java.util.Optional}. + * + * @author Gavin King + */ +sealed public interface Optional extends Serializable { + record Defined(T result) implements Optional {} + record Undefined() implements Optional {} + + static Undefined undefined() { + return new Undefined<>(); + } + static Defined of(T t) { + return new Defined<>(t); + } + + default boolean defined() { + return this instanceof Defined; + } + + default void consume(Consumer consumer) { + if (this instanceof Defined defined) { + consumer.accept( defined.result() ); + } + } + + default void consume(Consumer consumer, T value) { + if (this instanceof Defined defined) { + consumer.accept( defined.result() ); + } + else { + consumer.accept( value ); + } + } + + default void consume(Consumer consumer, Supplier supplier) { + if (this instanceof Defined defined) { + consumer.accept( defined.result() ); + } + else { + consumer.accept( supplier.get() ); + } + } + + default T evaluate(T value) { + return this instanceof Defined defined + ? defined.result() + : value; + } + + default T evaluate(Supplier supplier) { + return this instanceof Defined defined + ? defined.result() + : supplier.get(); + } + + default X evaluate(Function function, Supplier supplier) { + return this instanceof Defined defined + ? function.apply( defined.result() ) + : supplier.get(); + } + + default X evaluate(Function function, X value) { + return this instanceof Defined defined + ? function.apply( defined.result() ) + : value; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/internal/util/ReflectHelper.java b/hibernate-core/src/main/java/org/hibernate/internal/util/ReflectHelper.java index 263524d9b6e7..5a6031434883 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/util/ReflectHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/util/ReflectHelper.java @@ -30,7 +30,7 @@ import jakarta.persistence.Transient; -import static java.beans.Introspector.decapitalize; +import static org.hibernate.internal.util.StringHelper.decapitalize; import static java.lang.Character.isLowerCase; import static java.lang.Thread.currentThread; diff --git a/hibernate-core/src/main/java/org/hibernate/internal/util/StringHelper.java b/hibernate-core/src/main/java/org/hibernate/internal/util/StringHelper.java index c0127039985c..a62fedab1f0f 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/util/StringHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/util/StringHelper.java @@ -922,4 +922,37 @@ public static String safeInterning(final String string) { return string == null ? null : string.intern(); } + /** + * Converts a string to normal Java variable name capitalization following + * the JavaBeans Introspector rules. This normally means converting the first + * character from upper case to lower case, but in the (unusual) special case + * when there is more than one character and both the first and second characters + * are upper case, we leave it alone. + *

    + * Thus "FooBah" becomes "fooBah" and "X" becomes "x", but "URL" stays as "URL". + *

    + * This is a reimplementation of {@code java.beans.Introspector.decapitalize()} + * to avoid pulling in the java.desktop module dependency. + * + * @param name The string to be decapitalized. + * @return The decapitalized version of the string. + */ + public static String decapitalize(final String name) { + if ( name == null || name.isEmpty() ) { + return name; + } + final char firstChar = name.charAt( 0 ); + // Already lowercase - return as-is to avoid allocation + if ( Character.isLowerCase( firstChar ) ) { + return name; + } + // Both first and second chars uppercase - return unchanged per JavaBeans spec + if ( name.length() > 1 && Character.isUpperCase( name.charAt( 1 ) ) ) { + return name; + } + final char[] chars = name.toCharArray(); + chars[0] = Character.toLowerCase( firstChar ); + return new String( chars ); + } + } diff --git a/hibernate-core/src/main/java/org/hibernate/internal/util/SubSequence.java b/hibernate-core/src/main/java/org/hibernate/internal/util/SubSequence.java index f670f73be4ca..eacf855b5b41 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/util/SubSequence.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/util/SubSequence.java @@ -33,10 +33,10 @@ public char charAt(int index) { @Override public CharSequence subSequence(int start, int end) { - if ( start < 0 || start >= length ) { + if ( start < 0 || start > length ) { throw new StringIndexOutOfBoundsException( start ); } - if ( end > length ) { + if ( end < start || end > length ) { throw new StringIndexOutOfBoundsException( end ); } return sequence.subSequence( this.start + start, this.start + end ); diff --git a/hibernate-core/src/main/java/org/hibernate/internal/util/beans/BeanInfo.java b/hibernate-core/src/main/java/org/hibernate/internal/util/beans/BeanInfo.java new file mode 100644 index 000000000000..b96e4b82f865 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/internal/util/beans/BeanInfo.java @@ -0,0 +1,20 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.internal.util.beans; + +/** + * Describes the properties of a JavaBean. + * This is a reimplementation to avoid dependency on the {@code java.desktop} module. + */ +public interface BeanInfo { + /** + * Returns an array of {@link PropertyDescriptor}s describing the + * editable properties of the bean. + * + * @return An array of PropertyDescriptor objects, or null if the + * information should be obtained by automatic analysis. + */ + PropertyDescriptor[] getPropertyDescriptors(); +} diff --git a/hibernate-core/src/main/java/org/hibernate/internal/util/beans/BeanInfoHelper.java b/hibernate-core/src/main/java/org/hibernate/internal/util/beans/BeanInfoHelper.java index 248b52c18d64..117c6fece88b 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/util/beans/BeanInfoHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/util/beans/BeanInfoHelper.java @@ -3,17 +3,49 @@ * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.internal.util.beans; -import java.beans.BeanInfo; -import java.beans.IntrospectionException; -import java.beans.Introspector; + import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.hibernate.internal.util.StringHelper.decapitalize; /** - * Utility for helping deal with {@link BeanInfo} + * Utility for helping deal with {@link BeanInfo}. + *

    + * This is a simplified reimplementation to avoid dependency on the {@code java.desktop} module. + * Unlike the JDK's {@code java.beans.Introspector}, this implementation: + *

      + *
    • Does not validate type compatibility between getters and setters
    • + *
    • Does not introspect methods from implemented interfaces (only class hierarchy)
    • + *
    • Takes the first matching setter when overloads exist
    • + *
    + * These simplifications are acceptable for Hibernate's use case of property-name-based injection. + *

    + * Caching is implemented using {@link ClassValue}, which is the JVM's built-in mechanism for + * associating computed values with classes. Unlike the JDK's {@code Introspector} cache, this + * approach is inherently GC-friendly: when a class is unloaded, the associated {@link BeanInfo} + * is automatically released, preventing classloader leaks without requiring explicit cache flushing. * * @author Steve Ebersole */ public class BeanInfoHelper { + + /** + * Cache of BeanInfo instances, keyed by class. + * Uses ClassValue which automatically handles class unloading - when a class is GC'd, + * its associated BeanInfo is also released, preventing classloader leaks. + * This cache is for the common case where stopClass is null (introspect up to Object). + */ + private static final ClassValue BEAN_INFO_CACHE = new ClassValue<>() { + @Override + protected BeanInfo computeValue(final Class type) { + return computeBeanInfo( type, null ); + } + }; + public interface BeanInfoDelegate { void processBeanInfo(BeanInfo beanInfo) throws Exception; } @@ -48,7 +80,7 @@ public static void visitBeanInfo(Class beanClass, BeanInfoDelegate delegate) public static void visitBeanInfo(Class beanClass, Class stopClass, BeanInfoDelegate delegate) { try { - BeanInfo info = Introspector.getBeanInfo( beanClass, stopClass ); + final BeanInfo info = getBeanInfo( beanClass, stopClass ); try { delegate.processBeanInfo( info ); } @@ -61,11 +93,11 @@ public static void visitBeanInfo(Class beanClass, Class stopClass, BeanInf catch ( Exception e ) { throw new BeanIntrospectionException( "Error delegating bean info use", e ); } - finally { - Introspector.flushFromCaches( beanClass ); - } } - catch ( IntrospectionException e ) { + catch ( BeanIntrospectionException e ) { + throw e; + } + catch ( Exception e ) { throw new BeanIntrospectionException( "Unable to determine bean info from class [" + beanClass.getName() + "]", e ); } } @@ -76,7 +108,7 @@ public static T visitBeanInfo(Class beanClass, ReturningBeanInfoDelegate< public static T visitBeanInfo(Class beanClass, Class stopClass, ReturningBeanInfoDelegate delegate) { try { - BeanInfo info = Introspector.getBeanInfo( beanClass, stopClass ); + final BeanInfo info = getBeanInfo( beanClass, stopClass ); try { return delegate.processBeanInfo( info ); } @@ -89,14 +121,126 @@ public static T visitBeanInfo(Class beanClass, Class stopClass, Return catch ( Exception e ) { throw new BeanIntrospectionException( "Error delegating bean info use", e ); } - finally { - Introspector.flushFromCaches( beanClass ); - } } - catch ( IntrospectionException e ) { + catch ( BeanIntrospectionException e ) { + throw e; + } + catch ( Exception e ) { throw new BeanIntrospectionException( "Unable to determine bean info from class [" + beanClass.getName() + "]", e ); } } + /** + * Introspect a JavaBean and return a BeanInfo object describing its properties. + * This method walks up the class hierarchy to collect all properties. + *

    + * Results are cached using {@link ClassValue} for the common case where stopClass is null. + * The cache is GC-friendly and does not cause classloader leaks. + * + * @param beanClass The class to introspect + * @param stopClass The base class at which to stop the analysis. Any methods + * declared in the stopClass or its superclasses will be ignored. + * May be null. + * @return A BeanInfo object describing the target bean + */ + public static BeanInfo getBeanInfo(final Class beanClass, final Class stopClass) { + // Use cache for the common case (stopClass == null means introspect up to Object) + if ( stopClass == null ) { + return BEAN_INFO_CACHE.get( beanClass ); + } + // Non-null stopClass is rare; compute without caching to keep cache simple + return computeBeanInfo( beanClass, stopClass ); + } + + private static BeanInfo computeBeanInfo(final Class beanClass, final Class stopClass) { + // LinkedHashMap for reproducible ordering (important for build-time code) + final Map properties = new LinkedHashMap<>(); + + // Walk from subclass to superclass; subclass properties take precedence + Class currentClass = beanClass; + while ( currentClass != null && currentClass != stopClass && currentClass != Object.class ) { + introspectClass( currentClass, properties ); + currentClass = currentClass.getSuperclass(); + } + + final PropertyDescriptor[] descriptors = properties.values().toArray( new PropertyDescriptor[0] ); + return new SimpleBeanInfo( descriptors ); + } + /** + * Simple implementation of {@link BeanInfo} that holds a fixed array of property descriptors. + */ + private static class SimpleBeanInfo implements BeanInfo { + private final PropertyDescriptor[] propertyDescriptors; + + SimpleBeanInfo(PropertyDescriptor[] propertyDescriptors) { + this.propertyDescriptors = propertyDescriptors; + } + + @Override + public PropertyDescriptor[] getPropertyDescriptors() { + return propertyDescriptors; + } + } + + private static void introspectClass(final Class clazz, final Map properties) { + for ( final Method method : clazz.getDeclaredMethods() ) { + final int modifiers = method.getModifiers(); + // Skip static, non-public, bridge, and synthetic methods + if ( Modifier.isStatic( modifiers ) + || !Modifier.isPublic( modifiers ) + || method.isBridge() + || method.isSynthetic() ) { + continue; + } + + final String methodName = method.getName(); + final int paramCount = method.getParameterCount(); + final Class returnType = method.getReturnType(); + + // Check for getter: getXxx() or isXxx() + if ( paramCount == 0 && returnType != void.class ) { + String propertyName = null; + if ( methodName.startsWith( "get" ) && methodName.length() > 3 ) { + propertyName = decapitalize( methodName.substring( 3 ) ); + } + else if ( methodName.startsWith( "is" ) && methodName.length() > 2 + && ( returnType == boolean.class || returnType == Boolean.class ) ) { + propertyName = decapitalize( methodName.substring( 2 ) ); + } + + if ( propertyName != null && !propertyName.isEmpty() ) { + final PropertyDescriptor existing = properties.get( propertyName ); + if ( existing == null ) { + properties.put( propertyName, new PropertyDescriptor( propertyName, method, null ) ); + } + else if ( existing.getReadMethod() == null ) { + // Merge: we had a setter, now add the getter + properties.put( propertyName, + new PropertyDescriptor( propertyName, method, existing.getWriteMethod() ) ); + } + // else: subclass already defined a getter, keep it (subclass precedence) + } + } + + // Check for setter: setXxx(value) + // Note: if overloaded setters exist, we take the first one found. + // This is acceptable for name-based property matching. + if ( paramCount == 1 && methodName.startsWith( "set" ) && methodName.length() > 3 ) { + final String propertyName = decapitalize( methodName.substring( 3 ) ); + if ( !propertyName.isEmpty() ) { + final PropertyDescriptor existing = properties.get( propertyName ); + if ( existing == null ) { + properties.put( propertyName, new PropertyDescriptor( propertyName, null, method ) ); + } + else if ( existing.getWriteMethod() == null ) { + // Merge: we had a getter, now add the setter + properties.put( propertyName, + new PropertyDescriptor( propertyName, existing.getReadMethod(), method ) ); + } + // else: subclass already defined a setter, keep it (subclass precedence) + } + } + } + } } diff --git a/hibernate-core/src/main/java/org/hibernate/internal/util/beans/BeanIntrospectionException.java b/hibernate-core/src/main/java/org/hibernate/internal/util/beans/BeanIntrospectionException.java index f206f80b3b09..346994ee756e 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/util/beans/BeanIntrospectionException.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/util/beans/BeanIntrospectionException.java @@ -7,7 +7,7 @@ import org.hibernate.HibernateException; /** - * Indicates a problem dealing with {@link java.beans.BeanInfo} via the {@link BeanInfoHelper} delegate. + * Indicates a problem dealing with {@link BeanInfo} via the {@link BeanInfoHelper} delegate. * * @author Steve Ebersole */ diff --git a/hibernate-core/src/main/java/org/hibernate/internal/util/beans/PropertyDescriptor.java b/hibernate-core/src/main/java/org/hibernate/internal/util/beans/PropertyDescriptor.java new file mode 100644 index 000000000000..8c21d62ef9c1 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/internal/util/beans/PropertyDescriptor.java @@ -0,0 +1,50 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.internal.util.beans; + +import java.lang.reflect.Method; + +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Describes a single property of a JavaBean. + * This is a reimplementation to avoid dependency on the {@code java.desktop} module. + */ +public final class PropertyDescriptor { + private final String name; + private final @Nullable Method readMethod; + private final @Nullable Method writeMethod; + + public PropertyDescriptor(final String name, final @Nullable Method readMethod, final @Nullable Method writeMethod) { + this.name = name; + this.readMethod = readMethod; + this.writeMethod = writeMethod; + } + + /** + * Gets the programmatic name of this property. + */ + public String getName() { + return name; + } + + /** + * Gets the method used to read the property value. + * + * @return The getter method, or null if the property is write-only. + */ + public @Nullable Method getReadMethod() { + return readMethod; + } + + /** + * Gets the method used to write the property value. + * + * @return The setter method, or null if the property is read-only. + */ + public @Nullable Method getWriteMethod() { + return writeMethod; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/internal/util/collections/InstanceIdentityMap.java b/hibernate-core/src/main/java/org/hibernate/internal/util/collections/InstanceIdentityMap.java index 23c35c747b18..d6e303816b6c 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/util/collections/InstanceIdentityMap.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/util/collections/InstanceIdentityMap.java @@ -62,7 +62,7 @@ public boolean containsKey(int instanceId, Object key) { } /** - * @inheritDoc + * {@inheritDoc} * @implNote This only works for {@link InstanceIdentity} keys, and it's inefficient * since we need to do a type check. Prefer using {@link #containsKey(int, Object)}. */ @@ -130,7 +130,7 @@ private boolean containsMapping(Object key, Object value) { } /** - * @inheritDoc + * {@inheritDoc} * @implNote This only works for {@link InstanceIdentity} keys, and it's inefficient * since we need to do a type check. Prefer using {@link #get(int, Object)}. */ @@ -200,7 +200,7 @@ private boolean containsMapping(Object key, Object value) { } /** - * @inheritDoc + * {@inheritDoc} * @implNote This only works for {@link InstanceIdentity} keys, and it's inefficient * since we need to do a type check. Prefer using {@link #remove(int, Object)}. */ diff --git a/hibernate-core/src/main/java/org/hibernate/internal/util/type/PrimitiveWrapperHelper.java b/hibernate-core/src/main/java/org/hibernate/internal/util/type/PrimitiveWrapperHelper.java deleted file mode 100644 index e9c2de5052d0..000000000000 --- a/hibernate-core/src/main/java/org/hibernate/internal/util/type/PrimitiveWrapperHelper.java +++ /dev/null @@ -1,266 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * Copyright Red Hat Inc. and Hibernate Authors - */ -package org.hibernate.internal.util.type; - -/** - * Helper for primitive/wrapper utilities. - * - * @author Steve Ebersole - */ -public final class PrimitiveWrapperHelper { - private PrimitiveWrapperHelper() { - } - - /** - * Describes a particular primitive/wrapper combo - */ - public interface PrimitiveWrapperDescriptor { - Class getPrimitiveClass(); - Class getWrapperClass(); - } - - public static class BooleanDescriptor implements PrimitiveWrapperDescriptor { - public static final BooleanDescriptor INSTANCE = new BooleanDescriptor(); - - private BooleanDescriptor() { - } - - @Override - public Class getPrimitiveClass() { - return boolean.class; - } - - @Override - public Class getWrapperClass() { - return Boolean.class; - } - } - - public static class CharacterDescriptor implements PrimitiveWrapperDescriptor { - public static final CharacterDescriptor INSTANCE = new CharacterDescriptor(); - - private CharacterDescriptor() { - } - - @Override - public Class getPrimitiveClass() { - return char.class; - } - - @Override - public Class getWrapperClass() { - return Character.class; - } - } - - public static class ByteDescriptor implements PrimitiveWrapperDescriptor { - public static final ByteDescriptor INSTANCE = new ByteDescriptor(); - - private ByteDescriptor() { - } - - @Override - public Class getPrimitiveClass() { - return byte.class; - } - - @Override - public Class getWrapperClass() { - return Byte.class; - } - } - - public static class ShortDescriptor implements PrimitiveWrapperDescriptor { - public static final ShortDescriptor INSTANCE = new ShortDescriptor(); - - private ShortDescriptor() { - } - - @Override - public Class getPrimitiveClass() { - return short.class; - } - - @Override - public Class getWrapperClass() { - return Short.class; - } - } - - public static class IntegerDescriptor implements PrimitiveWrapperDescriptor { - public static final IntegerDescriptor INSTANCE = new IntegerDescriptor(); - - private IntegerDescriptor() { - } - - @Override - public Class getPrimitiveClass() { - return int.class; - } - - @Override - public Class getWrapperClass() { - return Integer.class; - } - } - - public static class LongDescriptor implements PrimitiveWrapperDescriptor { - public static final LongDescriptor INSTANCE = new LongDescriptor(); - - private LongDescriptor() { - } - - @Override - public Class getPrimitiveClass() { - return long.class; - } - - @Override - public Class getWrapperClass() { - return Long.class; - } - } - - public static class FloatDescriptor implements PrimitiveWrapperDescriptor { - public static final FloatDescriptor INSTANCE = new FloatDescriptor(); - - private FloatDescriptor() { - } - - @Override - public Class getPrimitiveClass() { - return float.class; - } - - @Override - public Class getWrapperClass() { - return Float.class; - } - } - - public static class DoubleDescriptor implements PrimitiveWrapperDescriptor { - public static final DoubleDescriptor INSTANCE = new DoubleDescriptor(); - - private DoubleDescriptor() { - } - - @Override - public Class getPrimitiveClass() { - return double.class; - } - - @Override - public Class getWrapperClass() { - return Double.class; - } - } - - @SuppressWarnings("unchecked") - public static PrimitiveWrapperDescriptor getDescriptorByPrimitiveType(Class primitiveClazz) { - if ( ! primitiveClazz.isPrimitive() ) { - throw new IllegalArgumentException( "Given class is not a primitive type : " + primitiveClazz.getName() ); - } - - if ( boolean.class == primitiveClazz ) { - return (PrimitiveWrapperDescriptor) BooleanDescriptor.INSTANCE; - } - - if ( char.class == primitiveClazz ) { - return (PrimitiveWrapperDescriptor) CharacterDescriptor.INSTANCE; - } - - if ( byte.class == primitiveClazz ) { - return (PrimitiveWrapperDescriptor) ByteDescriptor.INSTANCE; - } - - if ( short.class == primitiveClazz ) { - return (PrimitiveWrapperDescriptor) ShortDescriptor.INSTANCE; - } - - if ( int.class == primitiveClazz ) { - return (PrimitiveWrapperDescriptor) IntegerDescriptor.INSTANCE; - } - - if ( long.class == primitiveClazz ) { - return (PrimitiveWrapperDescriptor) LongDescriptor.INSTANCE; - } - - if ( float.class == primitiveClazz ) { - return (PrimitiveWrapperDescriptor) FloatDescriptor.INSTANCE; - } - - if ( double.class == primitiveClazz ) { - return (PrimitiveWrapperDescriptor) DoubleDescriptor.INSTANCE; - } - - if ( void.class == primitiveClazz ) { - throw new IllegalArgumentException( "void, as primitive type, has no wrapper equivalent" ); - } - - throw new IllegalArgumentException( "Unrecognized primitive type class : " + primitiveClazz.getName() ); - } - - @SuppressWarnings("unchecked") - public static PrimitiveWrapperDescriptor getDescriptorByWrapperType(Class wrapperClass) { - if ( wrapperClass.isPrimitive() ) { - throw new IllegalArgumentException( "Given class is a primitive type : " + wrapperClass.getName() ); - } - - if ( Boolean.class.equals( wrapperClass ) ) { - return (PrimitiveWrapperDescriptor) BooleanDescriptor.INSTANCE; - } - - if ( Character.class == wrapperClass ) { - return (PrimitiveWrapperDescriptor) CharacterDescriptor.INSTANCE; - } - - if ( Byte.class == wrapperClass ) { - return (PrimitiveWrapperDescriptor) ByteDescriptor.INSTANCE; - } - - if ( Short.class == wrapperClass ) { - return (PrimitiveWrapperDescriptor) ShortDescriptor.INSTANCE; - } - - if ( Integer.class == wrapperClass ) { - return (PrimitiveWrapperDescriptor) IntegerDescriptor.INSTANCE; - } - - if ( Long.class == wrapperClass ) { - return (PrimitiveWrapperDescriptor) LongDescriptor.INSTANCE; - } - - if ( Float.class == wrapperClass ) { - return (PrimitiveWrapperDescriptor) FloatDescriptor.INSTANCE; - } - - if ( Double.class == wrapperClass ) { - return (PrimitiveWrapperDescriptor) DoubleDescriptor.INSTANCE; - } - - // most likely Void.class, which we can't really handle here - throw new IllegalArgumentException( "Unrecognized wrapper type class : " + wrapperClass.getName() ); - } - - public static boolean isWrapper(Class clazz) { - try { - getDescriptorByWrapperType( clazz ); - return true; - } - catch (Exception e) { - return false; - } - } - - public static boolean arePrimitiveWrapperEquivalents(Class converterDefinedType, Class propertyType) { - if ( converterDefinedType.isPrimitive() ) { - return getDescriptorByPrimitiveType( converterDefinedType ).getWrapperClass().equals( propertyType ); - } - else if ( propertyType.isPrimitive() ) { - return getDescriptorByPrimitiveType( propertyType ).getWrapperClass().equals( converterDefinedType ); - } - return false; - } -} diff --git a/hibernate-core/src/main/java/org/hibernate/internal/util/type/PrimitiveWrappers.java b/hibernate-core/src/main/java/org/hibernate/internal/util/type/PrimitiveWrappers.java new file mode 100644 index 000000000000..163f1c35caeb --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/internal/util/type/PrimitiveWrappers.java @@ -0,0 +1,47 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.internal.util.type; + +/** + * Maps primitive types to their wrapper counterparts. + * + * @author Gavin King + */ +public final class PrimitiveWrappers { + + public static Class canonicalize(Class type) { + if ( type.isPrimitive() ) { + @SuppressWarnings("unchecked") + // completely safe, boolean.class is a Class + final var wrapperClass = (Class) wrapperClass( type ); + return wrapperClass; + } + else { + return type; + } + } + + private static Class wrapperClass(Class primitiveClass) { + return switch ( primitiveClass.getName() ) { + case "boolean" -> Boolean.class; + case "char" -> Character.class; + case "byte" -> Byte.class; + case "short" -> Short.class; + case "int" -> Integer.class; + case "long" -> Long.class; + case "float" -> Float.class; + case "double" -> Double.class; + default -> throw new AssertionError( "Unknown primitive type: " + primitiveClass ); + }; + } + + public static X cast(Class type, Object value) { + return canonicalize( type ).cast( value ); + } + + public static boolean isInstance(Class type, Object value) { + return canonicalize( type ).isInstance( value ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/jdbc/Expectation.java b/hibernate-core/src/main/java/org/hibernate/jdbc/Expectation.java index 941eb5e65d97..3e29db80f41e 100644 --- a/hibernate-core/src/main/java/org/hibernate/jdbc/Expectation.java +++ b/hibernate-core/src/main/java/org/hibernate/jdbc/Expectation.java @@ -179,6 +179,31 @@ protected int expectedRowCount() { } } + /** + * Row count checking. A row count is an integer value returned by + * {@link java.sql.PreparedStatement#executeUpdate()} or + * {@link java.sql.Statement#executeBatch()}. The row count is checked + * against an expected value, but is also allowed to be 0. + * For example, the expected row count for an {@code UPSERT} statement is 0 or 1. + */ + class OptionalRowCount implements Expectation { + @Override + public final void verifyOutcome(int rowCount, PreparedStatement statement, int batchPosition, String sql) { + if ( rowCount != 0 ) { + if ( batchPosition < 0 ) { + checkNonBatched( expectedRowCount(), rowCount, sql ); + } + else { + checkBatched( expectedRowCount(), rowCount, batchPosition, sql ); + } + } + } + + protected int expectedRowCount() { + return 1; + } + } + /** * Essentially identical to {@link RowCount} except that the row count * is obtained via an output parameter of a {@linkplain CallableStatement diff --git a/hibernate-core/src/main/java/org/hibernate/jpa/HibernatePersistenceConfiguration.java b/hibernate-core/src/main/java/org/hibernate/jpa/HibernatePersistenceConfiguration.java index 67e823efaab9..5ae350c28054 100644 --- a/hibernate-core/src/main/java/org/hibernate/jpa/HibernatePersistenceConfiguration.java +++ b/hibernate-core/src/main/java/org/hibernate/jpa/HibernatePersistenceConfiguration.java @@ -51,8 +51,7 @@ * Standard JPA configuration properties are enumerated by the supertype * {@link PersistenceConfiguration}. All configuration properties understood * by Hibernate are enumerated by {@link AvailableSettings}. - *

    - *

    + * 
    {@code
      * SessionFactory factory = new HibernatePersistenceConfiguration()
      *     // scan classes for mapping annotations
      *     .managedClasses(Item.class, Bid.class, User.class)
    @@ -60,7 +59,7 @@
      *     .setProperty(PersistenceConfiguration.JDBC_DATASOURCE,
      *                  "java:comp/env/jdbc/test")
      *     .buildSessionFactory();
    - * 
    + * }
    *

    * When instantiated, an instance of * {@code HibernatePersistenceConfiguration} has its properties initially diff --git a/hibernate-core/src/main/java/org/hibernate/jpa/HibernatePersistenceProvider.java b/hibernate-core/src/main/java/org/hibernate/jpa/HibernatePersistenceProvider.java index 51f3b5b9fb3c..97f76df178c7 100644 --- a/hibernate-core/src/main/java/org/hibernate/jpa/HibernatePersistenceProvider.java +++ b/hibernate-core/src/main/java/org/hibernate/jpa/HibernatePersistenceProvider.java @@ -5,6 +5,7 @@ package org.hibernate.jpa; import java.util.Collection; +import java.util.HashMap; import java.util.List; import java.util.Map; import jakarta.persistence.EntityManagerFactory; @@ -43,12 +44,13 @@ public class HibernatePersistenceProvider implements PersistenceProvider { /** * {@inheritDoc} * - * @implSpec Per the specification, the values passed as {@code properties} override values found in {@code persistence.xml} + * @implSpec The values passed in the {@code map} override values found + * in {@code persistence.xml} according to the JPA specification. */ @Override - public EntityManagerFactory createEntityManagerFactory(String persistenceUnitName, Map properties) { + public EntityManagerFactory createEntityManagerFactory(String persistenceUnitName, Map map) { JPA_LOGGER.startingCreateEntityManagerFactory( persistenceUnitName ); - final var builder = getEntityManagerFactoryBuilderOrNull( persistenceUnitName, properties ); + final var builder = getEntityManagerFactoryBuilderOrNull( persistenceUnitName, map ); if ( builder == null ) { JPA_LOGGER.couldNotObtainEmfBuilder("null"); return null; @@ -58,21 +60,40 @@ public EntityManagerFactory createEntityManagerFactory(String persistenceUnitNam } } - protected EntityManagerFactoryBuilder getEntityManagerFactoryBuilderOrNull(String persistenceUnitName, Map properties) { - return getEntityManagerFactoryBuilderOrNull( persistenceUnitName, properties, null, null ); + protected EntityManagerFactoryBuilder getEntityManagerFactoryBuilderOrNull( + String persistenceUnitName, Map properties) { + return getEntityManagerFactoryBuilderOrNull( + persistenceUnitName, + properties, + null, + null + ); } - protected EntityManagerFactoryBuilder getEntityManagerFactoryBuilderOrNull(String persistenceUnitName, Map properties, + protected EntityManagerFactoryBuilder getEntityManagerFactoryBuilderOrNull( + String persistenceUnitName, Map properties, ClassLoader providedClassLoader) { - return getEntityManagerFactoryBuilderOrNull( persistenceUnitName, properties, providedClassLoader, null ); + return getEntityManagerFactoryBuilderOrNull( + persistenceUnitName, + properties, + providedClassLoader, + null + ); } - protected EntityManagerFactoryBuilder getEntityManagerFactoryBuilderOrNull(String persistenceUnitName, Map properties, + protected EntityManagerFactoryBuilder getEntityManagerFactoryBuilderOrNull( + String persistenceUnitName, Map properties, ClassLoaderService providedClassLoaderService) { - return getEntityManagerFactoryBuilderOrNull( persistenceUnitName, properties, null, providedClassLoaderService ); + return getEntityManagerFactoryBuilderOrNull( + persistenceUnitName, + properties, + null, + providedClassLoaderService + ); } - private EntityManagerFactoryBuilder getEntityManagerFactoryBuilderOrNull(String persistenceUnitName, Map properties, + private EntityManagerFactoryBuilder getEntityManagerFactoryBuilderOrNull( + String persistenceUnitName, Map properties, ClassLoader providedClassLoader, ClassLoaderService providedClassLoaderService) { JPA_LOGGER.attemptingToObtainEmfBuilder( persistenceUnitName ); final var integration = wrap( properties ); @@ -92,21 +113,24 @@ private EntityManagerFactoryBuilder getEntityManagerFactoryBuilderOrNull(String ); } - final boolean matches = persistenceUnitName == null || persistenceUnit.getName().equals( persistenceUnitName ); - if ( !matches ) { - JPA_LOGGER.excludingDueToNameMismatch(); - continue; - } + final boolean matches = + persistenceUnitName == null + || persistenceUnit.getName().equals( persistenceUnitName ); + if ( matches ) { + // See if we (Hibernate) are the persistence provider + if ( isProvider( persistenceUnit, properties ) ) { + return providedClassLoaderService == null + ? getEntityManagerFactoryBuilder( persistenceUnit, integration, providedClassLoader ) + : getEntityManagerFactoryBuilder( persistenceUnit, integration, providedClassLoaderService ); + } + else { + JPA_LOGGER.excludingDueToProviderMismatch(); + } - // See if we (Hibernate) are the persistence provider - if ( !isProvider( persistenceUnit, properties ) ) { - JPA_LOGGER.excludingDueToProviderMismatch(); - continue; } - - return providedClassLoaderService == null - ? getEntityManagerFactoryBuilder( persistenceUnit, integration, providedClassLoader ) - : getEntityManagerFactoryBuilder( persistenceUnit, integration, providedClassLoaderService ); + else { + JPA_LOGGER.excludingDueToNameMismatch(); + } } JPA_LOGGER.foundNoMatchingPersistenceUnits(); @@ -118,11 +142,16 @@ protected static Map wrap(Map properties) { } // Check before changing: may be overridden in Quarkus - protected Collection locatePersistenceUnits(Map integration, ClassLoader providedClassLoader, + protected Collection locatePersistenceUnits( + Map integration, + ClassLoader providedClassLoader, ClassLoaderService providedClassLoaderService) { try { - var parser = PersistenceXmlParser.create( integration, providedClassLoader, providedClassLoaderService ); - final var xmlUrls = parser.getClassLoaderService().locateResources( "META-INF/persistence.xml" ); + final var parser = + PersistenceXmlParser.create( integration, providedClassLoader, providedClassLoaderService ); + final var xmlUrls = + parser.getClassLoaderService() + .locateResources( "META-INF/persistence.xml" ); if ( xmlUrls.isEmpty() ) { JPA_LOGGER.unableToFindPersistenceXmlInClasspath(); return List.of(); @@ -139,23 +168,38 @@ protected Collection locatePersistenceUnits(Map /** * {@inheritDoc} - *

    - * Note: per-spec, the values passed as {@code properties} override values found in {@link PersistenceUnitInfo} + * + * @implSpec The values passed in the {@code map} override values found + * in {@link PersistenceUnitInfo#getProperties()} according to + * the JPA specification. */ @Override - public EntityManagerFactory createContainerEntityManagerFactory(PersistenceUnitInfo info, Map properties) { + public EntityManagerFactory createContainerEntityManagerFactory(PersistenceUnitInfo info, Map map) { JPA_LOGGER.startingCreateContainerEntityManagerFactory( info.getPersistenceUnitName() ); - return getEntityManagerFactoryBuilder( info, properties ).build(); + return getEntityManagerFactoryBuilder( info, map ).build(); } + /** + * {@inheritDoc} + * + * @implSpec The values passed in the {@code map} override values found + * in {@link PersistenceUnitInfo#getProperties()} according to + * the JPA specification. + */ @Override - public void generateSchema(PersistenceUnitInfo info, Map map) { + public void generateSchema(PersistenceUnitInfo info, Map map) { JPA_LOGGER.startingGenerateSchemaForPuiName( info.getPersistenceUnitName() ); getEntityManagerFactoryBuilder( info, map ).generateSchema(); } + /** + * {@inheritDoc} + * + * @implSpec The values passed in the {@code map} override values found + * in {@code persistence.xml} according to the JPA specification. + */ @Override - public boolean generateSchema(String persistenceUnitName, Map map) { + public boolean generateSchema(String persistenceUnitName, Map map) { JPA_LOGGER.startingGenerateSchema( persistenceUnitName ); final var builder = getEntityManagerFactoryBuilderOrNull( persistenceUnitName, map ); if ( builder == null ) { @@ -168,18 +212,36 @@ public boolean generateSchema(String persistenceUnitName, Map map) { } } - protected EntityManagerFactoryBuilder getEntityManagerFactoryBuilder(PersistenceUnitInfo info, Map integration) { - return Bootstrap.getEntityManagerFactoryBuilder( info, integration ); + private static Map settingsMap(Map integration) { + final Map result = new HashMap<>(); + integration.forEach( (key, value) -> { + // ignore non-string keys + if (key instanceof String string) { + result.put( string, value ); + } + } ); + return result; + } + + protected EntityManagerFactoryBuilder getEntityManagerFactoryBuilder( + PersistenceUnitInfo info, Map integration) { + return Bootstrap.getEntityManagerFactoryBuilder( info, settingsMap( integration ) ); } - protected EntityManagerFactoryBuilder getEntityManagerFactoryBuilder(PersistenceUnitDescriptor persistenceUnitDescriptor, - Map integration, ClassLoader providedClassLoader) { - return Bootstrap.getEntityManagerFactoryBuilder( persistenceUnitDescriptor, integration, providedClassLoader ); + protected EntityManagerFactoryBuilder getEntityManagerFactoryBuilder( + PersistenceUnitDescriptor persistenceUnitDescriptor, + Map integration, + ClassLoader providedClassLoader) { + return Bootstrap.getEntityManagerFactoryBuilder( persistenceUnitDescriptor, + settingsMap( integration ), providedClassLoader ); } - protected EntityManagerFactoryBuilder getEntityManagerFactoryBuilder(PersistenceUnitDescriptor persistenceUnitDescriptor, - Map integration, ClassLoaderService providedClassLoaderService) { - return Bootstrap.getEntityManagerFactoryBuilder( persistenceUnitDescriptor, integration, providedClassLoaderService ); + protected EntityManagerFactoryBuilder getEntityManagerFactoryBuilder( + PersistenceUnitDescriptor persistenceUnitDescriptor, + Map integration, + ClassLoaderService providedClassLoaderService) { + return Bootstrap.getEntityManagerFactoryBuilder( persistenceUnitDescriptor, + settingsMap( integration ), providedClassLoaderService ); } private final ProviderUtil providerUtil = new ProviderUtil() { 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 8d82e0320a57..b768e2bed31b 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 @@ -210,7 +210,7 @@ private EntityManagerFactoryBuilderImpl( try { // merge configuration sources and build the "standard" service registry final var registryBuilder = getStandardServiceRegistryBuilder( bootstrapServiceRegistry ); - final MergedSettings mergedSettings = + final var mergedSettings = mergeSettings( persistenceUnit, integrationSettings, registryBuilder, mergedSettingsBaseline ); ignoreFlushBeforeCompletion( mergedSettings ); // keep the merged config values for phase-2 @@ -627,13 +627,10 @@ else if ( keyString.startsWith( COLLECTION_CACHE_PREFIX ) ) { private static String getCfgXmlResourceName(Map integrationSettings, MergedSettings mergedSettings) { final String cfgXmlResourceName = (String) mergedSettings.getConfigurationValues().remove( CFG_XML_FILE ); - if ( isEmpty( cfgXmlResourceName ) ) { - // see if integration settings named a Hibernate config file.... - return (String) integrationSettings.get( CFG_XML_FILE ); - } - else { - return cfgXmlResourceName; - } + return isEmpty( cfgXmlResourceName ) + // see if integration settings named a Hibernate config file + ? (String) integrationSettings.get( CFG_XML_FILE ) + : cfgXmlResourceName; } /** @@ -1304,8 +1301,7 @@ private void addMappingFiles(MetadataSources metadataSources) { } @SuppressWarnings("unchecked") // Safe, because we just checked! final var attributeConverterType = (Class>) converterClass; - converterDescriptors.add( ConverterDescriptors.of( attributeConverterType, - metamodelBuilder.getBootstrapContext().getClassmateContext() ) ); + converterDescriptors.add( ConverterDescriptors.of( attributeConverterType ) ); } else { metadataSources.addAnnotatedClass( converterClass ); @@ -1515,8 +1511,8 @@ private String exceptionHeader() { @SuppressWarnings("unchecked") private T loadSettingInstance(String settingName, Object settingValue, Class clazz) { final Class instanceClass; - if ( clazz.isAssignableFrom( settingValue.getClass() ) ) { - return (T) settingValue; + if ( clazz.isInstance( settingValue ) ) { + return clazz.cast( settingValue ); } else if ( settingValue instanceof Class ) { instanceClass = (Class) settingValue; diff --git a/hibernate-core/src/main/java/org/hibernate/jpa/boot/internal/MergedSettings.java b/hibernate-core/src/main/java/org/hibernate/jpa/boot/internal/MergedSettings.java index 7599e09a6bd6..22804e84d470 100644 --- a/hibernate-core/src/main/java/org/hibernate/jpa/boot/internal/MergedSettings.java +++ b/hibernate-core/src/main/java/org/hibernate/jpa/boot/internal/MergedSettings.java @@ -13,7 +13,6 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.Properties; import java.util.concurrent.ConcurrentHashMap; import static org.hibernate.cfg.PersistenceSettings.PERSISTENCE_UNIT_NAME; @@ -37,7 +36,7 @@ List getCacheRegionDefinitions() { } void processPersistenceUnitDescriptorProperties(PersistenceUnitDescriptor persistenceUnit) { - final Properties properties = persistenceUnit.getProperties(); + final var properties = persistenceUnit.getProperties(); if ( properties != null ) { getConfigurationValues().putAll( PropertiesHelper.map( properties ) ); } @@ -45,7 +44,7 @@ void processPersistenceUnitDescriptorProperties(PersistenceUnitDescriptor persis } void processHibernateConfigXmlResources(LoadedConfig loadedConfig) { - if ( !getConfigurationValues().containsKey( SESSION_FACTORY_NAME) ) { + if ( !getConfigurationValues().containsKey( SESSION_FACTORY_NAME) ) { // there is not already a SF-name in the merged settings final String sessionFactoryName = loadedConfig.getSessionFactoryName(); if ( sessionFactoryName != null ) { diff --git a/hibernate-core/src/main/java/org/hibernate/jpa/boot/spi/Bootstrap.java b/hibernate-core/src/main/java/org/hibernate/jpa/boot/spi/Bootstrap.java index 759aa78d24f3..7d0f7f3cfb82 100644 --- a/hibernate-core/src/main/java/org/hibernate/jpa/boot/spi/Bootstrap.java +++ b/hibernate-core/src/main/java/org/hibernate/jpa/boot/spi/Bootstrap.java @@ -30,7 +30,7 @@ private Bootstrap() { public static EntityManagerFactoryBuilder getEntityManagerFactoryBuilder( PersistenceUnitDescriptor persistenceUnitDescriptor, - Map integration) { + Map integration) { return new EntityManagerFactoryBuilderImpl( persistenceUnitDescriptor, integration ); } @@ -46,7 +46,7 @@ public static EntityManagerFactoryBuilder getEntityManagerFactoryBuilder( public static EntityManagerFactoryBuilder getEntityManagerFactoryBuilder( URL persistenceXmlUrl, String persistenceUnitName, - Map integration) { + Map integration) { return getEntityManagerFactoryBuilder( persistenceXmlUrl, persistenceUnitName, PersistenceUnitTransactionType.RESOURCE_LOCAL, integration ); } @@ -63,7 +63,7 @@ public static EntityManagerFactoryBuilder getEntityManagerFactoryBuilder( URL persistenceXmlUrl, String persistenceUnitName, PersistenceUnitTransactionType transactionType, - Map integration) { + Map integration) { return new EntityManagerFactoryBuilderImpl( PersistenceXmlParser.create( integration ).parse( List.of( persistenceXmlUrl ), transactionType ).get( persistenceUnitName ), integration @@ -72,14 +72,14 @@ public static EntityManagerFactoryBuilder getEntityManagerFactoryBuilder( public static EntityManagerFactoryBuilder getEntityManagerFactoryBuilder( PersistenceUnitDescriptor persistenceUnitDescriptor, - Map integration, + Map integration, ClassLoader providedClassLoader) { return new EntityManagerFactoryBuilderImpl( persistenceUnitDescriptor, integration, providedClassLoader ); } public static EntityManagerFactoryBuilder getEntityManagerFactoryBuilder( PersistenceUnitDescriptor persistenceUnitDescriptor, - Map integration, + Map integration, ClassLoaderService providedClassLoaderService) { return new EntityManagerFactoryBuilderImpl( persistenceUnitDescriptor, integration, providedClassLoaderService ); } @@ -90,27 +90,27 @@ public static EntityManagerFactoryBuilder getEntityManagerFactoryBuilder( @Internal public static EntityManagerFactoryBuilder getEntityManagerFactoryBuilder( PersistenceUnitDescriptor persistenceUnitDescriptor, - Map integration, + Map integration, Consumer mergedSettingsBaseline) { return new EntityManagerFactoryBuilderImpl( persistenceUnitDescriptor, integration, mergedSettingsBaseline ); } public static EntityManagerFactoryBuilder getEntityManagerFactoryBuilder( PersistenceUnitInfo persistenceUnitInfo, - Map integration) { + Map integration) { return getEntityManagerFactoryBuilder( new PersistenceUnitInfoDescriptor( persistenceUnitInfo ), integration ); } public static EntityManagerFactoryBuilder getEntityManagerFactoryBuilder( PersistenceUnitInfo persistenceUnitInfo, - Map integration, + Map integration, ClassLoader providedClassLoader) { return getEntityManagerFactoryBuilder( new PersistenceUnitInfoDescriptor( persistenceUnitInfo ), integration, providedClassLoader ); } public static EntityManagerFactoryBuilder getEntityManagerFactoryBuilder( PersistenceUnitInfo persistenceUnitInfo, - Map integration, + Map integration, ClassLoaderService providedClassLoaderService) { return getEntityManagerFactoryBuilder( new PersistenceUnitInfoDescriptor( persistenceUnitInfo ), integration, providedClassLoaderService ); } @@ -121,7 +121,7 @@ public static EntityManagerFactoryBuilder getEntityManagerFactoryBuilder( @Internal public static EntityManagerFactoryBuilder getEntityManagerFactoryBuilder( PersistenceUnitInfo persistenceUnitInfo, - Map integration, + Map integration, Consumer mergedSettingsBaseline) { return getEntityManagerFactoryBuilder( new PersistenceUnitInfoDescriptor( persistenceUnitInfo ), integration, mergedSettingsBaseline ); } diff --git a/hibernate-core/src/main/java/org/hibernate/jpa/boot/spi/PersistenceXmlParser.java b/hibernate-core/src/main/java/org/hibernate/jpa/boot/spi/PersistenceXmlParser.java index 772c67bcf017..98b0256f5c4a 100644 --- a/hibernate-core/src/main/java/org/hibernate/jpa/boot/spi/PersistenceXmlParser.java +++ b/hibernate-core/src/main/java/org/hibernate/jpa/boot/spi/PersistenceXmlParser.java @@ -5,34 +5,24 @@ package org.hibernate.jpa.boot.spi; import java.io.IOException; -import java.io.InputStream; import java.net.URL; -import java.net.URLConnection; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Properties; import javax.xml.transform.stream.StreamSource; -import org.hibernate.boot.archive.internal.ArchiveHelper; import org.hibernate.boot.jaxb.Origin; import org.hibernate.boot.jaxb.SourceType; import org.hibernate.boot.jaxb.configuration.spi.JaxbPersistenceImpl; import org.hibernate.boot.jaxb.configuration.spi.JaxbPersistenceImpl.JaxbPersistenceUnitImpl; -import org.hibernate.boot.jaxb.configuration.spi.JaxbPersistenceImpl.JaxbPersistenceUnitImpl.JaxbPropertiesImpl; -import org.hibernate.boot.jaxb.configuration.spi.JaxbPersistenceImpl.JaxbPersistenceUnitImpl.JaxbPropertiesImpl.JaxbPropertyImpl; import org.hibernate.boot.jaxb.internal.ConfigurationBinder; -import org.hibernate.boot.jaxb.spi.Binding; import org.hibernate.boot.registry.classloading.internal.ClassLoaderServiceImpl; import org.hibernate.boot.registry.classloading.internal.TcclLookupPrecedence; import org.hibernate.boot.registry.classloading.spi.ClassLoaderService; import org.hibernate.cfg.AvailableSettings; -import org.hibernate.internal.log.DeprecationLogger; -import org.hibernate.internal.util.StringHelper; import org.hibernate.jpa.boot.internal.ParsedPersistenceXmlDescriptor; -import org.hibernate.jpa.internal.util.ConfigurationHelper; import jakarta.persistence.PersistenceException; import jakarta.persistence.PersistenceUnitTransactionType; @@ -40,8 +30,20 @@ import static jakarta.persistence.PersistenceUnitTransactionType.JTA; import static jakarta.persistence.PersistenceUnitTransactionType.RESOURCE_LOCAL; +import static org.hibernate.boot.archive.internal.ArchiveHelper.getJarURLFromURLEntry; +import static org.hibernate.cfg.JdbcSettings.JAKARTA_JTA_DATASOURCE; +import static org.hibernate.cfg.JdbcSettings.JAKARTA_NON_JTA_DATASOURCE; +import static org.hibernate.cfg.JdbcSettings.JPA_JTA_DATASOURCE; +import static org.hibernate.cfg.JdbcSettings.JPA_NON_JTA_DATASOURCE; +import static org.hibernate.cfg.PersistenceSettings.JAKARTA_PERSISTENCE_PROVIDER; +import static org.hibernate.cfg.PersistenceSettings.JAKARTA_TRANSACTION_TYPE; +import static org.hibernate.cfg.PersistenceSettings.JPA_PERSISTENCE_PROVIDER; +import static org.hibernate.cfg.PersistenceSettings.JPA_TRANSACTION_TYPE; +import static org.hibernate.internal.log.DeprecationLogger.DEPRECATION_LOGGER; +import static org.hibernate.internal.util.StringHelper.isNotEmpty; import static org.hibernate.jpa.internal.JpaLogger.JPA_LOGGER; import static org.hibernate.internal.util.StringHelper.isEmpty; +import static org.hibernate.jpa.internal.util.ConfigurationHelper.overrideProperties; /** * Used by Hibernate to parse {@code persistence.xml} files in SE environments. @@ -95,17 +97,18 @@ private PersistenceXmlParser(Map integration, ClassLoader providedClassLoa providedClassLoaders.add( providedClassLoader ); } - @SuppressWarnings("unchecked") - final Collection classLoaders = - (Collection) integration.get( AvailableSettings.CLASSLOADERS ); + final var classLoaders = + (Collection) + integration.get( AvailableSettings.CLASSLOADERS ); if ( classLoaders != null ) { - providedClassLoaders.addAll( classLoaders ); + for ( var classLoader : classLoaders ) { + providedClassLoaders.add( (ClassLoader) classLoader ); + } } - classLoaderService = new ClassLoaderServiceImpl( - providedClassLoaders, - TcclLookupPrecedence.from( integration, TcclLookupPrecedence.AFTER ) - ); + classLoaderService = + new ClassLoaderServiceImpl( providedClassLoaders, + TcclLookupPrecedence.from( integration, TcclLookupPrecedence.AFTER ) ); } } @@ -162,28 +165,20 @@ private void parsePersistenceXml(Map persiste JPA_LOGGER.attemptingToParsePersistenceXml( xmlUrl.toExternalForm() ); } - final URL persistenceUnitRootUrl = ArchiveHelper.getJarURLFromURLEntry( xmlUrl, "/META-INF/persistence.xml" ); - - final JaxbPersistenceImpl jaxbPersistence = loadUrlWithJaxb( xmlUrl ); - final List jaxbPersistenceUnits = jaxbPersistence.getPersistenceUnit(); - - for ( int i = 0; i < jaxbPersistenceUnits.size(); i++ ) { - final JaxbPersistenceUnitImpl jaxbPersistenceUnit = jaxbPersistenceUnits.get( i ); - + final URL persistenceUnitRootUrl = getJarURLFromURLEntry( xmlUrl, "/META-INF/persistence.xml" ); + for ( var jaxbPersistenceUnit : loadUrlWithJaxb( xmlUrl ).getPersistenceUnit() ) { if ( persistenceUnits.containsKey( jaxbPersistenceUnit.getName() ) ) { JPA_LOGGER.duplicatedPersistenceUnitName( jaxbPersistenceUnit.getName() ); - continue; } - - final ParsedPersistenceXmlDescriptor persistenceUnitDescriptor = - new ParsedPersistenceXmlDescriptor( persistenceUnitRootUrl ); - bindPersistenceUnit( jaxbPersistenceUnit, persistenceUnitDescriptor ); - - // per JPA spec, any settings passed in to PersistenceProvider bootstrap methods should override - // values found in persistence.xml - applyIntegrationOverrides( integration, defaultTransactionType, persistenceUnitDescriptor ); - - persistenceUnits.put( persistenceUnitDescriptor.getName(), persistenceUnitDescriptor ); + else { + final var persistenceUnitDescriptor = + new ParsedPersistenceXmlDescriptor( persistenceUnitRootUrl ); + bindPersistenceUnit( jaxbPersistenceUnit, persistenceUnitDescriptor ); + // per JPA spec, any settings passed in to PersistenceProvider + // bootstrap methods should override values found in persistence.xml + applyIntegrationOverrides( integration, defaultTransactionType, persistenceUnitDescriptor ); + persistenceUnits.put( persistenceUnitDescriptor.getName(), persistenceUnitDescriptor ); + } } } @@ -191,7 +186,7 @@ private void bindPersistenceUnit( JaxbPersistenceUnitImpl jaxbPersistenceUnit, ParsedPersistenceXmlDescriptor persistenceUnitDescriptor) { final String name = jaxbPersistenceUnit.getName(); - if ( StringHelper.isNotEmpty( name ) ) { + if ( isNotEmpty( name ) ) { JPA_LOGGER.persistenceUnitNameFromXml( name ); persistenceUnitDescriptor.setName( name ); } @@ -208,21 +203,19 @@ private void bindPersistenceUnit( persistenceUnitDescriptor.addMappingFiles( jaxbPersistenceUnit.getMappingFiles() ); persistenceUnitDescriptor.addJarFileUrls( jaxbPersistenceUnit.getJarFiles() ); - final JaxbPropertiesImpl propertyContainer = jaxbPersistenceUnit.getPropertyContainer(); + final var propertyContainer = jaxbPersistenceUnit.getPropertyContainer(); if ( propertyContainer != null ) { - for ( JaxbPropertyImpl property : propertyContainer.getProperties() ) { + for ( var property : propertyContainer.getProperties() ) { persistenceUnitDescriptor.getProperties() .put( property.getName(), property.getValue() ); } } } - @SuppressWarnings("removal") private static void setTransactionType( JaxbPersistenceUnitImpl jaxbPersistenceUnit, ParsedPersistenceXmlDescriptor persistenceUnitDescriptor) { - final jakarta.persistence.spi.PersistenceUnitTransactionType transactionType = - jaxbPersistenceUnit.getTransactionType(); + final var transactionType = jaxbPersistenceUnit.getTransactionType(); if ( transactionType != null ) { persistenceUnitDescriptor.setTransactionType( PersistenceUnitTransactionTypeHelper.toNewForm( transactionType ) ); @@ -239,94 +232,81 @@ private boolean handleBoolean(Boolean incoming) { @SuppressWarnings("deprecation") private void applyIntegrationOverrides(Map integration, PersistenceUnitTransactionType defaultTransactionType, ParsedPersistenceXmlDescriptor persistenceUnitDescriptor) { - if ( integration.containsKey( AvailableSettings.JAKARTA_PERSISTENCE_PROVIDER ) ) { + if ( integration.containsKey( JAKARTA_PERSISTENCE_PROVIDER ) ) { persistenceUnitDescriptor.setProviderClassName( (String) - integration.get( AvailableSettings.JAKARTA_PERSISTENCE_PROVIDER ) ); + integration.get( JAKARTA_PERSISTENCE_PROVIDER ) ); } - else if ( integration.containsKey( AvailableSettings.JPA_PERSISTENCE_PROVIDER ) ) { - DeprecationLogger.DEPRECATION_LOGGER.deprecatedSetting( - AvailableSettings.JPA_PERSISTENCE_PROVIDER, - AvailableSettings.JAKARTA_PERSISTENCE_PROVIDER - ); + else if ( integration.containsKey( JPA_PERSISTENCE_PROVIDER ) ) { + DEPRECATION_LOGGER.deprecatedSetting( JPA_PERSISTENCE_PROVIDER, JAKARTA_PERSISTENCE_PROVIDER ); persistenceUnitDescriptor.setProviderClassName( (String) - integration.get( AvailableSettings.JPA_PERSISTENCE_PROVIDER ) ); + integration.get( JPA_PERSISTENCE_PROVIDER ) ); } - if ( integration.containsKey( AvailableSettings.JPA_TRANSACTION_TYPE ) ) { - DeprecationLogger.DEPRECATION_LOGGER.deprecatedSetting( - AvailableSettings.JPA_TRANSACTION_TYPE, - AvailableSettings.JAKARTA_TRANSACTION_TYPE - ); - String transactionType = (String) integration.get( AvailableSettings.JPA_TRANSACTION_TYPE ); + if ( integration.containsKey( JPA_TRANSACTION_TYPE ) ) { + DEPRECATION_LOGGER.deprecatedSetting( JPA_TRANSACTION_TYPE, JAKARTA_TRANSACTION_TYPE ); + String transactionType = (String) integration.get( JPA_TRANSACTION_TYPE ); persistenceUnitDescriptor.setTransactionType( parseTransactionType( transactionType ) ); } - else if ( integration.containsKey( AvailableSettings.JAKARTA_TRANSACTION_TYPE ) ) { - String transactionType = (String) integration.get( AvailableSettings.JAKARTA_TRANSACTION_TYPE ); + else if ( integration.containsKey( JAKARTA_TRANSACTION_TYPE ) ) { + String transactionType = (String) integration.get( JAKARTA_TRANSACTION_TYPE ); persistenceUnitDescriptor.setTransactionType( parseTransactionType( transactionType ) ); } - if ( integration.containsKey( AvailableSettings.JPA_JTA_DATASOURCE ) ) { - DeprecationLogger.DEPRECATION_LOGGER.deprecatedSetting( - AvailableSettings.JPA_JTA_DATASOURCE, - AvailableSettings.JAKARTA_JTA_DATASOURCE - ); - persistenceUnitDescriptor.setJtaDataSource( integration.get( AvailableSettings.JPA_JTA_DATASOURCE ) ); + if ( integration.containsKey( JPA_JTA_DATASOURCE ) ) { + DEPRECATION_LOGGER.deprecatedSetting( JPA_JTA_DATASOURCE, JAKARTA_JTA_DATASOURCE ); + persistenceUnitDescriptor.setJtaDataSource( integration.get( JPA_JTA_DATASOURCE ) ); } - else if ( integration.containsKey( AvailableSettings.JAKARTA_JTA_DATASOURCE ) ) { - persistenceUnitDescriptor.setJtaDataSource( integration.get( AvailableSettings.JAKARTA_JTA_DATASOURCE ) ); + else if ( integration.containsKey( JAKARTA_JTA_DATASOURCE ) ) { + persistenceUnitDescriptor.setJtaDataSource( integration.get( JAKARTA_JTA_DATASOURCE ) ); } - if ( integration.containsKey( AvailableSettings.JPA_NON_JTA_DATASOURCE ) ) { - DeprecationLogger.DEPRECATION_LOGGER.deprecatedSetting( - AvailableSettings.JPA_NON_JTA_DATASOURCE, - AvailableSettings.JAKARTA_NON_JTA_DATASOURCE - ); - persistenceUnitDescriptor.setNonJtaDataSource( integration.get( AvailableSettings.JPA_NON_JTA_DATASOURCE ) ); + if ( integration.containsKey( JPA_NON_JTA_DATASOURCE ) ) { + DEPRECATION_LOGGER.deprecatedSetting( JPA_NON_JTA_DATASOURCE, JAKARTA_NON_JTA_DATASOURCE ); + persistenceUnitDescriptor.setNonJtaDataSource( integration.get( JPA_NON_JTA_DATASOURCE ) ); } - else if ( integration.containsKey( AvailableSettings.JAKARTA_NON_JTA_DATASOURCE ) ) { - persistenceUnitDescriptor.setNonJtaDataSource( integration.get( AvailableSettings.JAKARTA_NON_JTA_DATASOURCE ) ); + else if ( integration.containsKey( JAKARTA_NON_JTA_DATASOURCE ) ) { + persistenceUnitDescriptor.setNonJtaDataSource( integration.get( JAKARTA_NON_JTA_DATASOURCE ) ); } applyTransactionTypeOverride( persistenceUnitDescriptor, defaultTransactionType ); - final Properties properties = persistenceUnitDescriptor.getProperties(); - ConfigurationHelper.overrideProperties( properties, integration ); + overrideProperties( persistenceUnitDescriptor.getProperties(), integration ); } - private void applyTransactionTypeOverride(ParsedPersistenceXmlDescriptor persistenceUnitDescriptor, + private void applyTransactionTypeOverride( + ParsedPersistenceXmlDescriptor persistenceUnitDescriptor, PersistenceUnitTransactionType defaultTransactionType) { - // if transaction type is set already, use that value + // if the transaction type is set already, use that value if ( persistenceUnitDescriptor.getPersistenceUnitTransactionType() == null ) { - // else - // if JTA DS - // use JTA - // else if NOT JTA DS - // use RESOURCE_LOCAL - // else - // use defaultTransactionType - if ( persistenceUnitDescriptor.getJtaDataSource() != null ) { - persistenceUnitDescriptor.setTransactionType( JTA ); - } - else if ( persistenceUnitDescriptor.getNonJtaDataSource() != null ) { - persistenceUnitDescriptor.setTransactionType( RESOURCE_LOCAL ); - } - else { - persistenceUnitDescriptor.setTransactionType( defaultTransactionType ); - } + persistenceUnitDescriptor.setTransactionType( + determineTransactionType( persistenceUnitDescriptor, defaultTransactionType ) ); } } - private static PersistenceUnitTransactionType parseTransactionType(String value) { - if ( isEmpty( value ) ) { - return null; - } - else if ( JTA.name().equalsIgnoreCase( value ) ) { + private static PersistenceUnitTransactionType determineTransactionType( + ParsedPersistenceXmlDescriptor persistenceUnitDescriptor, + PersistenceUnitTransactionType defaultTransactionType) { + if ( persistenceUnitDescriptor.getJtaDataSource() != null ) { return JTA; } - else if ( RESOURCE_LOCAL.name().equalsIgnoreCase( value ) ) { + else if ( persistenceUnitDescriptor.getNonJtaDataSource() != null ) { return RESOURCE_LOCAL; } else { + return defaultTransactionType; + } + } + + private static PersistenceUnitTransactionType parseTransactionType(String value) { + if ( isEmpty( value ) ) { + return null; + } + else { + for ( var transactionType : PersistenceUnitTransactionType.values() ) { + if ( transactionType.name().equalsIgnoreCase( value ) ) { + return transactionType; + } + } throw new PersistenceException( "Unknown persistence unit transaction type : " + value ); } } @@ -334,16 +314,14 @@ else if ( RESOURCE_LOCAL.name().equalsIgnoreCase( value ) ) { private JaxbPersistenceImpl loadUrlWithJaxb(URL xmlUrl) { final String resourceName = xmlUrl.toExternalForm(); try { - URLConnection conn = xmlUrl.openConnection(); + var connection = xmlUrl.openConnection(); // avoid JAR locking on Windows and Tomcat - conn.setUseCaches( false ); - - try ( InputStream inputStream = conn.getInputStream() ) { - final StreamSource inputSource = new StreamSource( inputStream ); - final ConfigurationBinder configurationBinder = new ConfigurationBinder( classLoaderService ); - final Binding binding = - configurationBinder.bind( inputSource, new Origin( SourceType.URL, resourceName ) ); - return binding.getRoot(); + connection.setUseCaches( false ); + try ( var inputStream = connection.getInputStream() ) { + return new ConfigurationBinder( classLoaderService ) + .bind( new StreamSource( inputStream ), + new Origin( SourceType.URL, resourceName ) ) + .getRoot(); } catch (IOException e) { throw new PersistenceException( "Unable to obtain input stream from [" + resourceName + "]", e ); diff --git a/hibernate-core/src/main/java/org/hibernate/jpa/boot/spi/ProviderChecker.java b/hibernate-core/src/main/java/org/hibernate/jpa/boot/spi/ProviderChecker.java index 436f8d95f4ac..20e9714d5a29 100644 --- a/hibernate-core/src/main/java/org/hibernate/jpa/boot/spi/ProviderChecker.java +++ b/hibernate-core/src/main/java/org/hibernate/jpa/boot/spi/ProviderChecker.java @@ -26,7 +26,7 @@ public final class ProviderChecker { /** * Does the descriptor and/or integration request Hibernate as the * {@link jakarta.persistence.spi.PersistenceProvider}? - *

    + *

    * Note that in the case of no requested provider being named, we * assume we are the provider. (The calls got to us somehow...) * diff --git a/hibernate-core/src/main/java/org/hibernate/jpa/internal/JpaLogger.java b/hibernate-core/src/main/java/org/hibernate/jpa/internal/JpaLogger.java index 9024e217f5bc..5c6a96c00bca 100644 --- a/hibernate-core/src/main/java/org/hibernate/jpa/internal/JpaLogger.java +++ b/hibernate-core/src/main/java/org/hibernate/jpa/internal/JpaLogger.java @@ -16,6 +16,7 @@ import org.jboss.logging.annotations.ValidIdRange; import java.lang.invoke.MethodHandles; +import java.util.Locale; import static org.jboss.logging.Logger.Level.DEBUG; import static org.jboss.logging.Logger.Level.INFO; @@ -36,7 +37,7 @@ public interface JpaLogger extends BasicLogger { String NAME = SubSystemLogging.BASE + ".jpa"; - JpaLogger JPA_LOGGER = Logger.getMessageLogger( MethodHandles.lookup(), JpaLogger.class, NAME ); + JpaLogger JPA_LOGGER = Logger.getMessageLogger( MethodHandles.lookup(), JpaLogger.class, NAME, Locale.ROOT ); @LogMessage(level = WARN) @Message(value = "Defining %s=true ignored in HEM", id = 8059) diff --git a/hibernate-core/src/main/java/org/hibernate/jpa/spi/JpaCompliance.java b/hibernate-core/src/main/java/org/hibernate/jpa/spi/JpaCompliance.java index 630800df6e7b..af90dae005bc 100644 --- a/hibernate-core/src/main/java/org/hibernate/jpa/spi/JpaCompliance.java +++ b/hibernate-core/src/main/java/org/hibernate/jpa/spi/JpaCompliance.java @@ -123,7 +123,7 @@ public interface JpaCompliance { * {@link jakarta.persistence.EntityManager#find} should be exactly the * expected type, allowing no type coercion. *

    - * Historically, Hibernate behaved the same way. Since 6.0 however, + * Historically, Hibernate behaved the same way. Since 6.0, however, * Hibernate has the ability to coerce the passed type to the expected * type. For example, an {@link Integer} may be widened to {@link Long}. * Coercion is performed by calling diff --git a/hibernate-core/src/main/java/org/hibernate/jpa/spi/NativeQueryConstructorTransformer.java b/hibernate-core/src/main/java/org/hibernate/jpa/spi/NativeQueryConstructorTransformer.java index f65b2bcb84c3..69448a111305 100644 --- a/hibernate-core/src/main/java/org/hibernate/jpa/spi/NativeQueryConstructorTransformer.java +++ b/hibernate-core/src/main/java/org/hibernate/jpa/spi/NativeQueryConstructorTransformer.java @@ -4,11 +4,11 @@ */ package org.hibernate.jpa.spi; +import java.lang.reflect.Constructor; + import org.hibernate.InstantiationException; import org.hibernate.query.TupleTransformer; -import java.lang.reflect.Constructor; - /** * A {@link TupleTransformer} which packages each native query result in * an instance of the result class by calling an appropriate constructor. @@ -26,6 +26,24 @@ public class NativeQueryConstructorTransformer implements TupleTransformer private final Class resultClass; private transient Constructor constructor; + public NativeQueryConstructorTransformer(Class resultClass) { + this.resultClass = resultClass; + } + + private static String constructorSignature(Constructor ctor) { + final var signature = new StringBuilder(); + signature.append( ctor.getDeclaringClass().getSimpleName() ).append( "(" ); + final var params = ctor.getParameterTypes(); + for ( int i = 0; i < params.length; i++ ) { + if ( i > 0 ) { + signature.append( ", " ); + } + signature.append( params[i].getSimpleName() ); + } + signature.append( ")" ); + return signature.toString(); + } + private Constructor constructor(Object[] elements) { if ( constructor == null ) { try { @@ -33,8 +51,8 @@ private Constructor constructor(Object[] elements) { // of the constructor we're looking for, so we need // to do something a bit weird here: match on just // the number of parameters - for ( final Constructor candidate : resultClass.getDeclaredConstructors() ) { - final Class[] parameterTypes = candidate.getParameterTypes(); + for ( final var candidate : resultClass.getDeclaredConstructors() ) { + final var parameterTypes = candidate.getParameterTypes(); if ( parameterTypes.length == elements.length ) { // found a candidate with the right number // of parameters @@ -55,24 +73,53 @@ private Constructor constructor(Object[] elements) { throw new InstantiationException( "Cannot instantiate query result type", resultClass, e ); } if ( constructor == null ) { - throw new InstantiationException( "Result class must have a single constructor with exactly " - + elements.length + " parameters", resultClass ); + final var message = new StringBuilder(); + message.append( "Result class" ) + .append( " must have exactly one constructor with " ) + .append( elements.length ) + .append( " parameters - found ['" ); + boolean first = true; + for ( var c : resultClass.getDeclaredConstructors() ) { + if ( c.getParameterCount() == elements.length ) { + if ( !first ) { + message.append( "', '" ); + } + message.append( constructorSignature( c ) ); + first = false; + } + } + + message.append( "'] in" ); + + throw new InstantiationException( message.toString(), resultClass ); } } return constructor; } - public NativeQueryConstructorTransformer(Class resultClass) { - this.resultClass = resultClass; - } - @Override public T transformTuple(Object[] tuple, String[] aliases) { + final var ctor = constructor( tuple ); try { - return constructor( tuple ).newInstance( tuple ); + return ctor.newInstance( tuple ); } catch (Exception e) { - throw new InstantiationException( "Cannot instantiate query result type", resultClass, e ); + final var message = new StringBuilder(); + message.append( "Could not instantiate query result type - expected '" ) + .append( constructorSignature( ctor ) ) + .append( "' but found '" ) + .append( ctor.getDeclaringClass().getSimpleName() ) + .append( '(' ); + + for ( int i = 0; i < tuple.length; i++ ) { + final Object value = tuple[i]; + if ( i > 0 ) { + message.append( ", " ); + } + message.append( value == null ? "null" : value.getClass().getSimpleName() ); + } + message.append( ")' in" ); + throw new InstantiationException( message.toString(), resultClass, e ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/jpa/spi/NativeQueryTupleTransformer.java b/hibernate-core/src/main/java/org/hibernate/jpa/spi/NativeQueryTupleTransformer.java index 2cb5490e63ab..5d1d37eec6e5 100644 --- a/hibernate-core/src/main/java/org/hibernate/jpa/spi/NativeQueryTupleTransformer.java +++ b/hibernate-core/src/main/java/org/hibernate/jpa/spi/NativeQueryTupleTransformer.java @@ -9,6 +9,8 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; + import jakarta.persistence.Tuple; import jakarta.persistence.TupleElement; @@ -134,6 +136,19 @@ public String toString() { return Arrays.toString( tuple ); } + + @Override + public boolean equals(Object obj) { + return obj instanceof NativeTupleImpl that + && Objects.equals( this.aliasToValue, that.aliasToValue ); + } + + @Override + public int hashCode() { + return Arrays.hashCode( tuple ); + } + + @Override public List> getElements() { final List> elements = new ArrayList<>( size ); diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/AbstractMultiIdEntityLoader.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/AbstractMultiIdEntityLoader.java index 24e8f5f49fbb..d32aa47b0929 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/AbstractMultiIdEntityLoader.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/AbstractMultiIdEntityLoader.java @@ -20,6 +20,7 @@ import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.sql.ast.SqlAstTranslatorFactory; import org.hibernate.sql.exec.spi.JdbcSelectExecutor; +import org.hibernate.type.descriptor.java.JavaType; import java.util.ArrayList; import java.util.List; @@ -39,11 +40,17 @@ public abstract class AbstractMultiIdEntityLoader implements MultiIdEntityLoa private final EntityMappingType entityDescriptor; private final SessionFactoryImplementor sessionFactory; protected final EntityIdentifierMapping identifierMapping; + private final boolean idCoercionEnabled; public AbstractMultiIdEntityLoader(EntityMappingType entityDescriptor, SessionFactoryImplementor sessionFactory) { this.entityDescriptor = entityDescriptor; this.sessionFactory = sessionFactory; identifierMapping = getLoadable().getIdentifierMapping(); + idCoercionEnabled = + !sessionFactory.getSessionFactoryOptions() + .getJpaCompliance().isLoadByIdComplianceEnabled() + // special handling for entity with @IdClass + && !entityDescriptor.getIdentifierMapping().isVirtual(); } protected EntityMappingType getEntityDescriptor() { @@ -111,8 +118,9 @@ private List orderedMultiLoad( Object[] ids, MultiIdLoadOptions loadOptions, SharedSessionContractImplementor session) { - final boolean idCoercionEnabled = isIdCoercionEnabled(); - final var idType = getLoadable().getIdentifierMapping().getJavaType(); + final var loadable = getLoadable(); + final var persister = loadable.getEntityPersister(); + final var idType = loadable.getIdentifierMapping().getJavaType(); final int maxBatchSize = maxBatchSize( ids, loadOptions ); @@ -124,9 +132,9 @@ private List orderedMultiLoad( final var lockOptions = lockOptions( loadOptions ); for ( int i = 0; i < ids.length; i++ ) { - final Object id = idCoercionEnabled ? idType.coerce( ids[i], session ) : ids[i]; - final var entityKey = new EntityKey( id, getLoadable().getEntityPersister() ); - if ( !loadFromEnabledCaches( loadOptions, session, id, lockOptions, entityKey, results, i ) ) { + final Object id = coerce( idType, ids[i] ); + final var entityKey = new EntityKey( id, persister ); + if ( !loadFromEnabledCaches( loadOptions, session, lockOptions, entityKey, results, i ) ) { // if we did not hit any of the continues above, // then we need to batch load the entity state. idsInBatch.add( id ); @@ -154,10 +162,13 @@ private List orderedMultiLoad( return (List) results; } + private Object coerce(JavaType idType, Object id) { + return idCoercionEnabled ? idType.coerce( id ) : id; + } + private static LockOptions lockOptions(MultiIdLoadOptions loadOptions) { - return loadOptions.getLockOptions() == null - ? new LockOptions( LockMode.NONE ) - : loadOptions.getLockOptions(); + final var lockOptions = loadOptions.getLockOptions(); + return lockOptions == null ? new LockOptions( LockMode.NONE ) : lockOptions; } protected abstract int maxBatchSize(Object[] ids, MultiIdLoadOptions loadOptions); @@ -169,21 +180,19 @@ private void handleResults( List results) { final var persistenceContext = session.getPersistenceContext(); for ( Integer position : elementPositionsLoadedByBatch ) { - // the element value at this position in the results List should be - // the EntityKey for that entity - reuse it + // the element value at this position in the results List + // should be the EntityKey for that entity - reuse it final var entityKey = (EntityKey) results.get( position ); - session.getPersistenceContextInternal().getBatchFetchQueue().removeBatchLoadableEntityKey( entityKey ); + session.getPersistenceContextInternal().getBatchFetchQueue() + .removeBatchLoadableEntityKey( entityKey ); final Object entity = persistenceContext.getEntity( entityKey ); - final Object result; - if ( entity == null - // the entity is locally deleted, and the options ask that we not return such entities - || loadOptions.getRemovalsMode() == RemovalsMode.REPLACE - && persistenceContext.getEntry( entity ).getStatus().isDeletedOrGone() ) { - result = null; - } - else { - result = persistenceContext.proxyFor( entity ); - } + final Object result = + entity == null + // the entity is locally deleted, and the options ask that we not return such entities + || loadOptions.getRemovalsMode() == RemovalsMode.REPLACE + && persistenceContext.getEntry( entity ).getStatus().isDeletedOrGone() + ? null + : persistenceContext.proxyFor( entity ); results.set( position, result ); } } @@ -198,12 +207,11 @@ protected abstract void loadEntitiesById( private boolean loadFromEnabledCaches( MultiIdLoadOptions loadOptions, SharedSessionContractImplementor session, - Object id, LockOptions lockOptions, EntityKey entityKey, List result, int i) { - return (loadOptions.getSessionCheckMode() == SessionCheckMode.ENABLED + return ( loadOptions.getSessionCheckMode() == SessionCheckMode.ENABLED || loadOptions.isSecondLevelCacheCheckingEnabled() ) && isLoadFromCaches( loadOptions, entityKey, lockOptions, result, i, session ); } @@ -214,21 +222,22 @@ private boolean isLoadFromCaches( LockOptions lockOptions, List results, int i, SharedSessionContractImplementor session) { - if ( loadOptions.getSessionCheckMode() == SessionCheckMode.ENABLED ) { + final var removalsMode = loadOptions.getRemovalsMode(); + if ( removalsMode == RemovalsMode.EXCLUDE ) { + // note, this method is only called from orderedMultiLoad() + throw new IllegalArgumentException( "RemovalsMode.EXCLUDE is incompatible with OrderingMode.ORDERED" ); + } // look for it in the Session first final var entry = loadFromSessionCache( entityKey, lockOptions, GET, session ); final Object entity = entry.entity(); if ( entity != null ) { // put a null in the results - final Object result; - if ( loadOptions.getRemovalsMode() == RemovalsMode.INCLUDE - || entry.isManaged() ) { - result = entity; - } - else { - result = null; - } + final Object result = + loadOptions.getRemovalsMode() == RemovalsMode.INCLUDE + || entry.isManaged() + ? entity + : null; results.add( i, result ); return true; } @@ -292,7 +301,8 @@ private Object[] resolveInCachesIfEnabled( @NonNull LockOptions lockOptions, SharedSessionContractImplementor session, ResolutionConsumer resolutionConsumer) { - return loadOptions.getSessionCheckMode() == SessionCheckMode.ENABLED || loadOptions.isSecondLevelCacheCheckingEnabled() + return loadOptions.getSessionCheckMode() == SessionCheckMode.ENABLED + || loadOptions.isSecondLevelCacheCheckingEnabled() // the user requested that we exclude ids corresponding to already managed // entities from the generated load SQL. So here we will iterate all // incoming id values and see whether it corresponds to an existing @@ -330,18 +340,19 @@ private List unresolvedIds( LockOptions lockOptions, SharedSessionContractImplementor session, ResolutionConsumer resolutionConsumer) { - final boolean idCoercionEnabled = isIdCoercionEnabled(); - final var idType = getLoadable().getIdentifierMapping().getJavaType(); + final var loadable = getLoadable(); + final var persister = loadable.getEntityPersister(); + final var idType = loadable.getIdentifierMapping().getJavaType(); List unresolvedIds = null; for ( int i = 0; i < ids.length; i++ ) { - final Object id = idCoercionEnabled ? idType.coerce( ids[i], session ) : ids[i]; + final Object id = coerce( idType, ids[i] ); unresolvedIds = loadFromCaches( loadOptions, lockOptions, resolutionConsumer, id, - new EntityKey( id, getLoadable().getEntityPersister() ), + new EntityKey( id, persister ), unresolvedIds, i, session @@ -353,10 +364,6 @@ private List unresolvedIds( // Depending on the implementation, a specific subtype of Object[] (e.g. Integer[]) may be needed. protected abstract Object[] toIdArray(List ids); - private boolean isIdCoercionEnabled() { - return !getSessionFactory().getSessionFactoryOptions().getJpaCompliance().isLoadByIdComplianceEnabled(); - } - public interface ResolutionConsumer { void consume(int position, EntityKey entityKey, T resolvedRef); } @@ -373,13 +380,16 @@ private List loadFromCaches( // look for it in the Session first final var entry = loadFromSessionCache( entityKey, lockOptions, GET, session ); final Object sessionEntity; - if ( loadOptions.getSessionCheckMode() == SessionCheckMode.ENABLED ) { + if ( loadOptions.getSessionCheckMode() == SessionCheckMode.ENABLED ) { sessionEntity = entry.entity(); - if ( sessionEntity != null - && loadOptions.getRemovalsMode() == RemovalsMode.REPLACE - && !entry.isManaged() ) { - resolutionConsumer.consume( i, entityKey, null ); - return unresolvedIds; + if ( sessionEntity != null && !entry.isManaged() ) { + switch ( loadOptions.getRemovalsMode() ) { + case REPLACE : + resolutionConsumer.consume( i, entityKey, null ); + return unresolvedIds; + case EXCLUDE: + return unresolvedIds; + } } } else { @@ -387,7 +397,8 @@ private List loadFromCaches( } final Object cachedEntity = - sessionEntity == null && loadOptions.isSecondLevelCacheCheckingEnabled() + sessionEntity == null + && loadOptions.isSecondLevelCacheCheckingEnabled() ? loadFromSecondLevelCache( entityKey, lockOptions, session ) : sessionEntity; @@ -404,8 +415,11 @@ private List loadFromCaches( return unresolvedIds; } - private Object loadFromSecondLevelCache(EntityKey entityKey, LockOptions lockOptions, SharedSessionContractImplementor session) { - final var persister = getLoadable().getEntityPersister(); - return session.loadFromSecondLevelCache( persister, entityKey, null, lockOptions.getLockMode() ); + private Object loadFromSecondLevelCache( + EntityKey entityKey, + LockOptions lockOptions, + SharedSessionContractImplementor session) { + return session.loadFromSecondLevelCache( getLoadable().getEntityPersister(), + entityKey, null, lockOptions.getLockMode() ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/AbstractMultiNaturalIdLoader.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/AbstractMultiNaturalIdLoader.java index 8ac139d2d38d..f3dee6a789d0 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/AbstractMultiNaturalIdLoader.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/AbstractMultiNaturalIdLoader.java @@ -129,7 +129,7 @@ private List sortResults( return results; } - private Object entityForNaturalId(PersistenceContext context, K naturalId) { + private Object entityForNaturalId(PersistenceContext context, Object naturalId) { final var descriptor = getEntityDescriptor(); final Object id = context.getNaturalIdResolutions().findCachedIdByNaturalId( naturalId, descriptor ); // id can be null if a non-existent natural id is requested, or a mutable natural id was changed and then deleted @@ -142,20 +142,26 @@ private Object[] checkPersistenceContextForCachedResults( SharedSessionContractImplementor session, LockOptions lockOptions, Consumer results ) { + final var removalsMode = loadOptions.getRemovalsMode(); + if ( removalsMode == RemovalsMode.EXCLUDE + && loadOptions.getOrderingMode() == OrderingMode.ORDERED ) { + throw new IllegalArgumentException( "RemovalsMode.EXCLUDE is incompatible with OrderingMode.ORDERED" ); + } final List unresolvedIds = arrayList( naturalIds.length ); final var context = session.getPersistenceContextInternal(); - final var naturalIdMapping = getEntityDescriptor().getNaturalIdMapping(); for ( K naturalId : naturalIds ) { - final Object entity = entityForNaturalId( context, naturalIdMapping.normalizeInput( naturalId ) ); + final Object entity = entityForNaturalId( context, naturalId ); if ( entity != null ) { // Entity is already in the persistence context final var entry = context.getEntry( entity ); - if ( loadOptions.getRemovalsMode() == RemovalsMode.INCLUDE + if ( removalsMode == RemovalsMode.INCLUDE || !entry.getStatus().isDeletedOrGone() ) { // either a managed entry, or a deleted one with returnDeleted enabled upgradeLock( entity, entry, lockOptions, session ); - final Object result = context.proxyFor( entity ); - results.accept( (E) result ); + if ( removalsMode != RemovalsMode.EXCLUDE ) { + final Object result = context.proxyFor( entity ); + results.accept( (E) result ); + } } } else { diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/AbstractNaturalIdLoader.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/AbstractNaturalIdLoader.java index 600074f7b0e3..2baec15775f8 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/AbstractNaturalIdLoader.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/AbstractNaturalIdLoader.java @@ -5,7 +5,10 @@ package org.hibernate.loader.ast.internal; import org.hibernate.HibernateException; +import org.hibernate.LockMode; import org.hibernate.LockOptions; +import org.hibernate.Locking; +import org.hibernate.Timeouts; import org.hibernate.engine.spi.LoadQueryInfluencers; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor; @@ -32,7 +35,6 @@ import org.hibernate.sql.ast.tree.select.SelectStatement; import org.hibernate.sql.exec.internal.BaseExecutionContext; import org.hibernate.sql.exec.internal.CallbackImpl; -import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; import org.hibernate.sql.exec.internal.JdbcParameterBindingImpl; import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl; import org.hibernate.sql.exec.internal.SqlTypedMappingJdbcParameter; @@ -40,6 +42,7 @@ import org.hibernate.sql.exec.spi.JdbcParameterBinding; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.exec.spi.JdbcParametersList; +import org.hibernate.sql.exec.spi.JdbcSelect; import org.hibernate.sql.results.graph.internal.ImmutableFetchList; import org.hibernate.sql.results.internal.RowTransformerSingularReturnImpl; import org.hibernate.sql.results.spi.ListResultsConsumer; @@ -86,13 +89,28 @@ public EntityMappingType getLoadable() { } @Override - public T load(Object naturalIdValue, NaturalIdLoadOptions options, SharedSessionContractImplementor session) { - final var factory = session.getFactory(); + public T load(Object naturalIdToLoad, Options options, SharedSessionContractImplementor session) { + final var lockOptions = makeLockOptions( options ); + return load( naturalIdToLoad, lockOptions, session ); + } + + private LockOptions makeLockOptions(Options options) { + if ( options.getLockMode() == null || options.getLockMode() == LockMode.NONE ) { + return LockOptions.NONE; + } + if ( options.getLockMode() == LockMode.READ ) { + return LockOptions.READ; + } + + final var lockOptions = new LockOptions( options.getLockMode() ); + lockOptions.setScope( options.getLockScope() != null ? options.getLockScope() : Locking.Scope.ROOT_ONLY ); + lockOptions.setTimeout( options.getLockTimeout() != null ? options.getLockTimeout() : Timeouts.WAIT_FOREVER ); + lockOptions.setFollowOnStrategy( options.getLockFollowOn() != null ? options.getLockFollowOn() : Locking.FollowOn.ALLOW ); + return lockOptions; + } - final var lockOptions = - options.getLockOptions() == null - ? new LockOptions() - : options.getLockOptions(); + private T load(Object naturalIdValue, LockOptions lockOptions, SharedSessionContractImplementor session) { + final var factory = session.getFactory(); final var sqlSelect = LoaderSelectBuilder.createSelect( getLoadable(), @@ -104,6 +122,7 @@ public T load(Object naturalIdValue, NaturalIdLoadOptions options, SharedSession session.getLoadQueryInfluencers(), lockOptions, JdbcParametersList.newBuilder()::add, + new SqlAliasBaseManager(), factory ); @@ -128,6 +147,14 @@ public T load(Object naturalIdValue, NaturalIdLoadOptions options, SharedSession ); } + @Override + public T load(Object naturalIdValue, NaturalIdLoadOptions options, SharedSessionContractImplementor session) { + final var lockOptions = options.getLockOptions() == null + ? new LockOptions() + : options.getLockOptions(); + return load( naturalIdValue, lockOptions, session ); + } + /** * Apply restriction necessary to match the given natural-id value. * Should also apply any predicates to the predicate consumer and @@ -148,9 +175,10 @@ protected Expression resolveColumnReference( TableGroup rootTableGroup, SelectableMapping selectableMapping, SqlExpressionResolver sqlExpressionResolver) { - final var tableReference = - rootTableGroup.getTableReference( rootTableGroup.getNavigablePath(), - selectableMapping.getContainingTableExpression() ); + final var tableReference = rootTableGroup.getTableReference( + rootTableGroup.getNavigablePath(), + selectableMapping.getContainingTableExpression() + ); if ( tableReference == null ) { throw new IllegalStateException( String.format( @@ -179,17 +207,16 @@ public Object resolveNaturalIdToId(Object naturalIdValue, SharedSessionContractI final var entityPath = new NavigablePath( entityDescriptor.getRootPathName() ); final var rootQuerySpec = new QuerySpec( true ); - final var sqlAstCreationState = - new LoaderSqlAstCreationState( - rootQuerySpec, - new SqlAliasBaseManager(), - new SimpleFromClauseAccessImpl(), - LockOptions.NONE, - (fetchParent, creationState) -> ImmutableFetchList.EMPTY, - true, - new LoadQueryInfluencers( factory ), - factory.getSqlTranslationEngine() - ); + final var sqlAstCreationState = new LoaderSqlAstCreationState( + rootQuerySpec, + new SqlAliasBaseManager(), + new SimpleFromClauseAccessImpl(), + LockOptions.NONE, + (fetchParent, creationState) -> ImmutableFetchList.EMPTY, + true, + new LoadQueryInfluencers( factory ), + factory.getSqlTranslationEngine() + ); final var rootTableGroup = entityDescriptor.createRootTableGroup( true, @@ -231,12 +258,13 @@ protected R executeNaturalIdQuery( Consumer predicateConsumer, LoaderSqlAstCreationState sqlAstCreationState, SharedSessionContractImplementor session) { + assert naturalIdMapping.isNormalized( naturalIdValue ); + final var factory = session.getFactory(); - final var bindings = - new JdbcParameterBindingsImpl( naturalIdMapping.getJdbcTypeCount() ); + final var bindings = new JdbcParameterBindingsImpl( naturalIdMapping.getJdbcTypeCount() ); applyNaturalIdRestriction( - naturalIdMapping().normalizeInput( naturalIdValue ), + naturalIdValue, rootTableGroup, predicateConsumer, bindings::addBinding, @@ -282,7 +310,7 @@ protected R executeNaturalIdQuery( }; } - private static JdbcOperationQuerySelect createJdbcOperationQuerySelect( + private static JdbcSelect createJdbcOperationQuerySelect( SelectStatement sqlSelect, SessionFactoryImplementor factory, JdbcParameterBindings bindings, @@ -308,6 +336,7 @@ public Object resolveIdToNaturalId(Object id, SharedSessionContractImplementor s session.getLoadQueryInfluencers(), new LockOptions(), builder::add, + new SqlAliasBaseManager(), factory ); final var jdbcParameters = builder.build(); diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionBatchLoaderArrayParam.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionBatchLoaderArrayParam.java index 3045c41a60d5..614838b1d353 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionBatchLoaderArrayParam.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionBatchLoaderArrayParam.java @@ -20,14 +20,15 @@ import org.hibernate.metamodel.mapping.SqlTypedMapping; import org.hibernate.metamodel.mapping.internal.SqlTypedMappingImpl; import org.hibernate.query.spi.QueryOptions; +import org.hibernate.sql.ast.spi.SqlAliasBaseManager; import org.hibernate.sql.ast.tree.expression.JdbcParameter; import org.hibernate.sql.ast.tree.select.SelectStatement; import org.hibernate.sql.exec.internal.JdbcParameterBindingImpl; import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl; import org.hibernate.sql.exec.internal.SqlTypedMappingJdbcParameter; -import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.exec.spi.JdbcParametersList; +import org.hibernate.sql.exec.spi.JdbcSelect; import org.hibernate.sql.results.internal.RowTransformerStandardImpl; import org.hibernate.sql.results.spi.ListResultsConsumer; @@ -48,7 +49,7 @@ public class CollectionBatchLoaderArrayParam private final SqlTypedMapping arraySqlTypedMapping; private final JdbcParameter jdbcParameter; private final SelectStatement sqlSelect; - private final JdbcOperationQuerySelect jdbcSelectOperation; + private final JdbcSelect jdbcSelectOperation; public CollectionBatchLoaderArrayParam( int domainBatchSize, @@ -82,6 +83,8 @@ public CollectionBatchLoaderArrayParam( ) ); + final var sqlAliasBaseGenerator = new SqlAliasBaseManager(); + jdbcParameter = new SqlTypedMappingJdbcParameter( arraySqlTypedMapping ); sqlSelect = LoaderSelectBuilder.createSelectBySingleArrayParameter( getLoadable(), @@ -89,12 +92,18 @@ public CollectionBatchLoaderArrayParam( getInfluencers(), new LockOptions(), jdbcParameter, + sqlAliasBaseGenerator, getSessionFactory() ); final var querySpec = sqlSelect.getQueryPart().getFirstQuerySpec(); final var tableGroup = querySpec.getFromClause().getRoots().get( 0 ); - attributeMapping.applySoftDeleteRestrictions( tableGroup, querySpec::applyPredicate ); + attributeMapping.applyAuxiliaryRestrictions( + tableGroup, + querySpec::applyPredicate, + getInfluencers(), + sqlAliasBaseGenerator + ); jdbcSelectOperation = getSessionFactory().getJdbcServices() .getJdbcEnvironment() diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionBatchLoaderInPredicate.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionBatchLoaderInPredicate.java index efea2c40a173..3e62b690fa42 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionBatchLoaderInPredicate.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionBatchLoaderInPredicate.java @@ -13,9 +13,10 @@ import org.hibernate.loader.ast.spi.SqlArrayMultiKeyLoader; import org.hibernate.metamodel.mapping.PluralAttributeMapping; import org.hibernate.query.spi.QueryOptions; +import org.hibernate.sql.ast.spi.SqlAliasBaseManager; import org.hibernate.sql.ast.tree.select.SelectStatement; -import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParametersList; +import org.hibernate.sql.exec.spi.JdbcSelect; import static org.hibernate.loader.ast.internal.MultiKeyLoadHelper.countIds; import static org.hibernate.loader.ast.internal.MultiKeyLoadLogging.MULTI_KEY_LOAD_LOGGER; @@ -34,7 +35,7 @@ public class CollectionBatchLoaderInPredicate private final int sqlBatchSize; private final JdbcParametersList jdbcParameters; private final SelectStatement sqlAst; - private final JdbcOperationQuerySelect jdbcSelect; + private final JdbcSelect jdbcSelect; public CollectionBatchLoaderInPredicate( int domainBatchSize, @@ -56,8 +57,10 @@ public CollectionBatchLoaderInPredicate( ); } + final var sqlAliasBaseGenerator = new SqlAliasBaseManager(); + final var jdbcParametersBuilder = JdbcParametersList.newBuilder(); - this.sqlAst = LoaderSelectBuilder.createSelect( + sqlAst = LoaderSelectBuilder.createSelect( attributeMapping, null, attributeMapping.getKeyDescriptor(), @@ -66,12 +69,18 @@ public CollectionBatchLoaderInPredicate( influencers, new LockOptions(), jdbcParametersBuilder::add, + sqlAliasBaseGenerator, sessionFactory ); final var querySpec = sqlAst.getQueryPart().getFirstQuerySpec(); final var tableGroup = querySpec.getFromClause().getRoots().get( 0 ); - attributeMapping.applySoftDeleteRestrictions( tableGroup, querySpec::applyPredicate ); + attributeMapping.applyAuxiliaryRestrictions( + tableGroup, + querySpec::applyPredicate, + influencers, + sqlAliasBaseGenerator + ); jdbcParameters = jdbcParametersBuilder.build(); assert jdbcParameters.size() == sqlBatchSize * keyColumnCount; diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionElementLoaderByIndex.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionElementLoaderByIndex.java index b37cfe985b56..a3f85a19b9e6 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionElementLoaderByIndex.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionElementLoaderByIndex.java @@ -16,6 +16,7 @@ import org.hibernate.metamodel.mapping.PluralAttributeMapping; import org.hibernate.metamodel.mapping.internal.EntityCollectionPart; import org.hibernate.query.spi.QueryOptions; +import org.hibernate.sql.ast.spi.SqlAliasBaseManager; import org.hibernate.sql.ast.tree.select.SelectStatement; import org.hibernate.sql.exec.internal.BaseExecutionContext; import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl; @@ -87,6 +88,7 @@ public CollectionElementLoaderByIndex( influencers, new LockOptions(), jdbcParametersBuilder::add, + new SqlAliasBaseManager(), sessionFactory ); jdbcParameters = jdbcParametersBuilder.build(); diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionLoaderSingleKey.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionLoaderSingleKey.java index 4ff1caf44f74..8dc4589f64a9 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionLoaderSingleKey.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionLoaderSingleKey.java @@ -15,11 +15,12 @@ import org.hibernate.loader.ast.spi.CollectionLoader; import org.hibernate.metamodel.mapping.PluralAttributeMapping; import org.hibernate.query.spi.QueryOptions; +import org.hibernate.sql.ast.spi.SqlAliasBaseManager; import org.hibernate.sql.ast.tree.select.SelectStatement; import org.hibernate.sql.exec.internal.BaseExecutionContext; import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl; -import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParametersList; +import org.hibernate.sql.exec.spi.JdbcSelect; import org.hibernate.sql.results.internal.RowTransformerStandardImpl; import org.hibernate.sql.results.spi.ListResultsConsumer; @@ -36,7 +37,7 @@ public class CollectionLoaderSingleKey implements CollectionLoader { private final int keyJdbcCount; private final SelectStatement sqlAst; - private final JdbcOperationQuerySelect jdbcSelect; + private final JdbcSelect jdbcSelect; private final JdbcParametersList jdbcParameters; public CollectionLoaderSingleKey( @@ -48,6 +49,8 @@ public CollectionLoaderSingleKey( keyJdbcCount = attributeMapping.getKeyDescriptor().getJdbcTypeCount(); final var jdbcParametersBuilder = JdbcParametersList.newBuilder(); + final var sqlAliasBaseGenerator = new SqlAliasBaseManager(); + sqlAst = LoaderSelectBuilder.createSelect( attributeMapping, null, @@ -57,12 +60,18 @@ public CollectionLoaderSingleKey( influencers, new LockOptions(), jdbcParametersBuilder::add, + sqlAliasBaseGenerator, sessionFactory ); final var querySpec = sqlAst.getQueryPart().getFirstQuerySpec(); final var tableGroup = querySpec.getFromClause().getRoots().get( 0 ); - attributeMapping.applySoftDeleteRestrictions( tableGroup, querySpec::applyPredicate ); + attributeMapping.applyAuxiliaryRestrictions( + tableGroup, + querySpec::applyPredicate, + influencers, + sqlAliasBaseGenerator + ); jdbcParameters = jdbcParametersBuilder.build(); jdbcSelect = diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionLoaderSubSelectFetch.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionLoaderSubSelectFetch.java index 24d7807674c9..dbef2e7ca33f 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionLoaderSubSelectFetch.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionLoaderSubSelectFetch.java @@ -16,6 +16,7 @@ import org.hibernate.loader.ast.spi.CollectionLoader; import org.hibernate.metamodel.mapping.PluralAttributeMapping; import org.hibernate.query.spi.QueryOptions; +import org.hibernate.sql.ast.spi.SqlAliasBaseManager; import org.hibernate.sql.ast.tree.select.SelectStatement; import org.hibernate.sql.results.graph.DomainResult; import org.hibernate.sql.results.internal.ResultsHelper; @@ -43,6 +44,8 @@ public CollectionLoaderSubSelectFetch( this.attributeMapping = attributeMapping; this.subselect = subselect; + final var sqlAliasBaseGenerator = new SqlAliasBaseManager(); + sqlAst = LoaderSelectBuilder.createSubSelectFetchSelect( attributeMapping, subselect, @@ -50,12 +53,18 @@ public CollectionLoaderSubSelectFetch( session.getLoadQueryInfluencers(), new LockOptions(), jdbcParameter -> {}, + sqlAliasBaseGenerator, session.getFactory() ); final var querySpec = sqlAst.getQueryPart().getFirstQuerySpec(); final var tableGroup = querySpec.getFromClause().getRoots().get( 0 ); - attributeMapping.applySoftDeleteRestrictions( tableGroup, querySpec::applyPredicate ); + attributeMapping.applyAuxiliaryRestrictions( + tableGroup, + querySpec::applyPredicate, + session.getLoadQueryInfluencers(), + sqlAliasBaseGenerator + ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/DatabaseSnapshotExecutor.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/DatabaseSnapshotExecutor.java index c676d11b812d..81ce23b7d327 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/DatabaseSnapshotExecutor.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/DatabaseSnapshotExecutor.java @@ -4,9 +4,6 @@ */ package org.hibernate.loader.ast.internal; -import java.util.ArrayList; -import java.util.List; - import org.hibernate.LockOptions; import org.hibernate.engine.spi.LoadQueryInfluencers; import org.hibernate.engine.spi.SessionFactoryImplementor; @@ -25,14 +22,17 @@ import org.hibernate.sql.exec.internal.BaseExecutionContext; import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl; import org.hibernate.sql.exec.internal.SqlTypedMappingJdbcParameter; -import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParametersList; +import org.hibernate.sql.exec.spi.JdbcSelect; import org.hibernate.sql.results.graph.DomainResult; import org.hibernate.sql.results.graph.internal.ImmutableFetchList; import org.hibernate.sql.results.internal.RowTransformerArrayImpl; import org.hibernate.sql.results.spi.ListResultsConsumer; import org.hibernate.type.StandardBasicTypes; +import java.util.ArrayList; +import java.util.List; + import static org.hibernate.internal.util.collections.ArrayHelper.EMPTY_OBJECT_ARRAY; import static org.hibernate.loader.LoaderLogging.LOADER_LOGGER; import static org.hibernate.pretty.MessageHelper.infoString; @@ -44,7 +44,7 @@ class DatabaseSnapshotExecutor { private final EntityMappingType entityDescriptor; - private final JdbcOperationQuerySelect jdbcSelect; + private final JdbcSelect jdbcSelect; private final JdbcParametersList jdbcParameters; DatabaseSnapshotExecutor( diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/EntityBatchLoaderArrayParam.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/EntityBatchLoaderArrayParam.java index 09322aa83d7d..55b076fccb79 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/EntityBatchLoaderArrayParam.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/EntityBatchLoaderArrayParam.java @@ -4,8 +4,6 @@ */ package org.hibernate.loader.ast.internal; -import java.util.Locale; - import org.hibernate.LockOptions; import org.hibernate.engine.spi.LoadQueryInfluencers; import org.hibernate.engine.spi.SharedSessionContractImplementor; @@ -18,10 +16,13 @@ import org.hibernate.metamodel.mapping.SqlTypedMapping; import org.hibernate.metamodel.mapping.internal.SqlTypedMappingImpl; import org.hibernate.query.spi.QueryOptions; +import org.hibernate.sql.ast.spi.SqlAliasBaseManager; import org.hibernate.sql.ast.tree.expression.JdbcParameter; import org.hibernate.sql.ast.tree.select.SelectStatement; import org.hibernate.sql.exec.internal.SqlTypedMappingJdbcParameter; -import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; +import org.hibernate.sql.exec.spi.JdbcSelect; + +import java.util.Locale; import static org.hibernate.loader.ast.internal.LoaderHelper.loadByArrayParameter; import static org.hibernate.loader.ast.internal.MultiKeyLoadHelper.trimIdBatch; @@ -44,7 +45,7 @@ public class EntityBatchLoaderArrayParam private final SqlTypedMapping arraySqlTypedMapping; private final JdbcParameter jdbcParameter; private final SelectStatement sqlAst; - private final JdbcOperationQuerySelect jdbcSelectOperation; + private final JdbcSelect jdbcSelectOperation; /** @@ -95,6 +96,7 @@ public EntityBatchLoaderArrayParam( loadQueryInfluencers, new LockOptions(), jdbcParameter, + new SqlAliasBaseManager(), sessionFactory ); diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/EntityBatchLoaderInPredicate.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/EntityBatchLoaderInPredicate.java index 78dc80db9297..8bf64034ffd4 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/EntityBatchLoaderInPredicate.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/EntityBatchLoaderInPredicate.java @@ -4,8 +4,6 @@ */ package org.hibernate.loader.ast.internal; -import java.util.Locale; - import org.hibernate.LockOptions; import org.hibernate.engine.spi.LoadQueryInfluencers; import org.hibernate.engine.spi.SharedSessionContractImplementor; @@ -15,9 +13,12 @@ import org.hibernate.metamodel.mapping.EntityIdentifierMapping; import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.query.spi.QueryOptions; +import org.hibernate.sql.ast.spi.SqlAliasBaseManager; import org.hibernate.sql.ast.tree.select.SelectStatement; -import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParametersList; +import org.hibernate.sql.exec.spi.JdbcSelect; + +import java.util.Locale; import static org.hibernate.loader.ast.internal.MultiKeyLoadLogging.MULTI_KEY_LOAD_LOGGER; import static org.hibernate.pretty.MessageHelper.infoString; @@ -41,7 +42,7 @@ public class EntityBatchLoaderInPredicate private final JdbcParametersList jdbcParameters; private final SelectStatement sqlAst; - private final JdbcOperationQuerySelect jdbcSelectOperation; + private final JdbcSelect jdbcSelectOperation; /** * @param domainBatchSize The maximum number of entities we will initialize for each load @@ -80,6 +81,7 @@ public EntityBatchLoaderInPredicate( loadQueryInfluencers, new LockOptions(), builder::add, + new SqlAliasBaseManager(), sessionFactory ); jdbcParameters = builder.build(); diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/EntityConcreteTypeLoader.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/EntityConcreteTypeLoader.java index ac770282f810..d593045a8566 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/EntityConcreteTypeLoader.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/EntityConcreteTypeLoader.java @@ -15,6 +15,7 @@ import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.query.spi.QueryOptions; +import org.hibernate.sql.ast.spi.SqlAliasBaseManager; import org.hibernate.sql.ast.tree.select.SelectStatement; import org.hibernate.sql.exec.internal.BaseExecutionContext; import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl; @@ -49,6 +50,7 @@ public EntityConcreteTypeLoader(EntityMappingType entityDescriptor, SessionFacto new LoadQueryInfluencers( sessionFactory ), new LockOptions(), builder::add, + new SqlAliasBaseManager(), sessionFactory ); jdbcParameters = builder.build(); diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/LoaderHelper.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/LoaderHelper.java index 77fc6ac9a14a..eb40e138af61 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/LoaderHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/LoaderHelper.java @@ -4,8 +4,6 @@ */ package org.hibernate.loader.ast.internal; -import java.util.List; - import org.hibernate.Hibernate; import org.hibernate.LockMode; import org.hibernate.LockOptions; @@ -26,12 +24,14 @@ import org.hibernate.sql.ast.tree.select.SelectStatement; import org.hibernate.sql.exec.internal.JdbcParameterBindingImpl; import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl; -import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParametersList; +import org.hibernate.sql.exec.spi.JdbcSelect; import org.hibernate.sql.results.internal.RowTransformerStandardImpl; import org.hibernate.sql.results.spi.ListResultsConsumer; import org.hibernate.type.descriptor.java.JavaType; +import java.util.List; + import static java.lang.System.arraycopy; import static java.lang.reflect.Array.newInstance; import static org.hibernate.loader.LoaderLogging.LOADER_LOGGER; @@ -152,9 +152,9 @@ public static Boolean getReadOnlyFromLoadQueryInfluencers(LoadQueryInfluencers i /** * Normalize an array of keys (primary, foreign or natural). - *

    + *

    * If the array is already typed as the key type, {@code keys} is simply returned. - *

    + *

    * Otherwise, a new typed array is created and the contents copied from {@code keys} to this new array. If * key {@linkplain org.hibernate.cfg.AvailableSettings#JPA_LOAD_BY_ID_COMPLIANCE coercion} is enabled, the * values will be coerced to the key type. @@ -188,7 +188,7 @@ public static K[] normalizeKeys( } else { for ( int i = 0; i < keys.length; i++ ) { - typedArray[i] = keyJavaType.coerce( keys[i], session ); + typedArray[i] = keyJavaType.cast( keyJavaType.coerce( keys[i] ) ); } } return typedArray; @@ -217,7 +217,7 @@ public static X[] createTypedArray(Class elementClass, @SuppressWarnings( public static List loadByArrayParameter( K[] idsToInitialize, SelectStatement sqlAst, - JdbcOperationQuerySelect jdbcOperation, + JdbcSelect jdbcOperation, JdbcParameter jdbcParameter, JdbcMapping arrayJdbcMapping, Object entityId, 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 21a312f47640..be37a2849b8f 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 @@ -37,10 +37,9 @@ import org.hibernate.spi.EntityIdentifierNavigablePath; import org.hibernate.spi.NavigablePath; import org.hibernate.sql.ast.SqlAstJoinType; -import org.hibernate.sql.ast.spi.AliasCollector; import org.hibernate.sql.ast.spi.FromClauseAccess; import org.hibernate.sql.ast.spi.SimpleFromClauseAccessImpl; -import org.hibernate.sql.ast.spi.SqlAliasBaseManager; +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.expression.ColumnReference; @@ -103,6 +102,7 @@ public static SelectStatement createSelectByUniqueKey( LoadQueryInfluencers loadQueryInfluencers, LockOptions lockOptions, Consumer jdbcParameterConsumer, + SqlAliasBaseGenerator sqlAliasBaseGenerator, SessionFactoryImplementor sessionFactory) { final var process = new LoaderSelectBuilder( sessionFactory.getSqlTranslationEngine(), @@ -115,7 +115,8 @@ public static SelectStatement createSelectByUniqueKey( lockOptions, determineGraphTraversalState( loadQueryInfluencers, sessionFactory.getJpaMetamodel() ), true, - jdbcParameterConsumer + jdbcParameterConsumer, + sqlAliasBaseGenerator ); return process.generateSelect(); } @@ -129,6 +130,7 @@ public static SelectStatement createSelectBySingleArrayParameter( LoadQueryInfluencers influencers, LockOptions lockOptions, JdbcParameter jdbcArrayParameter, + SqlAliasBaseGenerator sqlAliasBaseGenerator, SessionFactoryImplementor sessionFactory) { final var builder = new LoaderSelectBuilder( sessionFactory.getSqlTranslationEngine(), @@ -141,7 +143,8 @@ public static SelectStatement createSelectBySingleArrayParameter( lockOptions, determineGraphTraversalState( influencers, sessionFactory.getJpaMetamodel() ), true, - null + null, + sqlAliasBaseGenerator ); final var rootQuerySpec = new QuerySpec( true ); @@ -226,6 +229,7 @@ public static SelectStatement createSelect( LoadQueryInfluencers loadQueryInfluencers, LockOptions lockOptions, Consumer jdbcParameterConsumer, + SqlAliasBaseGenerator sqlAliasBaseGenerator, SessionFactoryImplementor sessionFactory) { final var process = new LoaderSelectBuilder( sessionFactory.getSqlTranslationEngine(), @@ -236,7 +240,8 @@ public static SelectStatement createSelect( numberOfKeysToLoad, loadQueryInfluencers, lockOptions, - jdbcParameterConsumer + jdbcParameterConsumer, + sqlAliasBaseGenerator ); return process.generateSelect(); } @@ -250,6 +255,7 @@ public static SelectStatement createSelect( LoadQueryInfluencers loadQueryInfluencers, LockOptions lockOptions, Consumer jdbcParameterConsumer, + SqlAliasBaseGenerator sqlAliasBaseGenerator, SessionFactoryImplementor sessionFactory) { final var process = new LoaderSelectBuilder( sessionFactory.getSqlTranslationEngine(), @@ -260,7 +266,8 @@ public static SelectStatement createSelect( numberOfKeysToLoad, loadQueryInfluencers, lockOptions, - jdbcParameterConsumer + jdbcParameterConsumer, + sqlAliasBaseGenerator ); return process.generateSelect(); } @@ -277,6 +284,7 @@ static SelectStatement createSelect( LoadQueryInfluencers loadQueryInfluencers, LockOptions lockOptions, Consumer jdbcParameterConsumer, + SqlAliasBaseGenerator sqlAliasBaseGenerator, SessionFactoryImplementor sessionFactory) { final var process = new LoaderSelectBuilder( sessionFactory.getSqlTranslationEngine(), @@ -289,9 +297,9 @@ static SelectStatement createSelect( lockOptions, determineGraphTraversalState( loadQueryInfluencers, sessionFactory.getJpaMetamodel() ), forceIdentifierSelection, - jdbcParameterConsumer + jdbcParameterConsumer, + sqlAliasBaseGenerator ); - return process.generateSelect(); } @@ -315,6 +323,7 @@ public static SelectStatement createSubSelectFetchSelect( LoadQueryInfluencers loadQueryInfluencers, LockOptions lockOptions, Consumer jdbcParameterConsumer, + SqlAliasBaseGenerator sqlAliasBaseGenerator, SessionFactoryImplementor sessionFactory) { final var process = new LoaderSelectBuilder( sessionFactory.getSqlTranslationEngine(), @@ -325,9 +334,10 @@ public static SelectStatement createSubSelectFetchSelect( -1, loadQueryInfluencers, lockOptions, - jdbcParameterConsumer + jdbcParameterConsumer, + sqlAliasBaseGenerator ); - return process.generateSelect( subselect ); + return process.generateSelect( subselect, sqlAliasBaseGenerator ); } private final SqlAstCreationContext creationContext; @@ -344,6 +354,7 @@ public static SelectStatement createSubSelectFetchSelect( private int fetchDepth; private RowCardinality rowCardinality = RowCardinality.SINGLE; + private final SqlAliasBaseGenerator sqlAliasBasGenerator; private LoaderSelectBuilder( SqlAstCreationContext creationContext, @@ -356,7 +367,8 @@ private LoaderSelectBuilder( LockOptions lockOptions, EntityGraphTraversalState entityGraphTraversalState, boolean forceIdentifierSelection, - Consumer jdbcParameterConsumer) { + Consumer jdbcParameterConsumer, + SqlAliasBaseGenerator sqlAliasBasGenerator) { this.creationContext = creationContext; this.loadable = loadable; this.partsToSelect = partsToSelect; @@ -368,6 +380,7 @@ private LoaderSelectBuilder( this.entityGraphTraversalState = entityGraphTraversalState; this.forceIdentifierSelection = forceIdentifierSelection; this.jdbcParameterConsumer = jdbcParameterConsumer; + this.sqlAliasBasGenerator = sqlAliasBasGenerator; if ( loadable instanceof PluralAttributeMapping pluralAttributeMapping ) { if ( pluralAttributeMapping.getMappedType().getCollectionSemantics() .getCollectionClassification() == CollectionClassification.BAG ) { @@ -385,7 +398,8 @@ private LoaderSelectBuilder( int numberOfKeysToLoad, LoadQueryInfluencers loadQueryInfluencers, LockOptions lockOptions, - Consumer jdbcParameterConsumer) { + Consumer jdbcParameterConsumer, + SqlAliasBaseGenerator sqlAliasBasGenerator) { this( creationContext, loadable, @@ -397,7 +411,8 @@ private LoaderSelectBuilder( lockOptions != null ? lockOptions : new LockOptions(), determineGraphTraversalState( loadQueryInfluencers, creationContext.getJpaMetamodel() ), determineWhetherToForceIdSelection( numberOfKeysToLoad, restrictedParts ), - jdbcParameterConsumer + jdbcParameterConsumer, + sqlAliasBasGenerator ); } @@ -410,7 +425,8 @@ private LoaderSelectBuilder( int numberOfKeysToLoad, LoadQueryInfluencers loadQueryInfluencers, LockOptions lockOptions, - Consumer jdbcParameterConsumer) { + Consumer jdbcParameterConsumer, + SqlAliasBaseGenerator sqlAliasBasGenerator) { this( creationContext, loadable, @@ -420,7 +436,8 @@ private LoaderSelectBuilder( numberOfKeysToLoad, loadQueryInfluencers, lockOptions, - jdbcParameterConsumer + jdbcParameterConsumer, + sqlAliasBasGenerator ); } @@ -570,7 +587,7 @@ private TableGroup buildRootTableGroup( private LoaderSqlAstCreationState createSqlAstCreationState(QuerySpec rootQuerySpec) { return new LoaderSqlAstCreationState( rootQuerySpec, - new SqlAliasBaseManager(), + sqlAliasBasGenerator, new SimpleFromClauseAccessImpl(), lockOptions, this::visitFetches, @@ -580,6 +597,10 @@ private LoaderSqlAstCreationState createSqlAstCreationState(QuerySpec rootQueryS ); } + private SqlAliasBaseGenerator getSqlAliasBaseGenerator() { + return sqlAliasBasGenerator; + } + private void applyRestriction( QuerySpec rootQuerySpec, NavigablePath rootNavigablePath, @@ -960,7 +981,7 @@ else if ( fetchable instanceof PluralAttributeMapping pluralAttributeMapping ) { return true; } - private SelectStatement generateSelect(SubselectFetch subselect) { + private SelectStatement generateSelect(SubselectFetch subselect, SqlAliasBaseGenerator sqlAliasBaseGenerator) { // todo (6.0) : we could even convert this to a join by piecing together // parts from the subselect-fetch sql-ast. e.g. today we do: @@ -989,11 +1010,9 @@ private SelectStatement generateSelect(SubselectFetch subselect) { final var rootQuerySpec = new QuerySpec( true ); rootQuerySpec.applyRootPathForLocking( rootNavigablePath ); - // We need to initialize the acronymMap based on subselect.getLoadingSqlAst() to avoid alias collisions - final var tableReferences = AliasCollector.getTableReferences( subselect.getLoadingSqlAst() ); final var sqlAstCreationState = new LoaderSqlAstCreationState( rootQuerySpec, - new SqlAliasBaseManager( tableReferences.keySet() ), + sqlAliasBaseGenerator, new SimpleFromClauseAccessImpl(), lockOptions, this::visitFetches, 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 6f6e364364e5..d706927b857e 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 @@ -33,7 +33,6 @@ import org.hibernate.sql.ast.Clause; import org.hibernate.sql.ast.spi.FromClauseAccess; import org.hibernate.sql.ast.spi.SqlAliasBaseGenerator; -import org.hibernate.sql.ast.spi.SqlAliasBaseManager; import org.hibernate.sql.ast.spi.SqlAstCreationContext; import org.hibernate.sql.ast.spi.SqlAstCreationState; import org.hibernate.sql.ast.spi.SqlAstProcessingState; @@ -57,7 +56,7 @@ public interface FetchProcessor { ImmutableFetchList visitFetches(FetchParent fetchParent, LoaderSqlAstCreationState creationState); } - private final SqlAliasBaseManager sqlAliasBaseManager; + private final SqlAliasBaseGenerator sqlAliasBaseManager; private final boolean forceIdentifierSelection; private final LoadQueryInfluencers loadQueryInfluencers; private final SqlAstCreationContext sf; @@ -72,7 +71,7 @@ public interface FetchProcessor { public LoaderSqlAstCreationState( QueryPart queryPart, - SqlAliasBaseManager sqlAliasBaseManager, + SqlAliasBaseGenerator sqlAliasBaseManager, FromClauseAccess fromClauseAccess, LockOptions lockOptions, FetchProcessor fetchProcessor, diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiIdEntityLoaderArrayParam.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiIdEntityLoaderArrayParam.java index 8d3e2d8618e9..ddeb5dbac4a8 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiIdEntityLoaderArrayParam.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiIdEntityLoaderArrayParam.java @@ -24,6 +24,7 @@ import org.hibernate.metamodel.mapping.internal.SqlTypedMappingImpl; import org.hibernate.query.spi.QueryOptions; import org.hibernate.query.spi.QueryOptionsAdapter; +import org.hibernate.sql.ast.spi.SqlAliasBaseManager; import org.hibernate.sql.ast.tree.expression.JdbcParameter; import org.hibernate.sql.exec.internal.JdbcParameterBindingImpl; import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl; @@ -103,6 +104,7 @@ protected void loadEntitiesById( session.getLoadQueryInfluencers(), lockOptions, jdbcParameter, + new SqlAliasBaseManager(), getSessionFactory() ); @@ -163,6 +165,7 @@ protected void loadEntitiesWithUnresolvedIds( session.getLoadQueryInfluencers(), lockOptions, jdbcParameter, + new SqlAliasBaseManager(), getSessionFactory() ); diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiIdEntityLoaderInPredicate.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiIdEntityLoaderInPredicate.java index 487fd91b074d..202d1004603d 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiIdEntityLoaderInPredicate.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiIdEntityLoaderInPredicate.java @@ -15,6 +15,7 @@ import org.hibernate.loader.ast.spi.MultiKeyLoadSizingStrategy; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.query.spi.QueryOptionsAdapter; +import org.hibernate.sql.ast.spi.SqlAliasBaseManager; import org.hibernate.sql.ast.tree.select.SelectStatement; import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl; import org.hibernate.sql.exec.spi.JdbcParameterBindings; @@ -113,6 +114,7 @@ private List performRegularMultiLoad( session.getLoadQueryInfluencers(), lockOptions, jdbcParametersBuilder::add, + new SqlAliasBaseManager(), getSessionFactory() ); diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiKeyLoadChunker.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiKeyLoadChunker.java index 18b8bbc55bf6..c450d5406e75 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiKeyLoadChunker.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiKeyLoadChunker.java @@ -10,9 +10,9 @@ import org.hibernate.sql.ast.tree.select.SelectStatement; import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl; import org.hibernate.sql.exec.spi.ExecutionContext; -import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.exec.spi.JdbcParametersList; +import org.hibernate.sql.exec.spi.JdbcSelect; import org.hibernate.sql.results.internal.RowTransformerStandardImpl; import org.hibernate.sql.results.spi.ManagedResultConsumer; @@ -52,7 +52,7 @@ interface ChunkBoundaryListener { private final JdbcParametersList jdbcParameters; private final SelectStatement sqlAst; - private final JdbcOperationQuerySelect jdbcSelect; + private final JdbcSelect jdbcSelect; public MultiKeyLoadChunker( int chunkSize, @@ -60,7 +60,7 @@ public MultiKeyLoadChunker( Bindable bindable, JdbcParametersList jdbcParameters, SelectStatement sqlAst, - JdbcOperationQuerySelect jdbcSelect) { + JdbcSelect jdbcSelect) { this.chunkSize = chunkSize; this.keyColumnCount = keyColumnCount; this.bindable = bindable; diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiKeyLoadLogging.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiKeyLoadLogging.java index 450395721f85..2f57294f29cd 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiKeyLoadLogging.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiKeyLoadLogging.java @@ -15,6 +15,7 @@ import org.jboss.logging.annotations.ValidIdRange; import java.lang.invoke.MethodHandles; +import java.util.Locale; import static org.jboss.logging.Logger.Level.TRACE; @@ -38,7 +39,7 @@ public interface MultiKeyLoadLogging extends BasicLogger { String LOGGER_NAME = SubSystemLogging.BASE + ".loader.multi"; - MultiKeyLoadLogging MULTI_KEY_LOAD_LOGGER = Logger.getMessageLogger( MethodHandles.lookup(), MultiKeyLoadLogging.class, LOGGER_NAME ); + MultiKeyLoadLogging MULTI_KEY_LOAD_LOGGER = Logger.getMessageLogger( MethodHandles.lookup(), MultiKeyLoadLogging.class, LOGGER_NAME, Locale.ROOT ); // Enablement messages @LogMessage(level = TRACE) diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiNaturalIdLoaderArrayParam.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiNaturalIdLoaderArrayParam.java index 39dc23766895..d27bef273a1b 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiNaturalIdLoaderArrayParam.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiNaturalIdLoaderArrayParam.java @@ -17,6 +17,7 @@ import org.hibernate.metamodel.mapping.internal.SimpleNaturalIdMapping; import org.hibernate.metamodel.mapping.internal.SqlTypedMappingImpl; import org.hibernate.query.spi.QueryOptionsAdapter; +import org.hibernate.sql.ast.spi.SqlAliasBaseManager; import org.hibernate.sql.exec.internal.SqlTypedMappingJdbcParameter; import org.hibernate.sql.exec.spi.JdbcParameterBindings; @@ -71,6 +72,7 @@ public List loadEntitiesWithUnresolvedIds( session.getLoadQueryInfluencers(), lockOptions, jdbcParameter, + new SqlAliasBaseManager(), factory ); final var jdbcSelectOperation = diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiNaturalIdLoadingBatcher.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiNaturalIdLoadingBatcher.java index d9d723a73a18..020ee729ec30 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiNaturalIdLoadingBatcher.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiNaturalIdLoadingBatcher.java @@ -4,9 +4,6 @@ */ package org.hibernate.loader.ast.internal; -import java.util.ArrayList; -import java.util.List; - import org.hibernate.LockOptions; import org.hibernate.engine.spi.LoadQueryInfluencers; import org.hibernate.engine.spi.SessionFactoryImplementor; @@ -15,14 +12,18 @@ import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.ModelPart; import org.hibernate.query.spi.QueryOptionsAdapter; +import org.hibernate.sql.ast.spi.SqlAliasBaseManager; import org.hibernate.sql.ast.tree.select.SelectStatement; import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl; -import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.exec.spi.JdbcParametersList; +import org.hibernate.sql.exec.spi.JdbcSelect; import org.hibernate.sql.results.internal.RowTransformerStandardImpl; import org.hibernate.sql.results.spi.ListResultsConsumer; +import java.util.ArrayList; +import java.util.List; + import static org.hibernate.internal.util.collections.CollectionHelper.arrayList; /** @@ -49,7 +50,7 @@ interface KeyValueResolver { private final KeyValueResolver keyValueResolver; - private final JdbcOperationQuerySelect jdbcSelect; + private final JdbcSelect jdbcSelect; private final LockOptions lockOptions; @@ -75,6 +76,7 @@ public MultiNaturalIdLoadingBatcher( loadQueryInfluencers, lockOptions, jdbcParametersBuilder::add, + new SqlAliasBaseManager(), sessionFactory ); this.jdbcParameters = jdbcParametersBuilder.build(); diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/SingleIdEntityLoaderProvidedQueryImpl.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/SingleIdEntityLoaderProvidedQueryImpl.java index 1f65168f7587..c4d4d430eed4 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/SingleIdEntityLoaderProvidedQueryImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/SingleIdEntityLoaderProvidedQueryImpl.java @@ -15,6 +15,7 @@ import org.hibernate.type.descriptor.java.JavaType; import static org.hibernate.internal.util.collections.ArrayHelper.EMPTY_OBJECT_ARRAY; +import static org.hibernate.query.ResultListTransformer.uniqueResultTransformer; /** * Implementation of SingleIdEntityLoader for cases where the application has @@ -44,6 +45,7 @@ public T load(Object pkValue, LockOptions lockOptions, Boolean readOnly, SharedS final var query = namedQueryMemento.toQuery( session, mappedJavaType.getJavaTypeClass() ); query.setParameter( (Parameter) query.getParameters().iterator().next(), pkValue ); query.setQueryFlushMode( QueryFlushMode.NO_FLUSH ); + query.setResultListTransformer( uniqueResultTransformer() ); return query.uniqueResult(); } diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/SingleIdEntityLoaderStandardImpl.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/SingleIdEntityLoaderStandardImpl.java index 48e6db7f6c12..4971c747004b 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/SingleIdEntityLoaderStandardImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/SingleIdEntityLoaderStandardImpl.java @@ -14,6 +14,7 @@ import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.loader.ast.spi.CascadingFetchProfile; import org.hibernate.metamodel.mapping.EntityMappingType; +import org.hibernate.sql.ast.spi.SqlAliasBaseManager; import org.hibernate.sql.exec.spi.JdbcParametersList; /** @@ -179,6 +180,7 @@ private static SingleIdLoadPlan createLoadPlan( influencers, lockOptions, jdbcParametersBuilder::add, + new SqlAliasBaseManager(), factory ), jdbcParametersBuilder.build(), diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/SingleUniqueKeyEntityLoaderStandard.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/SingleUniqueKeyEntityLoaderStandard.java index 166709179f7e..ed06653cd5ad 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/SingleUniqueKeyEntityLoaderStandard.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/SingleUniqueKeyEntityLoaderStandard.java @@ -4,8 +4,6 @@ */ package org.hibernate.loader.ast.internal; -import java.util.List; - import org.hibernate.HibernateException; import org.hibernate.LockOptions; import org.hibernate.engine.spi.LoadQueryInfluencers; @@ -20,18 +18,21 @@ import org.hibernate.metamodel.mapping.SingularAttributeMapping; import org.hibernate.metamodel.mapping.internal.ToOneAttributeMapping; import org.hibernate.query.spi.QueryOptions; +import org.hibernate.sql.ast.spi.SqlAliasBaseManager; import org.hibernate.sql.ast.tree.select.SelectStatement; import org.hibernate.sql.exec.internal.BaseExecutionContext; import org.hibernate.sql.exec.internal.CallbackImpl; import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl; import org.hibernate.sql.exec.spi.Callback; import org.hibernate.sql.exec.spi.ExecutionContext; -import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.exec.spi.JdbcParametersList; +import org.hibernate.sql.exec.spi.JdbcSelect; import org.hibernate.sql.results.internal.RowTransformerSingularReturnImpl; import org.hibernate.sql.results.spi.ListResultsConsumer; +import java.util.List; + import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; @@ -43,7 +44,7 @@ public class SingleUniqueKeyEntityLoaderStandard implements SingleUniqueKeyEn private final ModelPart uniqueKeyAttribute; private final String uniqueKeyAttributePath; private final JdbcParametersList jdbcParameters; - private final JdbcOperationQuerySelect jdbcSelect; + private final JdbcSelect jdbcSelect; public SingleUniqueKeyEntityLoaderStandard( EntityMappingType entityDescriptor, @@ -66,6 +67,7 @@ public SingleUniqueKeyEntityLoaderStandard( loadQueryInfluencers, new LockOptions(), builder::add, + new SqlAliasBaseManager(), factory ); jdbcParameters = builder.build(); @@ -74,11 +76,12 @@ public SingleUniqueKeyEntityLoaderStandard( private static String getAttributePath(AttributeMapping attribute) { ManagedMappingType declaringType = attribute.getDeclaringType(); - if ( declaringType instanceof EmbeddableMappingType embeddableMappingType ) { + if ( declaringType instanceof EmbeddableMappingType ) { final var path = new StringBuilder(); path.append( attribute.getAttributeName() ); do { - final var valueMapping = embeddableMappingType.getEmbeddedValueMapping(); + // declaringType must be cast each time (not a pattern variable) as it's updated each iteration + final var valueMapping = ( (EmbeddableMappingType) declaringType ).getEmbeddedValueMapping(); attribute = valueMapping.asAttributeMapping(); if ( attribute == null ) { break; @@ -127,6 +130,7 @@ public Object resolveId(Object ukValue, SharedSessionContractImplementor session new LoadQueryInfluencers( factory ), new LockOptions(), builder::add, + new SqlAliasBaseManager(), factory ); final var bindings = jdbcParameterBindings( ukValue, builder.build(), session ); @@ -147,7 +151,7 @@ private JdbcParameterBindings jdbcParameterBindings( } private static List list( - JdbcOperationQuerySelect jdbcSelect, + JdbcSelect jdbcSelect, JdbcParameterBindings jdbcParameterBindings, ExecutionContext executionContext) { return executionContext.getSession().getJdbcServices().getJdbcSelectExecutor() @@ -162,7 +166,7 @@ private static List list( ); } - private static JdbcOperationQuerySelect getJdbcSelect + private static JdbcSelect getJdbcSelect (SessionFactoryImplementor factory, SelectStatement sqlAst, JdbcParameterBindings jdbcParameterBindings) { return factory.getJdbcServices().getJdbcEnvironment().getSqlAstTranslatorFactory() .buildSelectTranslator( factory, sqlAst ) diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/Loadable.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/Loadable.java index 7f38ed2a4ecc..e09f49de47e1 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/Loadable.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/Loadable.java @@ -36,9 +36,9 @@ default boolean isAffectedByInfluencers(LoadQueryInfluencers influencers) { default boolean isAffectedByInfluencers(LoadQueryInfluencers influencers, boolean onlyApplyForLoadByKeyFilters) { return isAffectedByEntityGraph( influencers ) - || isAffectedByEnabledFetchProfiles( influencers ) - || isAffectedByEnabledFilters( influencers, onlyApplyForLoadByKeyFilters ) - || isAffectedByBatchSize( influencers ); + || isAffectedByEnabledFetchProfiles( influencers ) + || isAffectedByEnabledFilters( influencers, onlyApplyForLoadByKeyFilters ) + || isAffectedByBatchSize( influencers ); } default boolean isNotAffectedByInfluencers(LoadQueryInfluencers influencers) { @@ -49,7 +49,7 @@ default boolean isNotAffectedByInfluencers(LoadQueryInfluencers influencers) { && influencers.getEnabledCascadingFetchProfile() == null; } - private boolean isAffectedByBatchSize(LoadQueryInfluencers influencers) { + default boolean isAffectedByBatchSize(LoadQueryInfluencers influencers) { return influencers.getBatchSize() > 0 && influencers.getBatchSize() != getBatchSize(); } diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/MultiIdEntityLoader.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/MultiIdEntityLoader.java index 0063236946a9..48e5199b96a9 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/MultiIdEntityLoader.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/MultiIdEntityLoader.java @@ -4,13 +4,15 @@ */ package org.hibernate.loader.ast.spi; -import java.util.List; - import org.hibernate.engine.spi.SharedSessionContractImplementor; -/** - * Loader subtype for loading multiple entities by multiple identifier values. - */ +import java.util.List; + +/// EntityMultiLoader implementation based on [identifier][org.hibernate.KeyType#IDENTIFIER]. +/// +/// @see org.hibernate.Session#findMultiple +/// +/// @author Steve Ebersole public interface MultiIdEntityLoader extends EntityMultiLoader { /** * Load multiple entities by id. The exact result depends on the passed options. diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/MultiIdLoadOptions.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/MultiIdLoadOptions.java index b1417d1ff3e5..eab7df01fe53 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/MultiIdLoadOptions.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/MultiIdLoadOptions.java @@ -7,9 +7,14 @@ import org.hibernate.SessionCheckMode; import org.hibernate.engine.spi.SessionImplementor; -/** - * Encapsulation of the options for loading multiple entities by id - */ + +/// Encapsulation of the options for loading multiple entities (of a type) +/// by [id][org.hibernate.KeyType#IDENTIFIER]. +/// +/// @see org.hibernate.Session#findMultiple +/// @see MultiIdEntityLoader +/// +/// @author Steve Ebersole public interface MultiIdLoadOptions extends MultiLoadOptions { /** * Controls whether to check the current status of each identified entity diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/MultiKeyLoadSizingStrategy.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/MultiKeyLoadSizingStrategy.java index 58f0573d9d66..d7a6f8fa439f 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/MultiKeyLoadSizingStrategy.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/MultiKeyLoadSizingStrategy.java @@ -28,12 +28,10 @@ public interface MultiKeyLoadSizingStrategy { /** * Determine the optimal batch size (number of key values) to load at a time. - *

    + *

    * The return can be less than the total {@code numberOfKeys} to be loaded indicating * that the load should be split across multiple SQL queries. E.g. if we are loading * 7 keys and the strategy says the optimal size is 5, we will perform 2 queries. - *

    - * @apiNote * * @param numberOfKeyColumns The number of columns to which the key is mapped * @param numberOfKeys The total number of keys we need to load diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/MultiLoadOptions.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/MultiLoadOptions.java index 2279ad8279c8..c84ad7039880 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/MultiLoadOptions.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/MultiLoadOptions.java @@ -8,9 +8,13 @@ import org.hibernate.OrderingMode; import org.hibernate.RemovalsMode; -/** - * Base contract for options for multi-load operations - */ +/// Encapsulation of the options for loading multiple entities (of a type) +/// by [key][org.hibernate.KeyType]. +/// +/// @see MultiIdLoadOptions +/// @see MultiNaturalIdLoadOptions +/// +/// @author Steve Ebersole public interface MultiLoadOptions { /** * How should entities in removed status be handled. diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/MultiNaturalIdLoadOptions.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/MultiNaturalIdLoadOptions.java index 48ce0b5dc67b..f8d635636546 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/MultiNaturalIdLoadOptions.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/MultiNaturalIdLoadOptions.java @@ -4,8 +4,11 @@ */ package org.hibernate.loader.ast.spi; -/** - * Encapsulation of the options for loading multiple entities by natural-id - */ +/// Encapsulation of the options for loading multiple entities (of a type) +/// by [natural-id][org.hibernate.KeyType#NATURAL]. +/// +/// @see MultiNaturalIdLoader +/// +/// @author Steve Ebersole public interface MultiNaturalIdLoadOptions extends MultiLoadOptions { } diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/MultiNaturalIdLoader.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/MultiNaturalIdLoader.java index 4aa96b303666..7f3c6bb4f8f3 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/MultiNaturalIdLoader.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/MultiNaturalIdLoader.java @@ -4,25 +4,25 @@ */ package org.hibernate.loader.ast.spi; -import java.util.List; - import org.hibernate.engine.spi.SharedSessionContractImplementor; -/** - * Loader for entities by multiple natural-ids - * - * @param The entity Java type - */ +import java.util.List; + +/// EntityMultiLoader implementation based on [identifier][org.hibernate.KeyType#NATURAL]. +/// +/// @param The entity Java type +/// +/// @see org.hibernate.Session#findMultiple +/// +/// @author Steve Ebersole public interface MultiNaturalIdLoader extends EntityMultiLoader { - /** - * Load multiple entities by natural-id. The exact result depends on the passed options. - * - * @param naturalIds The natural-ids to load. The values of this array will depend on whether the - * natural-id is simple or complex. - * - * @param The basic form for a natural-id is a Map of its attribute values, or an array of the - * values positioned according to "attribute ordering". Simple natural-ids can also be expressed - * by their simple (basic/embedded) type. - */ + /// Load multiple entities by natural-id. The exact result depends on the passed options. + /// + /// @param naturalIds The natural-ids to load. The values of this array will depend on whether the + /// natural-id is simple or complex. + /// + /// @param The basic form for a natural-id is a Map of its attribute values, or an array of the + /// values positioned according to "attribute ordering". Simple natural-ids can also be expressed + /// by their simple (basic/embedded) type. List multiLoad(K[] naturalIds, MultiNaturalIdLoadOptions options, SharedSessionContractImplementor session); } diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/NaturalIdLoader.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/NaturalIdLoader.java index 9333140979de..388c4e52a0d4 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/NaturalIdLoader.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/NaturalIdLoader.java @@ -4,30 +4,46 @@ */ package org.hibernate.loader.ast.spi; +import jakarta.persistence.Timeout; +import org.hibernate.LockMode; +import org.hibernate.Locking; import org.hibernate.engine.spi.SharedSessionContractImplementor; -/** - * Loader for {@link org.hibernate.annotations.NaturalId} handling - * - * @author Steve Ebersole - */ +/// Loader for [org.hibernate.annotations.NaturalId] +/// +/// @author Steve Ebersole public interface NaturalIdLoader extends EntityLoader, MultiKeyLoader { + interface Options { + LockMode getLockMode(); + Timeout getLockTimeout(); + Locking.Scope getLockScope(); + Locking.FollowOn getLockFollowOn(); + } - /** - * Perform the load of the entity by its natural-id - * - * @param naturalIdToLoad The natural-id to load. One of 2 forms accepted: - * * Single-value - valid for entities with a simple (single-valued) - * natural-id - * * Map - valid for any natural-id load. The map is each value keyed - * by the attribute name that the value corresponds to. Even though - * this form is allowed for simple natural-ids, the single value form - * should be used as it is more efficient - * @param options The options to apply to the load operation - * @param session The session into which the entity is being loaded - */ + /// Perform the load of the entity by its natural-id + /// + /// @param naturalIdToLoad The natural-id to load. One of 2 forms accepted: + /// * Single-value - valid for entities with a simple (single-valued) + /// natural-id + /// * Map - valid for any natural-id load. The map is each value keyed + /// by the attribute name that the value corresponds to. Even though + /// this form is allowed for simple natural-ids, the single value form + /// should be used as it is more efficient + /// @param options The options to apply to the load operation + /// @param session The session into which the entity is being loaded + /// + /// @deprecated (since 7.3) : use [#load(Object, Options, SharedSessionContractImplementor)] instead. + @Deprecated T load(Object naturalIdToLoad, NaturalIdLoadOptions options, SharedSessionContractImplementor session); + /// Perform the load of the entity by its natural-id + /// + /// @param naturalIdToLoad The [normalized][org.hibernate.metamodel.mapping.NaturalIdMapping#normalizeInput] + /// form of the natural-id. + /// @param options The options to apply to the load operation + /// @param session The session into which the entity is being loaded + T load(Object naturalIdToLoad, Options options, SharedSessionContractImplementor session); + /** * Resolve the id from natural-id value */ diff --git a/hibernate-core/src/main/java/org/hibernate/loader/internal/BaseNaturalIdLoadAccessImpl.java b/hibernate-core/src/main/java/org/hibernate/loader/internal/BaseNaturalIdLoadAccessImpl.java index 14685b1c9ba7..1de0d68f623f 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/internal/BaseNaturalIdLoadAccessImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/internal/BaseNaturalIdLoadAccessImpl.java @@ -15,6 +15,7 @@ import org.hibernate.IdentifierLoadAccess; import org.hibernate.LockMode; import org.hibernate.LockOptions; +import org.hibernate.Locking; import org.hibernate.UnknownProfileException; import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.engine.spi.Status; @@ -70,6 +71,14 @@ protected Object with(LockMode lockMode, PessimisticLockScope lockScope) { return this; } + protected Object with(Locking.Scope lockScope) { + if ( lockOptions == null ) { + lockOptions = new LockOptions(); + } + lockOptions.setScope( lockScope ); + return this; + } + protected Object with(Timeout timeout) { if ( lockOptions == null ) { diff --git a/hibernate-core/src/main/java/org/hibernate/loader/internal/IdentifierLoadAccessImpl.java b/hibernate-core/src/main/java/org/hibernate/loader/internal/IdentifierLoadAccessImpl.java index f8d38d956b43..6bb5a3423a2a 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/internal/IdentifierLoadAccessImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/internal/IdentifierLoadAccessImpl.java @@ -26,8 +26,6 @@ import org.hibernate.graph.GraphSemantic; import org.hibernate.graph.spi.RootGraphImplementor; import org.hibernate.persister.entity.EntityPersister; -import org.hibernate.type.descriptor.java.JavaType; -import org.hibernate.type.spi.TypeConfiguration; import static org.hibernate.proxy.HibernateProxy.extractLazyInitializer; @@ -37,7 +35,7 @@ * @author Steve Ebersole */ // Hibernate Reactive extends this class: see ReactiveIdentifierLoadAccessImpl -public class IdentifierLoadAccessImpl implements IdentifierLoadAccess, JavaType.CoercionContext { +public class IdentifierLoadAccessImpl implements IdentifierLoadAccess { private final LoadAccessContext context; private final EntityPersister entityPersister; @@ -196,7 +194,10 @@ protected Object coerceId(Object id, SessionFactoryImplementor factory) { } else { try { - return entityPersister.getIdentifierMapping().getJavaType().coerce( id, this ); + final var identifierMapping = entityPersister.getIdentifierMapping(); + return identifierMapping.isVirtual() + ? id // special case for a class with an @IdClass + : identifierMapping.getJavaType().coerce( id ); } catch ( Exception e ) { throw new IllegalArgumentException( "Argument '" + id @@ -230,11 +231,6 @@ private static boolean isLoadByIdComplianceEnabled(SessionFactoryImplementor fac return factory.getSessionFactoryOptions().getJpaCompliance().isLoadByIdComplianceEnabled(); } - @Override - public TypeConfiguration getTypeConfiguration() { - return context.getSession().getSessionFactory().getTypeConfiguration(); - } - @Override public IdentifierLoadAccess enableFetchProfile(String profileName) { if ( !context.getSession().getFactory().containsFetchProfileDefinition( profileName ) ) { diff --git a/hibernate-core/src/main/java/org/hibernate/loader/internal/SimpleNaturalIdLoadAccessImpl.java b/hibernate-core/src/main/java/org/hibernate/loader/internal/SimpleNaturalIdLoadAccessImpl.java index f041771aaa06..c083911ffed7 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/internal/SimpleNaturalIdLoadAccessImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/internal/SimpleNaturalIdLoadAccessImpl.java @@ -16,10 +16,12 @@ import org.hibernate.HibernateException; import org.hibernate.LockMode; import org.hibernate.LockOptions; +import org.hibernate.Locking; import org.hibernate.SimpleNaturalIdLoadAccess; import org.hibernate.graph.GraphSemantic; import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.internal.SimpleNaturalIdMapping; +import org.hibernate.persister.entity.EntityPersister; /** * Implementation of {@link SimpleNaturalIdLoadAccess}. @@ -57,6 +59,11 @@ public SimpleNaturalIdLoadAccess with(LockMode lockMode, PessimisticLockScope return (SimpleNaturalIdLoadAccess) super.with( lockMode, lockScope ); } + public SimpleNaturalIdLoadAccess with(Locking.Scope lockScope) { + super.with( lockScope ); + return this; + } + @Override public SimpleNaturalIdLoadAccess with(Timeout timeout) { //noinspection unchecked @@ -99,7 +106,8 @@ private void verifySimplicity(Object naturalIdValue) { if ( !hasSimpleNaturalId && !naturalIdValue.getClass().isArray() && !(naturalIdValue instanceof List) - && !(naturalIdValue instanceof Map) ) { + && !(naturalIdValue instanceof Map) + && ! ( isNaturalIdClass( naturalIdValue ) ) ) { throw new HibernateException( String.format( Locale.ROOT, @@ -111,6 +119,11 @@ private void verifySimplicity(Object naturalIdValue) { } } + private boolean isNaturalIdClass(Object naturalIdValue) { + final EntityPersister entityPersister = entityPersister(); + return entityPersister.getNaturalIdMapping().getNaturalIdClass().isInstance( naturalIdValue ); + } + @Override public Optional loadOptional(Object naturalIdValue) { return Optional.ofNullable( load( naturalIdValue ) ); diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/Any.java b/hibernate-core/src/main/java/org/hibernate/mapping/Any.java index 71ca1269e581..fb9974ffe733 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/Any.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/Any.java @@ -8,6 +8,7 @@ import org.hibernate.MappingException; import org.hibernate.boot.spi.MetadataBuildingContext; import org.hibernate.engine.jdbc.spi.JdbcServices; +import org.hibernate.metamodel.mapping.DiscriminatorValue; import org.hibernate.metamodel.spi.ImplicitDiscriminatorStrategy; import org.hibernate.type.AnyType; import org.hibernate.type.MappingContext; @@ -35,7 +36,7 @@ public class Any extends SimpleValue { private BasicValue keyDescriptor; // common - private Map metaValueToEntityNameMap; + private Map metaValueToEntityNameMap; private ImplicitDiscriminatorStrategy implicitValueStrategy; private boolean lazy = true; @@ -157,7 +158,7 @@ public void addFormula(Formula formula) { private void applySelectableLocally(Selectable selectable) { // note: adding column to meta or key mapping ultimately calls back into `#applySelectableToSuper` // to add the column to the ANY super. - if ( discriminatorDescriptor == null && getColumnSpan() == 0 ) { + if ( discriminatorDescriptor == null && !hasColumns() ) { if ( selectable instanceof Column column ) { metaMapping.addColumn( column ); } @@ -183,11 +184,11 @@ public void setMetaType(String type) { metaMapping.setTypeName( type ); } - public Map getMetaValues() { + public Map getMetaValues() { return metaValueToEntityNameMap; } - public void setMetaValues(Map metaValueToEntityNameMap) { + public void setMetaValues(Map metaValueToEntityNameMap) { this.metaValueToEntityNameMap = metaValueToEntityNameMap; } @@ -247,7 +248,7 @@ public boolean isValid(MappingContext mappingContext) throws MappingException { } private static String columnName(Column column, MetadataBuildingContext buildingContext) { - final JdbcServices jdbcServices = + final var jdbcServices = buildingContext.getBootstrapContext().getServiceRegistry() .requireService( JdbcServices.class ); return column.getQuotedName( jdbcServices.getDialect() ); @@ -267,7 +268,7 @@ public void setDiscriminator(BasicValue discriminatorDescriptor) { } } - public void setDiscriminatorValueMappings(Map> discriminatorValueMappings) { + public void setDiscriminatorValueMappings(Map> discriminatorValueMappings) { metaValueToEntityNameMap = new HashMap<>(); discriminatorValueMappings.forEach( (value, entity) -> metaValueToEntityNameMap.put( value, entity.getName() ) ); } 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 ff4db8c1906e..40069d60b765 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/BasicValue.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/BasicValue.java @@ -4,15 +4,23 @@ */ package org.hibernate.mapping; +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.ParameterizedType; import java.util.HashMap; import java.util.Map; import java.util.Properties; import java.util.function.Consumer; import java.util.function.Function; +import org.hibernate.AnnotationException; +import org.hibernate.AssertionFailure; import org.hibernate.Incubating; import org.hibernate.Internal; import org.hibernate.MappingException; +import org.hibernate.boot.model.internal.Constructors; +import org.hibernate.models.spi.MemberDetails; +import org.hibernate.service.ServiceRegistry; import org.hibernate.type.TimeZoneStorageStrategy; import org.hibernate.annotations.SoftDelete; import org.hibernate.annotations.SoftDeleteType; @@ -63,15 +71,17 @@ import org.hibernate.type.internal.ConvertedBasicTypeImpl; import org.hibernate.type.spi.TypeConfiguration; import org.hibernate.type.spi.TypeConfigurationAware; +import org.hibernate.usertype.AnnotationBasedUserType; import org.hibernate.usertype.DynamicParameterizedType; import org.hibernate.usertype.UserType; -import com.fasterxml.classmate.ResolvedType; import jakarta.persistence.AttributeConverter; import jakarta.persistence.EnumType; import jakarta.persistence.TemporalType; +import org.hibernate.usertype.UserTypeCreationContext; import static java.lang.Boolean.parseBoolean; +import static org.hibernate.internal.util.GenericAssignability.isAssignableFrom; import static org.hibernate.boot.model.convert.spi.ConverterDescriptor.TYPE_NAME_PREFIX; import static org.hibernate.internal.CoreMessageLogger.CORE_LOGGER; import static org.hibernate.internal.util.ReflectHelper.reflectedPropertyType; @@ -83,6 +93,7 @@ /** * @author Steve Ebersole + * @author Yanming Zhou */ public class BasicValue extends SimpleValue implements JdbcTypeIndicators, Resolvable, JpaAttributeConverterCreationContext { @@ -145,6 +156,7 @@ public BasicValue(BasicValue original) { this.isSoftDelete = original.isSoftDelete; this.softDeleteStrategy = original.softDeleteStrategy; this.aggregateColumn = original.aggregateColumn; + this.jdbcTypeCode = original.jdbcTypeCode; } @Override @@ -215,8 +227,24 @@ public void setImplicitJavaTypeAccess(Function> getExplicitJavaTypeAccess() { + return explicitJavaTypeAccess; + } + + public Function getExplicitJdbcTypeAccess() { + return explicitJdbcTypeAccess; + } + + public Function> getExplicitMutabilityPlanAccess() { + return explicitMutabilityPlanAccess; + } + + public Function getImplicitJavaTypeAccess() { + return implicitJavaTypeAccess; + } + public Selectable getColumn() { - return getColumnSpan() == 0 ? null : getColumn( 0 ); + return hasColumns() ? getColumn( 0 ) : null; } public java.lang.reflect.Type getResolvedJavaType() { @@ -476,18 +504,17 @@ private ConverterDescriptor getSoftDeleteConverterDescriptor( SoftDelete.UnspecifiedConversion.class.equals( attributeConverterDescriptor.getAttributeConverterClass() ); if ( conversionWasUnspecified ) { final var jdbcType = BooleanJdbcType.INSTANCE.resolveIndicatedType( this, javaType ); - final var classmateContext = getBootstrapContext().getClassmateContext(); if ( jdbcType.isNumber() ) { - return ConverterDescriptors.of( NumericBooleanConverter.INSTANCE, classmateContext ); + return ConverterDescriptors.of( NumericBooleanConverter.INSTANCE ); } else if ( jdbcType.isString() ) { // here we pick 'T' / 'F' storage, though 'Y' / 'N' is equally valid - its 50/50 - return ConverterDescriptors.of( TrueFalseConverter.INSTANCE, classmateContext ); + return ConverterDescriptors.of( TrueFalseConverter.INSTANCE ); } else { // should indicate BIT or BOOLEAN == no conversion needed // - we still create the converter to properly set up JDBC type, etc - return ConverterDescriptors.of( PassThruSoftDeleteConverter.INSTANCE, classmateContext ); + return ConverterDescriptors.of( PassThruSoftDeleteConverter.INSTANCE ); } } else { @@ -509,12 +536,12 @@ public Class> getAttributeConverterClass } @Override - public ResolvedType getDomainValueResolvedType() { + public java.lang.reflect.Type getDomainValueResolvedType() { return underlyingDescriptor.getDomainValueResolvedType(); } @Override - public ResolvedType getRelationalValueResolvedType() { + public java.lang.reflect.Type getRelationalValueResolvedType() { return underlyingDescriptor.getRelationalValueResolvedType(); } @@ -612,14 +639,14 @@ public Boolean convertToEntityAttribute(Boolean relationalValue) { } } - private Resolution resolution(BasicJavaType explicitJavaType, JavaType javaType) { - final JavaType basicJavaType; + private Resolution resolution(BasicJavaType explicitJavaType, JavaType javaType) { + final JavaType basicJavaType; final JdbcType jdbcType; if ( explicitJdbcTypeAccess != null ) { final var typeConfiguration = getTypeConfiguration(); jdbcType = explicitJdbcTypeAccess.apply( typeConfiguration ); basicJavaType = javaType == null && jdbcType != null - ? jdbcType.getJdbcRecommendedJavaTypeMapping( null, null, typeConfiguration ) + ? jdbcType.getRecommendedJavaType( null, null, typeConfiguration ) : javaType; } else { @@ -630,7 +657,7 @@ private Resolution resolution(BasicJavaType explicitJavaType, JavaType throw new MappingException( "Unable to determine JavaType to use : " + this ); } - if ( basicJavaType instanceof BasicJavaType castType + if ( basicJavaType instanceof BasicJavaType castType && ( !basicJavaType.getJavaTypeClass().isEnum() || enumerationStyle == null ) ) { final var context = getBuildingContext(); final var autoAppliedTypeDef = context.getTypeDefinitionRegistry().resolveAutoApplied( castType ); @@ -673,8 +700,8 @@ private Resolution converterResolution(JavaType javaType, ConverterDescrip ); if ( javaType instanceof BasicPluralJavaType pluralJavaType - && !attributeConverterDescriptor.getDomainValueResolvedType().getErasedType() - .isAssignableFrom( javaType.getJavaTypeClass() ) ) { + && !isAssignableFrom( attributeConverterDescriptor.getDomainValueResolvedType(), + javaType.getJavaTypeClass() ) ) { // In this case, the converter applies to the element of a BasicPluralJavaType final BasicType registeredElementType = converterResolution.getLegacyResolvedBasicType(); final BasicType registeredType = registeredElementType == null ? null @@ -779,7 +806,7 @@ private JavaType specialJavaType( return xmlJavaType; } } - return javaTypeRegistry.getDescriptor( impliedJavaType ); + return javaTypeRegistry.resolveDescriptor( impliedJavaType ); } private MutabilityPlan mutabilityPlan( @@ -1030,6 +1057,10 @@ public void setExplicitTypeParams(Map explicitLocalTypeParams) { this.explicitLocalTypeParams = explicitLocalTypeParams; } + public Map getExplicitTypeParams() { + return explicitLocalTypeParams; + } + public void setExplicitTypeName(String typeName) { this.explicitTypeName = typeName; } @@ -1055,19 +1086,20 @@ public void setExplicitCustomType(Class> explicitCustomTyp throw new UnsupportedOperationException( "Unsupported attempt to set an explicit-custom-type when value is already resolved" ); } else { + final var parameters = buildCustomTypeProperties(); resolution = new UserTypeResolution<>( new CustomType<>( - getConfiguredUserTypeBean( explicitCustomType, getCustomTypeProperties() ), + getConfiguredUserTypeBean( explicitCustomType, getTypeAnnotation(), parameters ), getTypeConfiguration() ), null, - getCustomTypeProperties() + parameters ); } } } - private Properties getCustomTypeProperties() { + private Properties buildCustomTypeProperties() { final var properties = new Properties(); if ( isNotEmpty( getTypeParameters() ) ) { properties.putAll( getTypeParameters() ); @@ -1078,29 +1110,139 @@ private Properties getCustomTypeProperties() { return properties; } - private UserType getConfiguredUserTypeBean(Class> explicitCustomType, Properties properties) { + private UserType getConfiguredUserTypeBean( + Class> explicitCustomType, + Annotation typeAnnotation, Properties parameters) { final var typeInstance = - getBuildingContext().getBuildingOptions().isAllowExtensionsInCdi() - ? getUserTypeBean( explicitCustomType, properties ).getBeanInstance() - : FallbackBeanInstanceProducer.INSTANCE.produceBeanInstance( explicitCustomType ); - + createUserTypeInstance( explicitCustomType, parameters, typeAnnotation ); if ( typeInstance instanceof TypeConfigurationAware configurationAware ) { configurationAware.setTypeConfiguration( getTypeConfiguration() ); } + addParameterType( parameters, typeInstance ); + injectParameters( typeInstance, parameters ); + // envers - grr + setTypeParameters( parameters ); + return typeInstance; + } + + private UserType createUserTypeInstance( + Class> customType, + Properties parameters, + Annotation typeAnnotation) { + final var creationContext = new TypeCreationContext( parameters ); + final var typeInstance = instantiateUserType( customType, typeAnnotation, creationContext ); + if ( typeInstance instanceof AnnotationBasedUserType annotationBased ) { + initializeAnnotationBasedUserType( typeAnnotation, annotationBased, creationContext ); + } + return typeInstance; + } + + private void addParameterType(Properties properties, UserType typeInstance) { + if ( typeInstance instanceof DynamicParameterizedType + && parseBoolean( properties.getProperty( DynamicParameterizedType.IS_DYNAMIC ) ) + && properties.get( DynamicParameterizedType.PARAMETER_TYPE ) == null ) { + properties.put( DynamicParameterizedType.PARAMETER_TYPE, createParameterType() ); + } + } + + private void initializeAnnotationBasedUserType( + Annotation typeAnnotation, + AnnotationBasedUserType annotationBased, + UserTypeCreationContext creationContext) { + if ( typeAnnotation == null ) { + throw new AnnotationException( String.format( + "Custom type '%s' implements 'AnnotationBasedUserType' but no custom type annotation is present", + annotationBased.getClass().getName() ) ); + } + annotationBased.initialize( castAnnotationType( typeAnnotation, annotationBased ), creationContext ); + } + + private class TypeCreationContext implements UserTypeCreationContext { + private final Properties parameters; - if ( typeInstance instanceof DynamicParameterizedType ) { - if ( parseBoolean( properties.getProperty( DynamicParameterizedType.IS_DYNAMIC ) ) ) { - if ( properties.get( DynamicParameterizedType.PARAMETER_TYPE ) == null ) { - properties.put( DynamicParameterizedType.PARAMETER_TYPE, createParameterType() ); + private TypeCreationContext(Properties parameters) { + this.parameters = parameters; + } + + @Override + public MetadataBuildingContext getBuildingContext() { + return BasicValue.this.getBuildingContext(); + } + + @Override + public ServiceRegistry getServiceRegistry() { + return BasicValue.this.getServiceRegistry(); + } + + @Override + public MemberDetails getMemberDetails() { + return BasicValue.this.getMemberDetails(); + } + + @Override + public Properties getParameters() { + return parameters; + } + } + + private A castAnnotationType( + Annotation typeAnnotation, AnnotationBasedUserType annotationBased ) { + final var annotationType = annotationBased.getClass(); + for ( var iface: annotationType.getGenericInterfaces() ) { + if ( iface instanceof ParameterizedType parameterizedType + && parameterizedType.getRawType() == AnnotationBasedUserType.class ) { + final var typeArguments = parameterizedType.getActualTypeArguments(); + if ( typeArguments.length > 0 + && typeArguments[0] instanceof Class annotationClass ) { + if ( !annotationClass.isInstance( typeAnnotation ) ) { + throw new AnnotationException( String.format( "Annotation '%s' is not assignable to '%s'", + annotationType.getName(), iface.getTypeName() ) ); + } + @SuppressWarnings("unchecked") // safe, we just checked it + final var castAnnotation = (A) typeAnnotation; + return castAnnotation; } } } + throw new AssertionFailure( "Could not find implementing interface" ); + } - injectParameters( typeInstance, properties ); - // envers - grr - setTypeParameters( properties ); + private , A extends Annotation> T instantiateUserType( + Class customType, A typeAnnotation, + UserTypeCreationContext creationContext) { + try { + T userType; + if ( typeAnnotation != null ) { + @SuppressWarnings("unchecked") // totally safe + final var annotationType = (Class) typeAnnotation.annotationType(); + // attempt to instantiate it with the annotation and context object as constructor arguments + userType = + Constructors.construct( customType, + annotationType, typeAnnotation, + UserTypeCreationContext.class, creationContext ); + if ( userType != null ) { + return userType; + } + // attempt to instantiate it with the annotation as a constructor argument + userType = Constructors.construct( customType, annotationType, typeAnnotation ); + if ( userType != null ) { + return userType; + } + } - return typeInstance; + // attempt to instantiate it with the context object as a constructor argument + userType = Constructors.construct( customType, UserTypeCreationContext.class, creationContext ); + if ( userType != null ) { + return userType; + } + } + catch (InvocationTargetException | InstantiationException | IllegalAccessException e) { + throw new org.hibernate.InstantiationException( "Could not instantiate custom type", customType, e ); + } + + return getBuildingContext().getBuildingOptions().isAllowExtensionsInCdi() + ? getUserTypeBean( customType, creationContext.getParameters() ).getBeanInstance() + : FallbackBeanInstanceProducer.INSTANCE.produceBeanInstance( customType ); } private ManagedBean getUserTypeBean(Class explicitCustomType, Properties properties) { diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/Collection.java b/hibernate-core/src/main/java/org/hibernate/mapping/Collection.java index df98cf2be022..4af5c8b9b276 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/Collection.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/Collection.java @@ -17,6 +17,7 @@ import org.hibernate.internal.util.PropertiesHelper; import org.hibernate.internal.util.StringHelper; import org.hibernate.jdbc.Expectation; +import org.hibernate.persister.state.spi.StateManagement; import org.hibernate.resource.beans.spi.ManagedBean; import org.hibernate.service.ServiceRegistry; import org.hibernate.type.CollectionType; @@ -27,8 +28,10 @@ import java.util.ArrayList; import java.util.Comparator; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Properties; import java.util.function.Supplier; @@ -101,9 +104,15 @@ public abstract sealed class Collection private String customSQLDeleteAll; private boolean customDeleteAllCallable; - private Column softDeleteColumn; private SoftDeleteType softDeleteStrategy; + private Class stateManagementType; + private Table auxiliaryTable; + private boolean partitioned; + private Map auxiliaryColumns; + private String auxiliaryColumnInPrimaryKey; + private boolean primaryKeyDisabled; + private String loaderName; private Supplier insertExpectation; @@ -177,6 +186,16 @@ protected Collection(Collection original) { this.deleteExpectation = original.deleteExpectation; this.deleteAllExpectation = original.deleteAllExpectation; this.loaderName = original.loaderName; + this.auxiliaryTable = original.auxiliaryTable; + this.auxiliaryColumns = original.auxiliaryColumns == null ? null : new HashMap<>( original.auxiliaryColumns ); + this.stateManagementType = original.stateManagementType; + this.auxiliaryColumnInPrimaryKey = original.auxiliaryColumnInPrimaryKey; + this.primaryKeyDisabled = original.primaryKeyDisabled; + this.softDeleteStrategy = original.softDeleteStrategy; + this.partitioned = original.partitioned; + this.queryCacheLayout = original.queryCacheLayout; + this.cachedCollectionType = original.cachedCollectionType; + this.cachedCollectionSemantics = original.cachedCollectionSemantics; } @Override @@ -396,7 +415,7 @@ public void validate(MappingContext mappingContext) throws MappingException { private void checkColumnDuplication() throws MappingException { final String owner = "collection '" + getReferencedPropertyName() + "'"; - final HashSet cols = new HashSet<>(); + final HashSet cols = new HashSet<>(); getKey().checkColumnDuplication( cols, owner ); if ( isIndexed() ) { ( (IndexedCollection) this ).getIndex().checkColumnDuplication( cols, owner ); @@ -424,6 +443,11 @@ public int getColumnSpan() { return 0; } + @Override + public boolean hasColumns() { + return false; + } + @Override public Type getType() throws MappingException { return getCollectionType(); @@ -557,11 +581,55 @@ private void createForeignKeys() throws MappingException { public void createAllKeys() throws MappingException { createForeignKeys(); - if ( !isInverse() ) { + if ( !isInverse() && !isPrimaryKeyDisabled() ) { createPrimaryKey(); + adjustTemporalPrimaryKey(); + } + } + + private void adjustTemporalPrimaryKey() { + if ( isAuxiliaryColumnInPrimaryKey() ) { + final var startingColumn = getAuxiliaryColumn( auxiliaryColumnInPrimaryKey ); + if ( startingColumn != null ) { + final var primaryKey = collectionTable.getPrimaryKey(); + if ( primaryKey != null ) { + if ( !primaryKey.containsColumn( startingColumn ) ) { + primaryKey.addColumn( startingColumn ); + } + } + // TODO: we should probably only do this for the UK created in + // Set.createPrimaryKey() and not one the user defined + else if ( !collectionTable.getUniqueKeys().isEmpty() ) { + for ( var uniqueKey : collectionTable.getUniqueKeys().values() ) { + if ( !uniqueKey.containsColumn( startingColumn ) ) { + uniqueKey.addColumn( startingColumn ); + } + } + } + } } } + @Override + public boolean isPrimaryKeyDisabled() { + return primaryKeyDisabled; + } + + @Override + public void setPrimaryKeyDisabled(boolean disabled) { + this.primaryKeyDisabled = disabled; + } + + @Override + public void setAuxiliaryColumnInPrimaryKey(String key) { + this.auxiliaryColumnInPrimaryKey = key; + } + + @Override + public boolean isAuxiliaryColumnInPrimaryKey() { + return auxiliaryColumnInPrimaryKey != null; + } + public String getCacheConcurrencyStrategy() { return cacheConcurrencyStrategy; } @@ -770,6 +838,14 @@ public boolean[] getColumnUpdateability() { return EMPTY_BOOLEAN_ARRAY; } + @Override + public void setNonInsertable() { + } + + @Override + public void setNonUpdatable() { + } + @Override public boolean hasAnyUpdatableColumns() { return false; @@ -831,7 +907,7 @@ public boolean isColumnUpdateable(int index) { @Override public void enableSoftDelete(Column indicatorColumn, SoftDeleteType strategy) { - this.softDeleteColumn = indicatorColumn; + SoftDeletable.super.enableSoftDelete( indicatorColumn, strategy ); this.softDeleteStrategy = strategy; } @@ -841,8 +917,18 @@ public SoftDeleteType getSoftDeleteStrategy() { } @Override - public Column getSoftDeleteColumn() { - return softDeleteColumn; + public Table getMainTable() { + return collectionTable; + } + + @Override + public boolean isMainTablePartitioned() { + return partitioned; + } + + @Override + public void setMainTablePartitioned(boolean partitioned) { + this.partitioned = partitioned; } public Supplier getInsertExpectation() { @@ -881,4 +967,32 @@ public void setDeleteAllExpectation(Supplier deleteAllExp public boolean isPartitionKey() { return false; } + + public void setStateManagementType(Class stateManagementType) { + this.stateManagementType = stateManagementType; + } + + public Class getStateManagementType() { + return stateManagementType; + } + + public Table getAuxiliaryTable() { + return auxiliaryTable; + } + + public void setAuxiliaryTable(Table auxiliaryTable) { + this.auxiliaryTable = auxiliaryTable; + } + + public Column getAuxiliaryColumn(String column) { + return auxiliaryColumns == null ? null + : auxiliaryColumns.get( column ); + } + + public void addAuxiliaryColumn(String name, Column column) { + if ( auxiliaryColumns == null ) { + auxiliaryColumns = new HashMap<>(); + } + auxiliaryColumns.put( name, column ); + } } 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 f72ee0bea568..8c96785fa58d 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/Column.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/Column.java @@ -10,6 +10,7 @@ import java.util.Locale; import java.util.Objects; +import org.checkerframework.checker.nullness.qual.NonNull; import org.hibernate.AssertionFailure; import org.hibernate.Internal; import org.hibernate.MappingException; @@ -30,9 +31,6 @@ import org.hibernate.type.Type; import org.hibernate.type.descriptor.JdbcTypeNameMapper; import org.hibernate.type.descriptor.jdbc.JdbcType; -import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; -import org.hibernate.type.descriptor.sql.DdlType; -import org.hibernate.type.descriptor.sql.spi.DdlTypeRegistry; import org.hibernate.type.MappingContext; import org.hibernate.type.spi.TypeConfiguration; @@ -141,8 +139,12 @@ public void setName(String name) { @Internal public Identifier getNameIdentifier(MetadataBuildingContext buildingContext) { - return buildingContext.getMetadataCollector().getDatabase() - .toIdentifier( getQuotedName() ); + return getNameIdentifier( buildingContext.getMetadataCollector().getDatabase() ); + } + + @Internal + public Identifier getNameIdentifier(Database database) { + return database.toIdentifier( getQuotedName() ); } public boolean isExplicit() { @@ -195,35 +197,42 @@ public String getQuotedName(Dialect dialect) { @Override public String getAlias(Dialect dialect) { - final int lastLetter = lastIndexOfLetter( name ); final String suffix = AliasConstantsHelper.get( uniqueInteger ); + return qualifyAlias( dialect, suffix, aliasRoot() ); + } - final String alias; + private @NonNull String aliasRoot() { + final int lastLetter = lastIndexOfLetter( name ); if ( lastLetter == -1 ) { - alias = "column"; + return "column"; } else { final String lowerCaseName = name.toLowerCase( Locale.ROOT ); - alias = lowerCaseName.length() > lastLetter + 1 + return lowerCaseName.length() > lastLetter + 1 ? lowerCaseName.substring( 0, lastLetter + 1 ) : lowerCaseName; } + } + private @NonNull String qualifyAlias(Dialect dialect, String suffix, String alias) { + final int suffixLength = suffix.length(); + final int maxAliasLength = dialect.getMaxAliasLength(); + final int freeLength = maxAliasLength - suffixLength; final boolean useRawName = - name.length() + suffix.length() <= dialect.getMaxAliasLength() + name.length() <= freeLength && !quoted && !name.equalsIgnoreCase( dialect.rowId(null) ); if ( !useRawName ) { - if ( suffix.length() >= dialect.getMaxAliasLength() ) { + if ( suffixLength >= maxAliasLength ) { throw new MappingException( String.format( "Unique suffix [%s] length must be less than maximum [%d]", - suffix, dialect.getMaxAliasLength() + suffix, maxAliasLength ) ); } - if ( alias.length() + suffix.length() > dialect.getMaxAliasLength() ) { - return alias.substring( 0, dialect.getMaxAliasLength() - suffix.length() ) + suffix; + if ( alias.length() > freeLength ) { + return alias.substring( 0, freeLength ) + suffix; } } return alias + suffix; @@ -283,50 +292,23 @@ public boolean equals(Column column) { public int getSqlTypeCode(MappingContext mapping) throws MappingException { if ( sqlTypeCode == null ) { - final Type type = getValue().getType(); - final int[] sqlTypeCodes; - try { - sqlTypeCodes = type.getSqlTypeCodes( mapping ); - } - catch ( Exception cause ) { - throw new MappingException( - String.format( - Locale.ROOT, - "Unable to resolve JDBC type code for column '%s' of table '%s'", - getName(), - getValue().getTable().getName() - ), - cause - ); - } - final int index = getTypeIndex(); - if ( index >= sqlTypeCodes.length ) { - throw new MappingException( - String.format( - Locale.ROOT, - "Unable to resolve JDBC type code for column '%s' of table '%s'", - getName(), - getValue().getTable().getName() - ) - ); - } - sqlTypeCode = sqlTypeCodes[index]; + sqlTypeCode = getSqlTypeCode( mapping, getValue().getType() ); } return sqlTypeCode; } + private int getSqlTypeCode(MappingContext mapping, Type type) { + return ( (BasicType) getUnderlyingType( mapping, type, typeIndex ) ) + .getJdbcType() + .getDefaultSqlTypeCode(); + } + private String getSqlTypeName(TypeConfiguration typeConfiguration, Dialect dialect, MappingContext mapping) { if ( sqlTypeName == null ) { - final DdlTypeRegistry ddlTypeRegistry = typeConfiguration.getDdlTypeRegistry(); - final JdbcTypeRegistry jdbcTypeRegistry = typeConfiguration.getJdbcTypeRegistry(); - final int sqlTypeCode = getSqlTypeCode( mapping ); - final JdbcType jdbcType = - jdbcTypeRegistry.getConstructor( sqlTypeCode ) == null - ? jdbcTypeRegistry.findDescriptor( sqlTypeCode ) - : ( (BasicType) getUnderlyingType( mapping, getValue().getType(), typeIndex ) ).getJdbcType(); - final DdlType descriptor = jdbcType == null - ? null - : ddlTypeRegistry.getDescriptor( jdbcType.getDdlTypeCode() ); + final var ddlTypeRegistry = typeConfiguration.getDdlTypeRegistry(); + final var type = ( (BasicType) getUnderlyingType( mapping, getValue().getType(), typeIndex ) ); + final var jdbcType = type.getJdbcType(); + final var descriptor = ddlTypeRegistry.getDescriptor( jdbcType.getDdlTypeCode() ); if ( descriptor == null ) { throw new MappingException( String.format( @@ -334,19 +316,19 @@ private String getSqlTypeName(TypeConfiguration typeConfiguration, Dialect diale "Unable to determine SQL type name for column '%s' of table '%s' because there is no type mapping for org.hibernate.type.SqlTypes code: %s (%s)", getName(), getValue().getTable().getName(), - sqlTypeCode, - JdbcTypeNameMapper.getTypeName( sqlTypeCode ) + jdbcType.getDefaultSqlTypeCode(), + JdbcTypeNameMapper.getTypeName( jdbcType.getDefaultSqlTypeCode() ) ) ); } try { - final Size size = getColumnSize( dialect, mapping ); - sqlTypeName = descriptor.getTypeName( - size, - getUnderlyingType( mapping, getValue().getType(), typeIndex ), - ddlTypeRegistry - ); + final var size = getColumnSize( dialect, mapping ); + sqlTypeName = descriptor.getTypeName( size, type, ddlTypeRegistry ); sqlTypeLob = descriptor.isLob( size ); + // TODO: this is rubbish (could not find another way) + if ( dialect.getAggregateSupport().useLengthsInCasts() ) { + length = size.getLength(); + } } catch ( Exception cause ) { throw new MappingException( @@ -367,7 +349,7 @@ private String getSqlTypeName(TypeConfiguration typeConfiguration, Dialect diale private static Type getUnderlyingType(MappingContext mappingContext, Type type, int typeIndex) { if ( type instanceof ComponentType componentType ) { int cols = 0; - for ( Type subtype : componentType.getSubtypes() ) { + for ( var subtype : componentType.getSubtypes() ) { final int columnSpan = subtype.getColumnSpan( mappingContext ); if ( cols+columnSpan > typeIndex ) { return getUnderlyingType( mappingContext, subtype, typeIndex-cols ); @@ -377,7 +359,7 @@ private static Type getUnderlyingType(MappingContext mappingContext, Type type, throw new IndexOutOfBoundsException(); } else if ( type instanceof EntityType entityType ) { - final Type idType = entityType.getIdentifierOrUniqueKeyType( mappingContext ); + final var idType = entityType.getIdentifierOrUniqueKeyType( mappingContext ); return getUnderlyingType( mappingContext, idType, typeIndex ); } else { @@ -406,7 +388,7 @@ public void setSqlTypeCode(Integer typeCode) { } public String getSqlType(Metadata mapping) { - final Database database = mapping.getDatabase(); + final var database = mapping.getDatabase(); return getSqlTypeName( database.getTypeConfiguration(), database.getDialect(), mapping ); } @@ -448,22 +430,21 @@ public Size getColumnSize(Dialect dialect, MappingContext mappingContext) { } Size calculateColumnSize(Dialect dialect, MappingContext mappingContext) { - Type type = getValue().getType(); - Long lengthToUse = getLength(); - Integer precisionToUse = getPrecision(); - Integer scaleToUse = getScale(); + var lengthToUse = getLength(); + var precisionToUse = getPrecision(); + var scaleToUse = getScale(); + var type = getValue().getType(); if ( type instanceof EntityType ) { type = getTypeForEntityValue( mappingContext, type, getTypeIndex() ); } - if ( type instanceof ComponentType ) { - type = getTypeForComponentValue( mappingContext, type, getTypeIndex() ); + if ( type instanceof ComponentType componentType ) { + type = getTypeForComponentValue( mappingContext, componentType, getTypeIndex() ); } - if ( type instanceof BasicType basicType ) { - if ( isTemporal( basicType.getExpressibleJavaType() ) ) { - precisionToUse = getTemporalPrecision(); - lengthToUse = null; - scaleToUse = null; - } + if ( type instanceof BasicType basicType + && isTemporal( basicType.getExpressibleJavaType() ) ) { + precisionToUse = getTemporalPrecision(); + lengthToUse = null; + scaleToUse = null; } if ( type == null ) { throw new AssertionFailure( "no typing information available to determine column size" ); @@ -480,23 +461,25 @@ Size calculateColumnSize(Dialect dialect, MappingContext mappingContext) { return size; } - private Type getTypeForComponentValue(MappingContext mappingContext, Type type, int typeIndex) { - final Type[] subtypes = ( (ComponentType) type ).getSubtypes(); + private Type getTypeForComponentValue(MappingContext mappingContext, ComponentType type, int typeIndex) { + final var subtypes = type.getSubtypes(); int typeStartIndex = 0; - for ( Type subtype : subtypes ) { + for ( var subtype : subtypes ) { final int columnSpan = subtype.getColumnSpan( mappingContext ); if ( typeStartIndex + columnSpan > typeIndex ) { final int subtypeIndex = typeIndex - typeStartIndex; if ( subtype instanceof EntityType ) { return getTypeForEntityValue( mappingContext, subtype, subtypeIndex ); } - if ( subtype instanceof ComponentType ) { - return getTypeForComponentValue( mappingContext, subtype, subtypeIndex ); + else if ( subtype instanceof ComponentType componentType ) { + return getTypeForComponentValue( mappingContext, componentType, subtypeIndex ); } - if ( subtypeIndex == 0 ) { + else if ( subtypeIndex == 0 ) { return subtype; } - break; + else { + break; + } } typeStartIndex += columnSpan; } @@ -514,11 +497,20 @@ private Type getTypeForComponentValue(MappingContext mappingContext, Type type, private Type getTypeForEntityValue(MappingContext mappingContext, Type type, int typeIndex) { int index = 0; if ( type instanceof EntityType entityType ) { - return getTypeForEntityValue( mappingContext, entityType.getIdentifierOrUniqueKeyType( mappingContext ), typeIndex ); + return getTypeForEntityValue( + mappingContext, + entityType.getIdentifierOrUniqueKeyType( mappingContext ), + typeIndex + ); } else if ( type instanceof ComponentType componentType ) { - for ( Type subtype : componentType.getSubtypes() ) { - final Type result = getTypeForEntityValue( mappingContext, subtype, typeIndex - index ); + for ( var subtype : componentType.getSubtypes() ) { + final var result = + getTypeForEntityValue( + mappingContext, + subtype, + typeIndex - index + ); if ( result != null ) { return result; } @@ -557,13 +549,10 @@ public boolean isSqlTypeLob(Metadata mapping) { try { final int typeCode = getSqlTypeCode( mapping ); final var ddlType = ddlTypeRegistry.getDescriptor( typeCode ); - if ( ddlType == null ) { - sqlTypeLob = JdbcType.isLob( typeCode ); - } - else { - final Size size = getColumnSize( dialect, mapping ); - sqlTypeLob = ddlType.isLob( size ); - } + sqlTypeLob = + ddlType == null + ? JdbcType.isLob( typeCode ) + : ddlType.isLob( getColumnSize( dialect, mapping ) ); } catch ( MappingException cause ) { throw cause; 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 1ee0aed059a5..b393c755b8e2 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/Component.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/Component.java @@ -16,6 +16,7 @@ import org.hibernate.Internal; import org.hibernate.MappingException; +import org.hibernate.boot.model.internal.GeneratorBinder; import org.hibernate.boot.model.relational.Database; import org.hibernate.boot.model.relational.ExportableProducer; import org.hibernate.boot.model.relational.QualifiedName; @@ -28,8 +29,8 @@ import org.hibernate.generator.BeforeExecutionGenerator; import org.hibernate.generator.Generator; import org.hibernate.id.CompositeNestedGeneratedValueGenerator; +import org.hibernate.id.CompositeNestedGeneratedValueGenerator.GenerationPlan; import org.hibernate.id.Configurable; -import org.hibernate.id.IdentifierGenerationException; import org.hibernate.internal.util.ReflectHelper; import org.hibernate.internal.util.collections.ArrayHelper; import org.hibernate.internal.util.collections.CollectionHelper; @@ -246,6 +247,17 @@ public int getColumnSpan() { return getSelectables().size(); } + @Override + public boolean hasColumns() { + for ( var property : properties ) { + if ( property.hasColumns() ) { + return true; + } + } + return discriminator != null + && discriminator.hasColumns(); + } + public List getAggregatedColumns() { final List aggregatedColumns = new ArrayList<>( getPropertySpan() ); collectAggregatedColumns( aggregatedColumns, this ); @@ -312,16 +324,16 @@ public void setStructName(QualifiedName structName) { } @Override - public void checkColumnDuplication(Set distinctColumns, String owner) { + public void checkColumnDuplication(Set distinctColumns, String owner) { if ( aggregateColumn == null ) { if ( isPolymorphic() ) { // We can allow different subtypes reusing the same columns // since only one subtype can exist at one time - final Map> distinctColumnsByClass = new HashMap<>(); + final Map> distinctColumnsByClass = new HashMap<>(); for ( var prop : properties ) { if ( prop.isUpdatable() || prop.isInsertable() ) { final String declaringClass = propertyDeclaringClasses.get( prop ); - final Set set = distinctColumnsByClass.computeIfAbsent( + final Set set = distinctColumnsByClass.computeIfAbsent( declaringClass, k -> new HashSet<>( distinctColumns ) ); @@ -490,7 +502,7 @@ public boolean isSame(Component other) { @Override public boolean[] getColumnInsertability() { - final boolean[] result = new boolean[getColumnSpan()]; + final var result = new boolean[getColumnSpan()]; int i = 0; for ( var property : getProperties() ) { i += copyFlags( property.getValue().getColumnInsertability(), result, i, property.isInsertable() ); @@ -522,7 +534,7 @@ public boolean hasAnyInsertableColumns() { @Override public boolean[] getColumnUpdateability() { - final boolean[] result = new boolean[getColumnSpan()]; + final var result = new boolean[getColumnSpan()]; int i = 0; for ( var property : getProperties() ) { i += copyFlags( property.getValue().getColumnUpdateability(), result, i, property.isUpdatable() ); @@ -534,6 +546,26 @@ public boolean[] getColumnUpdateability() { return result; } + @Override + public void setNonUpdatable() { + for ( var property : properties ) { + property.getValue().setNonUpdatable(); + } + if ( isPolymorphic() ) { + getDiscriminator().setNonUpdatable(); + } + } + + @Override + public void setNonInsertable() { + for ( var property : properties ) { + property.getValue().setNonInsertable(); + } + if ( isPolymorphic() ) { + getDiscriminator().setNonInsertable(); + } + } + @Override public boolean hasAnyUpdatableColumns() { for ( var property : properties ) { @@ -582,8 +614,8 @@ public boolean matchesAllProperties(String... propertyNames) { } public boolean hasProperty(String propertyName) { - for ( Property prop : properties ) { - if ( prop.getName().equals(propertyName) ) { + for ( var property : properties ) { + if ( property.getName().equals(propertyName) ) { return true; } } @@ -666,81 +698,10 @@ public String toString() { @Override public Generator createGenerator(Dialect dialect, RootClass rootClass, Property property, GeneratorSettings defaults) { return getCustomIdGeneratorCreator().isAssigned() - ? buildIdentifierGenerator( dialect, rootClass, defaults ) + ? GeneratorBinder.buildIdentifierGenerator( this, dialect, rootClass, defaults ) : super.createGenerator( dialect, rootClass, property, defaults ); } - private Generator buildIdentifierGenerator(Dialect dialect, RootClass rootClass, GeneratorSettings defaults) { - final var generator = - new CompositeNestedGeneratedValueGenerator( - new StandardGenerationContextLocator( rootClass.getEntityName() ), - getType() - ); - final var properties = getProperties(); - for ( int i = 0; i < properties.size(); i++ ) { - final var property = properties.get( i ); - if ( property.getValue().isSimpleValue() ) { - final SimpleValue value = (SimpleValue) property.getValue(); - if ( !value.getCustomIdGeneratorCreator().isAssigned() ) { - // skip any 'assigned' generators, they would have been - // handled by the StandardGenerationContextLocator - if ( value.createGenerator( dialect, rootClass, property, defaults ) - instanceof BeforeExecutionGenerator beforeExecutionGenerator ) { - generator.addGeneratedValuePlan( new ValueGenerationPlan( - beforeExecutionGenerator, - getType().isMutable() - ? injector( property, getAttributeDeclarer( rootClass ) ) - : null, - i - ) ); - } - else { - throw new IdentifierGenerationException( "Identity generation isn't supported for composite ids" ); - } - } - } - } - return generator; - } - - /** - * Return the class that declares the composite pk attributes, - * which might be an {@code @IdClass}, an {@code @EmbeddedId}, - * of the entity class itself. - */ - private Class getAttributeDeclarer(RootClass rootClass) { - // See the javadoc discussion on CompositeNestedGeneratedValueGenerator - // for the various scenarios we need to account for here - if ( rootClass.getIdentifierMapper() != null ) { - // we have the @IdClass / case - return resolveComponentClass(); - } - else if ( rootClass.getIdentifierProperty() != null ) { - // we have the "@EmbeddedId" / case - return resolveComponentClass(); - } - else { - // we have the "straight up" embedded (again the Hibernate term) - // component identifier: the entity class itself is the id class - return rootClass.getMappedClass(); - } - } - - private Setter injector(Property property, Class attributeDeclarer) { - return property.getPropertyAccessStrategy( attributeDeclarer ) - .buildPropertyAccess( attributeDeclarer, property.getName(), true ) - .getSetter(); - } - - private Class resolveComponentClass() { - try { - return getComponentClass(); - } - catch ( Exception e ) { - return null; - } - } - @Internal public String[] getPropertyNames() { final String[] propertyNames = new String[properties.size()]; @@ -786,7 +747,7 @@ public Object locateGenerationContext(SharedSessionContractImplementor session, } } - public static class ValueGenerationPlan implements CompositeNestedGeneratedValueGenerator.GenerationPlan { + public static class ValueGenerationPlan implements GenerationPlan { private final BeforeExecutionGenerator generator; private final Setter injector; private final int propertyIndex; @@ -850,9 +811,10 @@ public boolean isValid(MappingContext mappingContext) throws MappingException { } if ( assignedPropertyNames.size() != properties.size() ) { final ArrayList missingProperties = new ArrayList<>(); - for ( Property property : properties ) { - if ( !assignedPropertyNames.contains( property.getName() ) ) { - missingProperties.add( property.getName() ); + for ( var property : properties ) { + final String propertyName = property.getName(); + if ( !assignedPropertyNames.contains( propertyName ) ) { + missingProperties.add( propertyName ); } } throw new MappingException( "component type [" + componentClassName + "] has " + properties.size() + " properties but the instantiator only assigns " + assignedPropertyNames.size() + " properties. missing properties: " + missingProperties ); @@ -886,7 +848,7 @@ private int[] sortProperties(boolean forceRetainOriginalOrder) { // because XML mappings might refer to this through the defined order if ( forceRetainOriginalOrder || isAlternateUniqueKey() || isEmbedded() || getBuildingContext() instanceof MappingDocument ) { - final Property[] originalProperties = properties.toArray( new Property[0] ); + final var originalProperties = properties.toArray( new Property[0] ); properties.sort( Comparator.comparing( Property::getName ) ); originalPropertyOrder = new int[originalProperties.length]; for ( int j = 0; j < originalPropertyOrder.length; j++ ) { @@ -944,6 +906,15 @@ public boolean isSimpleRecord() { return simple; } + private Class resolveComponentClass() { + try { + return getComponentClass(); + } + catch ( Exception e ) { + return null; + } + } + public Class getCustomInstantiator() { return customInstantiator; } diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/Contributable.java b/hibernate-core/src/main/java/org/hibernate/mapping/Contributable.java index 6536e6d85200..4c5c956bfd66 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/Contributable.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/Contributable.java @@ -9,7 +9,7 @@ /** * Parts of the mapping model which are associated with a * {@linkplain #getContributor() contributor} (ORM, Envers, etc). - *

    + *

    * The most useful aspect of this is the {@link ContributableDatabaseObject} * specialization. * diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/IdentifiableTypeClass.java b/hibernate-core/src/main/java/org/hibernate/mapping/IdentifiableTypeClass.java index ef3a0211018b..75f119296953 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/IdentifiableTypeClass.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/IdentifiableTypeClass.java @@ -18,8 +18,13 @@ public interface IdentifiableTypeClass extends TableContainer { List getDeclaredProperties(); + Component getIdentifierMapper(); + Table getImplicitTable(); + boolean isVersioned(); + Property getVersion(); + /** * @deprecated No longer used */ diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/IdentifierCollection.java b/hibernate-core/src/main/java/org/hibernate/mapping/IdentifierCollection.java index f83bbd1ded5e..86fff6980020 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/IdentifierCollection.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/IdentifierCollection.java @@ -65,7 +65,7 @@ void createPrimaryKey() { if ( !isOneToMany() ) { final var primaryKey = new PrimaryKey( getCollectionTable() ); primaryKey.addColumns( getIdentifier() ); - getCollectionTable().setPrimaryKey(primaryKey); + getCollectionTable().setPrimaryKey( primaryKey ); } // create an index on the key columns?? } diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/Join.java b/hibernate-core/src/main/java/org/hibernate/mapping/Join.java index c6d8513e02d5..dfe48165b9a7 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/Join.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/Join.java @@ -115,7 +115,7 @@ public void disableForeignKeyCreation() { public void createForeignKey() { final var foreignKey = getKey().createForeignKeyOfEntity( persistentClass.getEntityName() ); - if ( disableForeignKeyCreation ) { + if ( foreignKey != null && disableForeignKeyCreation ) { foreignKey.disableCreation(); } } diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/JoinedSubclass.java b/hibernate-core/src/main/java/org/hibernate/mapping/JoinedSubclass.java index 14917846d48c..707ed0a41366 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/JoinedSubclass.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/JoinedSubclass.java @@ -4,8 +4,6 @@ */ package org.hibernate.mapping; -import java.util.List; - import org.hibernate.MappingException; import org.hibernate.boot.Metadata; import org.hibernate.boot.spi.MetadataBuildingContext; @@ -55,10 +53,6 @@ public void validate(Metadata mapping) throws MappingException { } } - public List getReferenceableProperties() { - return getProperties(); - } - public Object accept(PersistentClassVisitor mv) { return mv.accept(this); } diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/ManyToOne.java b/hibernate-core/src/main/java/org/hibernate/mapping/ManyToOne.java index af73c108a036..dddfb193c005 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/ManyToOne.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/ManyToOne.java @@ -95,7 +95,7 @@ public void createPropertyRefConstraints(Map persistent component.sortProperties(); } // todo : if "none" another option is to create the ForeignKey object still but to set its #disableCreation flag - if ( isForeignKeyEnabled() && !hasFormula() ) { + if ( isConstrained() && !hasAuxiliaryColumnInPrimaryKey( referencedClass ) ) { final var foreignKey = getTable().createForeignKey( getForeignKeyName(), getConstraintColumns(), diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/MappedSuperclass.java b/hibernate-core/src/main/java/org/hibernate/mapping/MappedSuperclass.java index c0e0a879b347..279a2702d67b 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/MappedSuperclass.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/MappedSuperclass.java @@ -49,6 +49,7 @@ public boolean hasIdentifierProperty() { return getIdentifierProperty() != null; } + @Override public boolean isVersioned() { return getVersion() != null; } @@ -63,6 +64,7 @@ public PersistentClass getSuperPersistentClass() { return superPersistentClass; } + @Override public List getDeclaredProperties() { return declaredProperties; } @@ -109,6 +111,7 @@ public void setDeclaredIdentifierProperty(Property prop) { this.identifierProperty = prop; } + @Override public Property getVersion() { //get direct version or the one from the super mappedSuperclass // or the one from the super persistentClass @@ -132,6 +135,7 @@ public void setDeclaredVersion(Property prop) { this.version = prop; } + @Override public Component getIdentifierMapper() { //get direct identifierMapper or the one from the super mappedSuperclass // or the one from the super persistentClass diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/MappingHelper.java b/hibernate-core/src/main/java/org/hibernate/mapping/MappingHelper.java index bfe537b75570..a10dfd72f6ba 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/MappingHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/MappingHelper.java @@ -16,6 +16,7 @@ import org.hibernate.resource.beans.internal.FallbackBeanInstanceProducer; import org.hibernate.resource.beans.spi.ManagedBean; import org.hibernate.resource.beans.spi.ProvidedInstanceManagedBeanImpl; +import org.hibernate.usertype.AnnotationBasedUserType; import org.hibernate.usertype.ParameterizedType; import org.hibernate.usertype.UserCollectionType; @@ -75,9 +76,10 @@ public static void injectParameters(Object type, Properties parameters) { if ( type instanceof ParameterizedType parameterizedType ) { parameterizedType.setParameterValues( parameters == null ? EMPTY_PROPERTIES : parameters ); } - else if ( parameters != null && !parameters.isEmpty() ) { + else if ( parameters != null && !parameters.isEmpty() + && !( type instanceof AnnotationBasedUserType ) ) { throw new MappingException( "'UserType' implementation '" + type.getClass().getName() - + "' does not implement 'ParameterizedType' but parameters were provided" ); + + "' does not implement 'ParameterizedType' or 'AnnotationBasedUserType' but parameters were provided" ); } } @@ -102,11 +104,11 @@ private static ManagedBean createLocalUserTypeBean( } public static void checkPropertyColumnDuplication( - Set distinctColumns, + Set distinctColumns, List properties, String owner) throws MappingException { for ( var property : properties ) { - if ( property.isUpdatable() || property.isInsertable() ) { + if ( ( property.isUpdatable() || property.isInsertable() ) && !property.isGenericSpecialization() ) { property.getValue().checkColumnDuplication( distinctColumns, owner ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/OneToMany.java b/hibernate-core/src/main/java/org/hibernate/mapping/OneToMany.java index 7d0f98fdd0bd..6709c3689dca 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/OneToMany.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/OneToMany.java @@ -93,6 +93,11 @@ public int getColumnSpan() { return associatedClass.getKey().getColumnSpan(); } + @Override + public boolean hasColumns() { + return associatedClass.getKey().hasColumns(); + } + @Override public FetchMode getFetchMode() { return FetchMode.JOIN; @@ -203,6 +208,14 @@ public boolean hasAnyUpdatableColumns() { return false; } + @Override + public void setNonInsertable() { + } + + @Override + public void setNonUpdatable() { + } + public NotFoundAction getNotFoundAction() { return notFoundAction; } diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/OneToOne.java b/hibernate-core/src/main/java/org/hibernate/mapping/OneToOne.java index 22b99da2bbe5..1ac63b1a3d94 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/OneToOne.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/OneToOne.java @@ -61,7 +61,7 @@ public String getEntityName() { } public OneToOneType getType() throws MappingException { - if ( getColumnSpan()>0 ) { + if ( hasColumns() ) { return new SpecialOneToOneType( getTypeConfiguration(), getReferencedEntityName(), @@ -93,7 +93,7 @@ public OneToOneType getType() throws MappingException { @Override public void createUniqueKey(MetadataBuildingContext context) { - if ( !hasFormula() && getColumnSpan()>0 ) { + if ( !hasFormula() && hasColumns() ) { getTable().createUniqueKey( getConstraintColumns(), context ); } } @@ -117,6 +117,11 @@ public boolean isConstrained() { return constrained; } + @Override + boolean isActuallyConstrained() { + return constrained && super.isConstrained(); + } + /** * Returns the foreignKeyType. * @return AssociationType.ForeignKeyType diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/PersistentClass.java b/hibernate-core/src/main/java/org/hibernate/mapping/PersistentClass.java index b53e780c0b5a..fee402e687f4 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/PersistentClass.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/PersistentClass.java @@ -315,6 +315,7 @@ public String getEntityName() { public abstract KeyValue getIdentifier(); + @Override public abstract Property getVersion(); public abstract Property getDeclaredVersion(); @@ -325,9 +326,9 @@ public String getEntityName() { public abstract boolean isPolymorphic(); + @Override public abstract boolean isVersioned(); - public boolean isCached() { return isCached; } @@ -362,6 +363,8 @@ public boolean isExplicitPolymorphism() { public abstract List getPropertyClosure(); + public abstract List getAllPropertyClosure(); + public abstract List

  • Configuration parameters
    getTableClosure(); public abstract List getKeyClosure(); @@ -431,8 +434,12 @@ public void setEntityName(String entityName) { } public void createPrimaryKey() { - //Primary key constraint final var table = getTable(); + final var primaryKey = makePrimaryKey( table ); + table.setPrimaryKey( primaryKey ); + } + + PrimaryKey makePrimaryKey(Table table) { final var primaryKey = new PrimaryKey( table ); primaryKey.setName( PK_ALIAS.toAliasString( table.getName() ) ); primaryKey.addColumns( getKey() ); @@ -443,7 +450,7 @@ public void createPrimaryKey() { } } } - table.setPrimaryKey( primaryKey ); + return primaryKey; } private boolean addPartitionKeyToPrimaryKey() { @@ -722,6 +729,23 @@ public int getJoinClosureSpan() { } public int getPropertyClosureSpan() { + int span = 0; + for ( Property property : properties ) { + if ( !property.isGeneric() ) { + span += 1; + } + } + for ( var join : joins ) { + for ( Property property : join.getProperties() ) { + if ( !property.isGeneric() ) { + span += 1; + } + } + } + return span; + } + + public int getAllPropertyClosureSpan() { int span = properties.size(); for ( var join : joins ) { span += join.getPropertySpan(); @@ -754,6 +778,23 @@ public int getJoinNumber(Property prop) { * @return A list over the "normal" properties. */ public List getProperties() { + final ArrayList list = new ArrayList<>(); + for ( Property property : properties ) { + if ( !property.isGeneric() ) { + list.add( property ); + } + } + for ( var join : joins ) { + for ( Property property : join.getProperties() ) { + if ( !property.isGeneric() ) { + list.add( property ); + } + } + } + return list; + } + + public List getAllProperties() { final ArrayList> list = new ArrayList<>(); list.add( properties ); for ( var join : joins ) { @@ -871,7 +912,7 @@ protected List getNonDuplicatedProperties() { protected void checkColumnDuplication() { final String owner = "entity '" + getEntityName() + "'"; - final HashSet cols = new HashSet<>(); + final HashSet cols = new HashSet<>(); if ( getIdentifierMapper() == null ) { //an identifier mapper => getKey will be included in the getNonDuplicatedPropertyIterator() //and checked later, so it needs to be excluded @@ -950,6 +991,7 @@ public boolean hasPartitionedSelectionMapping() { return false; } + @Override public Component getIdentifierMapper() { return identifierMapper; } 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 c5e35b1def13..2a5f8aeff8dc 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/Property.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/Property.java @@ -24,6 +24,7 @@ import org.hibernate.jpa.event.spi.CallbackDefinition; import org.hibernate.metamodel.RepresentationMode; import org.hibernate.metamodel.spi.RuntimeModelCreationContext; +import org.hibernate.models.spi.MemberDetails; import org.hibernate.property.access.spi.Getter; import org.hibernate.property.access.spi.PropertyAccess; import org.hibernate.property.access.spi.PropertyAccessStrategy; @@ -61,6 +62,8 @@ public class Property implements Serializable, MetaAttributable { private boolean insertable = true; private boolean selectable = true; private boolean optimisticLocked = true; + private boolean temporalExcluded; + private boolean auditedExcluded; private GeneratorCreator generatorCreator; private String propertyAccessorName; private PropertyAccessStrategy propertyAccessStrategy; @@ -71,6 +74,7 @@ public class Property implements Serializable, MetaAttributable { private PersistentClass persistentClass; private boolean naturalIdentifier; private boolean isGeneric; + private boolean isGenericSpecialization; private boolean lob; private java.util.List callbackDefinitions; private String returnedClassName; @@ -98,6 +102,10 @@ public int getColumnSpan() { return value.getColumnSpan(); } + boolean hasColumns() { + return value.hasColumns(); + } + /** * Delegates to {@link Value#getSelectables()}. */ @@ -126,12 +134,17 @@ public Value getValue() { return value; } + public void resetInsertable(boolean insertable) { + setInsertable( insertable ); + if ( !insertable ) { + value.setNonInsertable(); + } + } + public void resetUpdateable(boolean updateable) { setUpdatable( updateable ); - final boolean[] columnUpdateability = getValue().getColumnUpdateability(); - final int columnSpan = getColumnSpan(); - for ( int i = 0; i < columnSpan; i++ ) { - columnUpdateability[i] = updateable; + if ( !updateable ) { + value.setNonUpdatable(); } } @@ -490,6 +503,30 @@ public void setGeneric(boolean generic) { this.isGeneric = generic; } + public boolean isGenericSpecialization() { + return isGenericSpecialization; + } + + public void setGenericSpecialization(boolean genericSpecialization) { + isGenericSpecialization = genericSpecialization; + } + + public boolean isTemporalExcluded() { + return temporalExcluded; + } + + public void setTemporalExcluded(boolean temporalExcluded) { + this.temporalExcluded = temporalExcluded; + } + + public boolean isAuditedExcluded() { + return auditedExcluded; + } + + public void setAuditedExcluded(boolean auditedExcluded) { + this.auditedExcluded = auditedExcluded; + } + public boolean isLob() { return lob; } @@ -544,6 +581,8 @@ public Property copy() { property.setInsertable( isInsertable() ); property.setSelectable( isSelectable() ); property.setOptimisticLocked( isOptimisticLocked() ); + property.setTemporalExcluded( isTemporalExcluded() ); + property.setAuditedExcluded( isAuditedExcluded() ); property.setValueGeneratorCreator( getValueGeneratorCreator() ); property.setPropertyAccessorName( getPropertyAccessorName() ); property.setPropertyAccessStrategy( getPropertyAccessStrategy() ); @@ -554,6 +593,7 @@ public Property copy() { property.setPersistentClass( getPersistentClass() ); property.setNaturalIdentifier( isNaturalIdentifier() ); property.setGeneric( isGeneric() ); + property.setGenericSpecialization( isGenericSpecialization() ); property.setLob( isLob() ); property.addCallbackDefinitions( getCallbackDefinitions() ); property.setReturnedClassName( getReturnedClassName() ); @@ -607,6 +647,13 @@ public Value getValue() { return value; } + @Override + public MemberDetails getMemberDetails() { + return value instanceof SimpleValue simpleValue + ? simpleValue.getMemberDetails() + : null; // TODO: Give Property a reference to the MemberDetails + } + @Override public SqlStringGenerationContext getSqlStringGenerationContext() { return context.getSqlStringGenerationContext(); diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/QualifiedColumnName.java b/hibernate-core/src/main/java/org/hibernate/mapping/QualifiedColumnName.java new file mode 100644 index 000000000000..4e5fc1d6e7a5 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/mapping/QualifiedColumnName.java @@ -0,0 +1,17 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.mapping; + +import org.hibernate.boot.model.naming.Identifier; + +/** + * Represents a fully qualified column name for uniqueness checks. + */ +public record QualifiedColumnName( + Identifier catalogName, + Identifier schemaName, + Identifier tableName, + Identifier columnName +) {} diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/RootClass.java b/hibernate-core/src/main/java/org/hibernate/mapping/RootClass.java index 6503fa477807..fdf3a04f39e9 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/RootClass.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/RootClass.java @@ -4,14 +4,18 @@ */ package org.hibernate.mapping; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import org.hibernate.MappingException; import org.hibernate.annotations.SoftDeleteType; import org.hibernate.boot.Metadata; import org.hibernate.boot.spi.MetadataBuildingContext; +import org.hibernate.models.spi.ClassDetails; +import org.hibernate.persister.state.spi.StateManagement; import static org.hibernate.internal.CoreMessageLogger.CORE_LOGGER; import static org.hibernate.internal.util.ReflectHelper.overridesEquals; @@ -34,6 +38,7 @@ public final class RootClass extends PersistentClass implements TableOwner, Soft private String cacheConcurrencyStrategy; private String cacheRegionName; private boolean lazyPropertiesCacheable = true; + private ClassDetails naturalIdClass; private String naturalIdCacheRegionName; private Value discriminator; @@ -47,9 +52,16 @@ public final class RootClass extends PersistentClass implements TableOwner, Soft private int nextSubclassId; private Property declaredIdentifierProperty; private Property declaredVersion; - private Column softDeleteColumn; + private SoftDeleteType softDeleteStrategy; + private Class stateManagementType; + private Table auxiliaryTable; + private boolean partitioned; + private Map auxiliaryColumns; + private String auxiliaryColumnInPrimaryKey; + private boolean primaryKeyDisabled; + public RootClass(MetadataBuildingContext buildingContext) { super( buildingContext ); } @@ -134,6 +146,11 @@ public List getPropertyClosure() { return getProperties(); } + @Override + public List getAllPropertyClosure() { + return getAllProperties(); + } + @Override public List
    getTableClosure() { return List.of( getTable() ); @@ -365,6 +382,14 @@ public void setLazyPropertiesCacheable(boolean lazyPropertiesCacheable) { this.lazyPropertiesCacheable = lazyPropertiesCacheable; } + public ClassDetails getNaturalIdClass() { + return naturalIdClass; + } + + public void setNaturalIdClass(ClassDetails naturalIdClass) { + this.naturalIdClass = naturalIdClass; + } + @Override public String getNaturalIdCacheRegionName() { return naturalIdCacheRegionName; @@ -396,18 +421,47 @@ public Set
    getIdentityTables() { @Override public void enableSoftDelete(Column indicatorColumn, SoftDeleteType strategy) { - softDeleteColumn = indicatorColumn; + SoftDeletable.super.enableSoftDelete( indicatorColumn, strategy ); softDeleteStrategy = strategy; } @Override - public Column getSoftDeleteColumn() { - return softDeleteColumn; + public SoftDeleteType getSoftDeleteStrategy() { + return softDeleteStrategy; } @Override - public SoftDeleteType getSoftDeleteStrategy() { - return softDeleteStrategy; + public Table getMainTable() { + return table; + } + + @Override + public boolean isMainTablePartitioned() { + return partitioned; + } + + public void setMainTablePartitioned(boolean partitioned) { + this.partitioned = partitioned; + } + + @Override + public boolean isAuxiliaryColumnInPrimaryKey() { + return auxiliaryColumnInPrimaryKey != null; + } + + @Override + public void setAuxiliaryColumnInPrimaryKey(String key) { + this.auxiliaryColumnInPrimaryKey = key; + } + + @Override + public boolean isPrimaryKeyDisabled() { + return primaryKeyDisabled; + } + + @Override + public void setPrimaryKeyDisabled(boolean disabled) { + this.primaryKeyDisabled = disabled; } @Override @@ -415,4 +469,50 @@ public Object accept(PersistentClassVisitor mv) { return mv.accept( this ); } + @Override + public PrimaryKey makePrimaryKey(Table table) { + if ( isPrimaryKeyDisabled() ) { + return null; + } + else { + final var primaryKey = super.makePrimaryKey( table ); + if ( isAuxiliaryColumnInPrimaryKey() ) { + if ( isVersioned() ) { + primaryKey.addColumns( getVersion().getValue() ); + } + else { + primaryKey.addColumn( getAuxiliaryColumn( auxiliaryColumnInPrimaryKey ) ); + } + } + return primaryKey; + } + } + + public void setStateManagementType(Class stateManagementType) { + this.stateManagementType = stateManagementType; + } + + public Class getStateManagementType() { + return stateManagementType; + } + + public Table getAuxiliaryTable() { + return auxiliaryTable; + } + + public void setAuxiliaryTable(Table auxiliaryTable) { + this.auxiliaryTable = auxiliaryTable; + } + + public Column getAuxiliaryColumn(String column) { + return auxiliaryColumns == null ? null + : auxiliaryColumns.get( column ); + } + + public void addAuxiliaryColumn(String name, Column column) { + if ( auxiliaryColumns == null ) { + auxiliaryColumns = new HashMap<>(); + } + auxiliaryColumns.put( name, column ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/Set.java b/hibernate-core/src/main/java/org/hibernate/mapping/Set.java index ddf9a042bc1e..606efa04f26a 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/Set.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/Set.java @@ -4,9 +4,13 @@ */ package org.hibernate.mapping; +import java.util.ArrayList; +import java.util.List; import java.util.function.Supplier; import org.hibernate.MappingException; +import org.hibernate.boot.model.naming.Identifier; +import org.hibernate.boot.model.naming.ImplicitUniqueKeyNameSource; import org.hibernate.boot.spi.MetadataBuildingContext; import org.hibernate.resource.beans.spi.ManagedBean; import org.hibernate.type.CollectionType; @@ -19,7 +23,8 @@ /** * A mapping model object representing a collection of type {@link java.util.List}. * A set has no nullable element columns (unless it is a one-to-many association). - * It has a primary key consisting of all columns (i.e. key columns + element columns). + * It has a primary key consisting of all columns (i.e. key columns + element columns), + * or a unique key if some element columns are nullable. * * @author Gavin King */ @@ -38,7 +43,7 @@ public Set(Supplier> customTypeBeanRes super( customTypeBeanResolver, persistentClass, buildingContext ); } - private Set(Collection original) { + private Set(Set original) { super( original ); } @@ -79,22 +84,67 @@ else if ( hasOrder() ) { void createPrimaryKey() { if ( !isOneToMany() ) { final var collectionTable = getCollectionTable(); - var primaryKey = collectionTable.getPrimaryKey(); - if ( primaryKey == null ) { - primaryKey = new PrimaryKey( getCollectionTable() ); - primaryKey.addColumns( getKey() ); + if ( !collectionTable.hasPrimaryKey() + && collectionTable.getUniqueKeys().isEmpty() ) { + boolean useUniqueKey = false; for ( var selectable : getElement().getSelectables() ) { - if ( selectable instanceof Column col ) { - if ( !col.isNullable() ) { - primaryKey.addColumn( col ); + if ( selectable instanceof Column column ) { + try { + if ( column.isSqlTypeLob( getMetadata() ) ) { + return; + } } - else { - return; + catch (MappingException me) { + // ignore + } + if ( column.isNullable() ) { + useUniqueKey = true; } } } - if ( primaryKey.getColumnSpan() != getKey().getColumnSpan() ) { - collectionTable.setPrimaryKey( primaryKey ); + final var key = useUniqueKey + ? new UniqueKey( collectionTable ) + : new PrimaryKey( collectionTable ); + key.addColumns( getKey() ); + for ( var selectable : getElement().getSelectables() ) { + if ( selectable instanceof Column column ) { + key.addColumn( column ); + } + } + key.setName( getBuildingContext().getBuildingOptions().getImplicitNamingStrategy() + .determineUniqueKeyName( new ImplicitUniqueKeyNameSource() { + @Override + public Identifier getTableName() { + return getTable().getNameIdentifier(); + } + + @Override + public List getColumnNames() { + final List list = new ArrayList<>(); + for ( var c : key.getColumns() ) { + list.add( c.getNameIdentifier( getBuildingContext() ) ); + } + return list; + } + + @Override + public Identifier getUserProvidedIdentifier() { + return null; + } + + @Override + public MetadataBuildingContext getBuildingContext() { + return Set.this.getBuildingContext(); + } + } ) + .render( getMetadata().getDatabase().getDialect() ) ); + if ( key.getColumnSpan() > getKey().getColumnSpan() ) { + if ( useUniqueKey ) { + collectionTable.addUniqueKey( (UniqueKey) key ); + } + else { + collectionTable.setPrimaryKey( (PrimaryKey) key ); + } } // else { //for backward compatibility, allow a set with no not-null diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/SimpleValue.java b/hibernate-core/src/main/java/org/hibernate/mapping/SimpleValue.java index 4cabe13f4194..28322426675f 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/SimpleValue.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/SimpleValue.java @@ -88,6 +88,8 @@ public abstract class SimpleValue implements KeyValue { private String typeName; private Properties typeParameters; + private Annotation typeAnnotation; + private MemberDetails memberDetails; private boolean isVersion; private boolean isNationalized; private boolean isLob; @@ -121,12 +123,20 @@ public SimpleValue(MetadataBuildingContext buildingContext, Table table) { protected SimpleValue(SimpleValue original) { this.buildingContext = original.buildingContext; this.metadata = original.metadata; - this.columns.addAll( original.columns ); + for ( var selectable : original.columns ) { + if ( selectable instanceof Column column ) { + final var newColumn = column.clone(); + newColumn.setValue( this ); + this.columns.add( newColumn ); + } + } this.insertability.addAll( original.insertability ); this.updatability.addAll( original.updatability ); this.partitionKey = original.partitionKey; this.typeName = original.typeName; this.typeParameters = original.typeParameters == null ? null : new Properties( original.typeParameters ); + this.typeAnnotation = original.typeAnnotation; + this.memberDetails = original.memberDetails; this.isVersion = original.isVersion; this.isNationalized = original.isNationalized; this.isLob = original.isLob; @@ -227,8 +237,8 @@ protected void justAddFormula(Formula formula) { public void sortColumns(int[] originalOrder) { if ( columns.size() > 1 ) { final var originalColumns = columns.toArray( new Selectable[0] ); - final boolean[] originalInsertability = toBooleanArray( insertability ); - final boolean[] originalUpdatability = toBooleanArray( updatability ); + final var originalInsertability = toBooleanArray( insertability ); + final var originalUpdatability = toBooleanArray( updatability ); for ( int i = 0; i < originalOrder.length; i++ ) { final int originalIndex = originalOrder[i]; final var selectable = originalColumns[i]; @@ -257,6 +267,11 @@ public int getColumnSpan() { return columns.size(); } + @Override + public boolean hasColumns() { + return !columns.isEmpty(); + } + protected Selectable getColumn(int position){ return columns.get( position ); } @@ -298,8 +313,7 @@ void setAttributeConverterDescriptor(String typeName) { (Class>) classForName( AttributeConverter.class, converterClassName, bootstrapContext ); attributeConverterDescriptor = - ConverterDescriptors.of( clazz, null, false, - bootstrapContext.getClassmateContext() ); + ConverterDescriptors.of( clazz, null, false ); } ClassLoaderService classLoaderService() { @@ -307,7 +321,7 @@ ClassLoaderService classLoaderService() { } public void makeVersion() { - this.isVersion = true; + isVersion = true; } public boolean isVersion() { @@ -341,7 +355,7 @@ public void createForeignKey(PersistentClass referencedEntity, AnnotatedJoinColu @Override public ForeignKey createForeignKeyOfEntity(String entityName) { - if ( isConstrained() ) { + if ( isConstrained() && !hasAuxiliaryColumnInPrimaryKey( entityName ) ) { final var foreignKey = table.createForeignKey( getForeignKeyName(), getConstraintColumns(), @@ -359,7 +373,7 @@ public ForeignKey createForeignKeyOfEntity(String entityName) { @Override public ForeignKey createForeignKeyOfEntity(String entityName, List referencedColumns) { - if ( isConstrained() ) { + if ( isConstrained() && !hasAuxiliaryColumnInPrimaryKey( entityName ) ) { final var foreignKey = table.createForeignKey( getForeignKeyName(), getConstraintColumns(), @@ -375,6 +389,20 @@ public ForeignKey createForeignKeyOfEntity(String entityName, List refer return null; } + protected boolean hasAuxiliaryColumnInPrimaryKey(PersistentClass referencedEntity) { + return referencedEntity.getRootClass().isAuxiliaryColumnInPrimaryKey(); + } + + protected boolean hasAuxiliaryColumnInPrimaryKey(String entityName) { + if ( entityName == null ) { + return false; + } + else { + final var referencedEntity = metadata.getEntityBinding( entityName ); + return referencedEntity != null && hasAuxiliaryColumnInPrimaryKey( referencedEntity ); + } + } + @Override public void createUniqueKey(MetadataBuildingContext context) { if ( hasFormula() ) { @@ -589,12 +617,11 @@ public boolean isNullable() { // considered nullable return true; } - else if ( selectable instanceof Column column ) { - if ( !column.isNullable() ) { - // if there is a single non-nullable column, the Value - // overall is considered non-nullable. - return false; - } + else if ( selectable instanceof Column column + && !column.isNullable() ) { + // if there is a single non-nullable column, the Value + // overall is considered non-nullable. + return false; } } // nullable by default @@ -792,11 +819,27 @@ public void setTypeParameters(Map parameters) { } } + public void setTypeAnnotation(Annotation typeAnnotation) { + this.typeAnnotation = typeAnnotation; + } + + public void setMemberDetails(MemberDetails memberDetails) { + this.memberDetails = memberDetails; + } + public Properties getTypeParameters() { return typeParameters; } - public void copyTypeFrom( SimpleValue sourceValue ) { + public Annotation getTypeAnnotation() { + return typeAnnotation; + } + + public MemberDetails getMemberDetails() { + return memberDetails; + } + + public void copyTypeFrom(SimpleValue sourceValue ) { setTypeName( sourceValue.getTypeName() ); setTypeParameters( sourceValue.getTypeParameters() ); @@ -818,6 +861,8 @@ public boolean isSame(SimpleValue other) { return Objects.equals( columns, other.columns ) && Objects.equals( typeName, other.typeName ) && Objects.equals( typeParameters, other.typeParameters ) + && Objects.equals( typeAnnotation, other.typeAnnotation ) + && Objects.equals( memberDetails, other.memberDetails ) && Objects.equals( table, other.table ) && Objects.equals( foreignKeyName, other.foreignKeyName ) && Objects.equals( foreignKeyDefinition, other.foreignKeyDefinition ); @@ -854,6 +899,16 @@ public boolean[] getColumnUpdateability() { return extractBooleansFromList( updatability ); } + @Override + public void setNonInsertable() { + insertability.replaceAll( current -> false ); + } + + @Override + public void setNonUpdatable() { + updatability.replaceAll( current -> false ); + } + @Override public boolean hasAnyUpdatableColumns() { for ( int i = 0; i < updatability.size(); i++ ) { @@ -883,7 +938,7 @@ public void setPartitionKey(boolean partitionColumn) { } private static boolean[] extractBooleansFromList(List list) { - final boolean[] array = new boolean[ list.size() ]; + final var array = new boolean[ list.size() ]; int i = 0; for ( Boolean value : list ) { array[ i++ ] = value; @@ -912,11 +967,11 @@ private static Annotation[] getAnnotations(MemberDetails memberDetails) { protected ParameterType createParameterType() { try { - final String[] columnNames = new String[ columns.size() ]; - final Long[] columnLengths = new Long[ columns.size() ]; - for ( int i = 0; i < columns.size(); i++ ) { - final var selectable = columns.get(i); - if ( selectable instanceof Column column ) { + final int size = columns.size(); + final var columnNames = new String[size]; + final var columnLengths = new Long[size]; + for ( int i = 0; i < size; i++ ) { + if ( columns.get(i) instanceof Column column ) { columnNames[i] = column.getName(); columnLengths[i] = column.getLength(); } @@ -1096,6 +1151,11 @@ public Type getType() { return SimpleValue.this.getType(); } + @Override + public MemberDetails getMemberDetails() { + return SimpleValue.this.getMemberDetails(); + } + // we could add this if it helps integrate old infrastructure // @Override // public Properties getParameters() { diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/SoftDeletable.java b/hibernate-core/src/main/java/org/hibernate/mapping/SoftDeletable.java index 30eeeb2da084..06564bb70cba 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/SoftDeletable.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/SoftDeletable.java @@ -4,26 +4,40 @@ */ package org.hibernate.mapping; +import org.hibernate.Incubating; import org.hibernate.annotations.SoftDeleteType; +import org.hibernate.persister.state.internal.SoftDeleteStateManagement; /** * Part of the boot model which can be soft-deleted * * @author Steve Ebersole + * + * @deprecated This is no longer needed after the + * introduction of {@link Stateful}. */ -public interface SoftDeletable { +@Incubating @Deprecated(forRemoval = true) +public interface SoftDeletable extends Stateful { + + String INDICATOR = "indicator"; + /** * Enable soft-delete for this part of the model. * * @param indicatorColumn The column which indicates soft-deletion * @param strategy The strategy for indicating soft-deletion */ - void enableSoftDelete(Column indicatorColumn, SoftDeleteType strategy); + default void enableSoftDelete(Column indicatorColumn, SoftDeleteType strategy) { + addAuxiliaryColumn( INDICATOR, indicatorColumn ); + setStateManagementType( SoftDeleteStateManagement.class ); + } /** * The column which indicates soft-deletion. */ - Column getSoftDeleteColumn(); + default Column getSoftDeleteColumn() { + return getAuxiliaryColumn( INDICATOR ); + } /** * The strategy for indicating soft-deletion. diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/Stateful.java b/hibernate-core/src/main/java/org/hibernate/mapping/Stateful.java new file mode 100644 index 000000000000..22d9c3ef2df7 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/mapping/Stateful.java @@ -0,0 +1,70 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.mapping; + +import org.hibernate.InstantiationException; +import org.hibernate.persister.state.spi.StateManagement; +import org.hibernate.persister.state.internal.StandardStateManagement; + +/** + * Abstracts over things which can have a {@linkplain StateManagement + * customized state management strategy}, providing slots to plug in + * extra columns related to custom state management. + * + * @author Gavin King + * + * @see org.hibernate.annotations.Temporal + * @see org.hibernate.annotations.Audited + * @see org.hibernate.annotations.SoftDelete + * + * @since 7.4 + */ +public interface Stateful { + + void setStateManagementType(Class stateManagementType); + + Class getStateManagementType(); + + Table getAuxiliaryTable(); + + void setAuxiliaryTable(Table auxiliaryTable); + + Table getMainTable(); + + boolean isMainTablePartitioned(); + + void setMainTablePartitioned(boolean partitioned); + + Column getAuxiliaryColumn(String column); + + void addAuxiliaryColumn(String name, Column column); + + boolean isAuxiliaryColumnInPrimaryKey(); + + void setAuxiliaryColumnInPrimaryKey(String key); + + boolean isPrimaryKeyDisabled(); + + void setPrimaryKeyDisabled(boolean disabled); + + default StateManagement getStateManagement() { + final var stateManagementType = getStateManagementType(); + if ( stateManagementType == null ) { + return StandardStateManagement.INSTANCE; + } + else { + try { + return (StateManagement) + stateManagementType + .getDeclaredField( "INSTANCE" ) + .get( null ); + } + catch (IllegalAccessException | NoSuchFieldException e) { + throw new InstantiationException( "Could not create StateManagement", + stateManagementType, e ); + } + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/Subclass.java b/hibernate-core/src/main/java/org/hibernate/mapping/Subclass.java index 8a420fb15bbe..d6c322557a24 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/Subclass.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/Subclass.java @@ -138,6 +138,11 @@ public List getPropertyClosure() { return new JoinedList<>( getSuperclass().getPropertyClosure(), getProperties() ); } + @Override + public List getAllPropertyClosure() { + return new JoinedList<>( getSuperclass().getAllPropertyClosure(), getAllProperties() ); + } + @Override public List
    getTableClosure() { return new JoinedList<>( @@ -238,6 +243,11 @@ public int getPropertyClosureSpan() { return getSuperclass().getPropertyClosureSpan() + super.getPropertyClosureSpan(); } + @Override + public int getAllPropertyClosureSpan() { + return getSuperclass().getAllPropertyClosureSpan() + super.getAllPropertyClosureSpan(); + } + @Override public List getJoinClosure() { return new JoinedList<>( getSuperclass().getJoinClosure(), super.getJoinClosure() ); diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/Table.java b/hibernate-core/src/main/java/org/hibernate/mapping/Table.java index 04d3f60d8a6b..498d0982e578 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/Table.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/Table.java @@ -30,7 +30,6 @@ import org.hibernate.dialect.Dialect; import org.hibernate.resource.transaction.spi.DdlTransactionIsolator; -import org.jboss.logging.Logger; import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; @@ -46,7 +45,6 @@ * @author Gavin King */ public class Table implements Serializable, ContributableDatabaseObject { - private static final Logger LOG = Logger.getLogger( Table.class ); private static final Column[] EMPTY_COLUMN_ARRAY = new Column[0]; private final String contributor; @@ -72,6 +70,7 @@ public class Table implements Serializable, ContributableDatabaseObject { private String comment; private String viewQuery; private String options; + private String extraDeclarations; private List> initCommandProducers; private List> resyncCommandProducers; @@ -110,8 +109,9 @@ public Table( String subselect, boolean isAbstract) { this.contributor = contributor; - this.catalog = namespace.getPhysicalName().catalog(); - this.schema = namespace.getPhysicalName().schema(); + final var physicalName = namespace.getPhysicalName(); + this.catalog = physicalName.catalog(); + this.schema = physicalName.schema(); this.name = physicalTableName; this.subselect = subselect; this.isAbstract = isAbstract; @@ -119,8 +119,9 @@ public Table( public Table(String contributor, Namespace namespace, String subselect, boolean isAbstract) { this.contributor = contributor; - this.catalog = namespace.getPhysicalName().catalog(); - this.schema = namespace.getPhysicalName().schema(); + final var physicalName = namespace.getPhysicalName(); + this.catalog = physicalName.catalog(); + this.schema = physicalName.schema(); this.subselect = subselect; this.isAbstract = isAbstract; } @@ -247,7 +248,7 @@ public Column getColumn(Column column) { return null; } else { - final Column existing = columns.get( column.getCanonicalName() ); + final var existing = columns.get( column.getCanonicalName() ); return column.equals( existing ) ? existing : null; } } @@ -276,15 +277,11 @@ public void addColumn(Column column) { if ( oldColumn == null ) { if ( primaryKey != null ) { for ( var primaryKeyColumn : primaryKey.getColumns() ) { - if ( primaryKeyColumn.getCanonicalName().equals( column.getCanonicalName() ) ) { + if ( Objects.equals( column.getCanonicalName(), + primaryKeyColumn.getCanonicalName() ) ) { + // Force the column to be non-null + // as it is part of the primary key column.setNullable( false ); - if ( LOG.isTraceEnabled() ) { - LOG.tracef( - "Forcing column [%s] to be non-null as it is part of the primary key for table [%s]", - column.getCanonicalName(), - getNameIdentifier().getCanonicalName() - ); - } } } } @@ -452,18 +449,21 @@ public void setPrimaryKey(PrimaryKey primaryKey) { } public Index getOrCreateIndex(String indexName) { - Index index = indexes.get( indexName ); - if ( index == null ) { - index = new Index(); - index.setName( indexName ); - index.setTable( this ); - indexes.put( indexName, index ); + final var index = indexes.get( indexName ); + if ( index != null ) { + return index; + } + else { + final var newIndex = new Index(); + newIndex.setName( indexName ); + newIndex.setTable( this ); + indexes.put( indexName, newIndex ); + return newIndex; } - return index; } public Index getIndex(String indexName) { - return indexes.get( indexName ); + return indexes.get( indexName ); } public Index addIndex(Index index) { @@ -859,4 +859,12 @@ public String getOptions() { public void setOptions(String options) { this.options = options; } + + public String getExtraDeclarations() { + return extraDeclarations; + } + + public void setExtraDeclarations(String extraDeclarations) { + this.extraDeclarations = extraDeclarations; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/ToOne.java b/hibernate-core/src/main/java/org/hibernate/mapping/ToOne.java index 4826d44f23cf..52feb8b9796b 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/ToOne.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/ToOne.java @@ -176,7 +176,7 @@ public int[] sortProperties() { ? entityBinding.getIdentifier() : entityBinding.getRecursiveProperty( referencedPropertyName ).getValue(); if ( value instanceof Component component ) { - final int[] originalPropertyOrder = component.sortProperties(); + final var originalPropertyOrder = component.sortProperties(); if ( !sorted ) { if ( originalPropertyOrder != null ) { sortColumns( originalPropertyOrder ); @@ -192,14 +192,17 @@ public int[] sortProperties() { return null; } + boolean isActuallyConstrained() { + return isConstrained(); + } + @Override public void createForeignKey(PersistentClass referencedEntity, AnnotatedJoinColumns joinColumns) { // Ensure properties are sorted before we create a foreign key sortProperties(); - if ( isForeignKeyEnabled() - && referencedPropertyName == null - && !hasFormula() - && isConstrained() ) { + if ( referencedPropertyName == null + && isActuallyConstrained() + && !hasAuxiliaryColumnInPrimaryKey( referencedEntity ) ) { final var firstColumn = joinColumns.getJoinColumns().get( 0 ); final Object owner = findReferencedColumnOwner( referencedEntity, firstColumn, getBuildingContext() ); if ( owner instanceof Join join ) { diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/Value.java b/hibernate-core/src/main/java/org/hibernate/mapping/Value.java index a31aab07297d..a9becae74c04 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/Value.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/Value.java @@ -12,6 +12,7 @@ import org.hibernate.Incubating; import org.hibernate.Internal; import org.hibernate.MappingException; +import org.hibernate.boot.model.naming.Identifier; import org.hibernate.boot.spi.MetadataBuildingContext; import org.hibernate.metamodel.mapping.JdbcMapping; import org.hibernate.service.ServiceRegistry; @@ -51,6 +52,13 @@ public interface Value extends Serializable { */ List getColumns(); + /** + * Does the mapping involve at least one column? + * + * @since 7.3 + */ + boolean hasColumns(); + /** * Same as {@link #getSelectables()} except it returns the PK for the * non-owning side of a one-to-one association. @@ -78,9 +86,7 @@ default JdbcMapping getSelectableType(MappingContext mappingContext, int index) private JdbcMapping getType(MappingContext factory, Type elementType, int index) { if ( elementType instanceof CompositeType compositeType ) { - final Type[] subtypes = compositeType.getSubtypes(); - for ( int i = 0; i < subtypes.length; i++ ) { - final Type subtype = subtypes[i]; + for ( final var subtype : compositeType.getSubtypes() ) { final int columnSpan = subtype instanceof EntityType entityType ? getIdType( entityType ).getColumnSpan( factory ) @@ -107,7 +113,7 @@ else if ( elementType instanceof MetaType metaType ) { } private Type getIdType(EntityType entityType) { - final PersistentClass entityBinding = + final var entityBinding = getBuildingContext().getMetadataCollector() .getEntityBinding( entityType.getAssociatedEntityName() ); return entityType.isReferenceToPrimaryKey() @@ -145,16 +151,23 @@ private Type getIdType(EntityType entityType) { boolean isSame(Value other); boolean[] getColumnInsertability(); + boolean hasAnyInsertableColumns(); boolean[] getColumnUpdateability(); + boolean hasAnyUpdatableColumns(); + void setNonUpdatable(); + void setNonInsertable(); + @Incubating default MetadataBuildingContext getBuildingContext() { throw new UnsupportedOperationException( "Value#getBuildingContext is not implemented by: " + getClass().getName() ); } + ServiceRegistry getServiceRegistry(); + Value copy(); boolean isColumnInsertable(int index); @@ -177,14 +190,28 @@ default String getExtraCreateTableInfo() { * @param owner the owner of this value, used just for error reporting */ @Internal - default void checkColumnDuplication(Set distinctColumns, String owner) { + default void checkColumnDuplication(Set distinctColumns, String owner) { for ( int i = 0; i < getSelectables().size(); i++ ) { - final Selectable selectable = getSelectables().get( i ); - if ( isColumnInsertable( i ) || isColumnUpdateable( i ) ) { - final Column col = (Column) selectable; - if ( !distinctColumns.add( col.getName() ) ) { + if ( getSelectables().get( i ) instanceof Column column + && ( isColumnInsertable( i ) || isColumnUpdateable( i ) ) ) { + final var primaryTable = getTable(); + final Identifier catalog; + final Identifier schema; + final Identifier table; + if ( primaryTable != null ) { + catalog = primaryTable.getCatalogIdentifier(); + schema = primaryTable.getSchemaIdentifier(); + table = primaryTable.getNameIdentifier(); + } + else { + catalog = null; + schema = null; + table = null; + } + final var columnName = column.getNameIdentifier( getBuildingContext() ); + if ( !distinctColumns.add( new QualifiedColumnName( catalog, schema, table, columnName ) ) ){ throw new MappingException( - "Column '" + col.getName() + "Column '" + column.getName() + "' is duplicated in mapping for " + owner + " (use '@Column(insertable=false, updatable=false)' when mapping multiple properties to the same column)" ); diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/package-info.java b/hibernate-core/src/main/java/org/hibernate/mapping/package-info.java index 933119b20822..f49209c6f25f 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/package-info.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/package-info.java @@ -24,14 +24,14 @@ * The lifecycle of these mapping objects is outlined below. *
      *
    1. It is the responsibility of the metadata binders in the package - * {@link org.hibernate.boot.model.internal} to process a set of + * {@code org.hibernate.boot.model.internal} to process a set of * annotated classes and produce fully-initialized mapping model * objects. This is in itself a complicated multiphase process, * since, for example, the type of an association mapping in one * entity cannot be fully assigned until the target entity has * been processed. *
    2. The mapping model objects are then passed to the constructor - * of {@link org.hibernate.internal.SessionFactoryImpl}, which + * of {@code org.hibernate.internal.SessionFactoryImpl}, which * simply passes them along on to an object which implements * {@link org.hibernate.metamodel.MappingMetamodel} and uses them * to create persister objects for diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/AbstractCompositeIdentifierMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/AbstractCompositeIdentifierMapping.java index 0a1ac0b12589..d201ffe211e1 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/AbstractCompositeIdentifierMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/AbstractCompositeIdentifierMapping.java @@ -143,7 +143,7 @@ public TableGroupJoin createTableGroupJoin( null, creationState ); - return new TableGroupJoin( navigablePath, joinType, tableGroup, null ); + return new TableGroupJoin( navigablePath, joinType, tableGroup ); } @Override 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 9277ea7e1098..940c0745304d 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 @@ -212,7 +212,7 @@ private DomainType determineSimpleType(ValueContext typeContext) { public static DomainType determineSimpleType(ValueContext typeContext, MetadataContext context) { return switch ( typeContext.getValueClassification() ) { case BASIC -> basicDomainType( typeContext, context ); - case ENTITY -> entityDomainType (typeContext, context ); + case ENTITY -> entityDomainType( typeContext, context ); case EMBEDDABLE -> embeddableDomainType( typeContext, context ); default -> throw new AssertionFailure( "Unknown type : " + typeContext.getValueClassification() ); }; @@ -367,7 +367,7 @@ private static JavaType determineRelationalJavaType( final var descriptor = value.getJpaAttributeConverterDescriptor(); if ( descriptor != null ) { return context.getJavaTypeRegistry().resolveDescriptor( - descriptor.getRelationalValueResolvedType().getErasedType() + descriptor.getRelationalValueResolvedType() ); } } @@ -699,7 +699,7 @@ private static Member resolveVirtualIdentifierMember( Property property, EntityP private static Member resolveEntityMember(Property property, EntityPersister declaringEntity) { final String propertyName = property.getName(); return !propertyName.equals( declaringEntity.getIdentifierPropertyName() ) - && declaringEntity.findAttributeMapping( propertyName ) == null + && declaringEntity.findAttributeMapping( propertyName ) == null && !property.isGeneric() // just like in #determineIdentifierJavaMember , this *should* indicate we have an IdClass mapping ? resolveVirtualIdentifierMember( property, declaringEntity ) : getter( declaringEntity, property, propertyName, property.getType().getReturnedClass() ); diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EmbeddableHelper.java b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EmbeddableHelper.java index dbb05cd63ee6..6e2eeffddc05 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EmbeddableHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EmbeddableHelper.java @@ -4,14 +4,18 @@ */ package org.hibernate.metamodel.internal; -import static java.util.Arrays.binarySearch; - public class EmbeddableHelper { public static int[] determineMappingIndex(String[] sortedNames, String[] names) { final int[] index = new int[sortedNames.length]; int i = 0; for ( String name : names ) { - final int mappingIndex = binarySearch( sortedNames, name ); + int mappingIndex = -1; + for ( int j = 0; j < sortedNames.length; j++ ) { + if ( name.equals( sortedNames[j] ) ) { + mappingIndex = j; + break; + } + } if ( mappingIndex != -1 ) { index[i++] = mappingIndex; } @@ -22,7 +26,13 @@ public static int[] determineMappingIndex(String[] sortedNames, String[] names) public static boolean resolveIndex(String[] sortedComponentNames, String[] componentNames, int[] index) { boolean hasGaps = false; for ( int i = 0; i < componentNames.length; i++ ) { - final int newIndex = binarySearch( sortedComponentNames, componentNames[i] ); + int newIndex = -1; + for ( int j = 0; j < sortedComponentNames.length; j++ ) { + if ( componentNames[i].equals( sortedComponentNames[j] ) ) { + newIndex = j; + break; + } + } index[i] = newIndex; hasGaps = hasGaps || newIndex < 0; } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EmbeddableInstantiatorDynamicMap.java b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EmbeddableInstantiatorDynamicMap.java index b66760bf778c..ed3e1042c2ee 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EmbeddableInstantiatorDynamicMap.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EmbeddableInstantiatorDynamicMap.java @@ -4,7 +4,6 @@ */ package org.hibernate.metamodel.internal; -import java.util.Map; import java.util.function.Supplier; import org.hibernate.mapping.Component; @@ -30,7 +29,7 @@ public EmbeddableInstantiatorDynamicMap( @Override public Object instantiate(ValueAccess valuesAccess) { - final Map dataMap = generateDataMap(); + final var dataMap = generateDataMap(); final var values = valuesAccess == null ? null : valuesAccess.getValues(); if ( values != null ) { final var mappingType = runtimeDescriptorAccess.get(); diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EmbeddableInstantiatorPojoIndirecting.java b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EmbeddableInstantiatorPojoIndirecting.java index 4b02422129a0..67efacb44cbb 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EmbeddableInstantiatorPojoIndirecting.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EmbeddableInstantiatorPojoIndirecting.java @@ -13,7 +13,9 @@ /** * Support for instantiating embeddables as POJO representation through a constructor */ -public class EmbeddableInstantiatorPojoIndirecting extends AbstractPojoInstantiator implements EmbeddableInstantiator { +public class EmbeddableInstantiatorPojoIndirecting + extends AbstractPojoInstantiator + implements EmbeddableInstantiator { protected final Constructor constructor; protected final int[] index; @@ -30,7 +32,7 @@ public static EmbeddableInstantiatorPojoIndirecting of( if ( componentNames == null ) { throw new IllegalArgumentException( "Can't determine field assignment for constructor: " + constructor ); } - final int[] index = new int[componentNames.length]; + final var index = new int[componentNames.length]; return EmbeddableHelper.resolveIndex( propertyNames, componentNames, index ) ? new EmbeddableInstantiatorPojoIndirectingWithGap( constructor, index ) : new EmbeddableInstantiatorPojoIndirecting( constructor, index ); diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EmbeddableInstantiatorPojoOptimized.java b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EmbeddableInstantiatorPojoOptimized.java index 40c72aeabeae..5e77b55c317e 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EmbeddableInstantiatorPojoOptimized.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EmbeddableInstantiatorPojoOptimized.java @@ -15,7 +15,9 @@ * Support for instantiating embeddables as POJO representation * using bytecode optimizer */ -public class EmbeddableInstantiatorPojoOptimized extends AbstractPojoInstantiator implements StandardEmbeddableInstantiator { +public class EmbeddableInstantiatorPojoOptimized + extends AbstractPojoInstantiator + implements StandardEmbeddableInstantiator { private final Supplier embeddableMappingAccess; private final InstantiationOptimizer instantiationOptimizer; diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EmbeddableInstantiatorPojoStandard.java b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EmbeddableInstantiatorPojoStandard.java index f6218d5565c8..d28556d9d9a8 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EmbeddableInstantiatorPojoStandard.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EmbeddableInstantiatorPojoStandard.java @@ -18,12 +18,16 @@ /** * Support for instantiating embeddables as POJO representation */ -public class EmbeddableInstantiatorPojoStandard extends AbstractPojoInstantiator implements StandardEmbeddableInstantiator { +public class EmbeddableInstantiatorPojoStandard + extends AbstractPojoInstantiator + implements StandardEmbeddableInstantiator { private final Supplier embeddableMappingAccess; private final Constructor constructor; - public EmbeddableInstantiatorPojoStandard(Class embeddableClass, Supplier embeddableMappingAccess) { + public EmbeddableInstantiatorPojoStandard( + Class embeddableClass, + Supplier embeddableMappingAccess) { super( embeddableClass ); this.embeddableMappingAccess = embeddableMappingAccess; this.constructor = resolveConstructor( embeddableClass ); diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EmbeddableInstantiatorRecordIndirecting.java b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EmbeddableInstantiatorRecordIndirecting.java index aa323b43c8d5..9b51f59eb072 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EmbeddableInstantiatorRecordIndirecting.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EmbeddableInstantiatorRecordIndirecting.java @@ -22,8 +22,8 @@ public EmbeddableInstantiatorRecordIndirecting(Class javaType, int[] index) { } public static EmbeddableInstantiatorRecordIndirecting of(Class javaType, String[] propertyNames) { - final String[] componentNames = getRecordComponentNames( javaType ); - final int[] index = new int[componentNames.length]; + final var componentNames = getRecordComponentNames( javaType ); + final var index = new int[componentNames.length]; return EmbeddableHelper.resolveIndex( propertyNames, componentNames, index ) ? new EmbeddableInstantiatorRecordIndirectingWithGap( javaType, index ) : new EmbeddableInstantiatorRecordIndirecting( javaType, index ); @@ -49,7 +49,8 @@ public Object instantiate(ValueAccess valuesAccess) { } // Handles gaps, by leaving the value null for that index - private static class EmbeddableInstantiatorRecordIndirectingWithGap extends EmbeddableInstantiatorRecordIndirecting { + private static class EmbeddableInstantiatorRecordIndirectingWithGap + extends EmbeddableInstantiatorRecordIndirecting { public EmbeddableInstantiatorRecordIndirectingWithGap(Class javaType, int[] index) { super( javaType, index ); diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EmbeddableRepresentationStrategyPojo.java b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EmbeddableRepresentationStrategyPojo.java index ff64b3d31b40..86b61966b486 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EmbeddableRepresentationStrategyPojo.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EmbeddableRepresentationStrategyPojo.java @@ -39,7 +39,6 @@ public class EmbeddableRepresentationStrategyPojo implements EmbeddableRepresent private final PropertyAccess[] propertyAccesses; private final Map attributeNameToPositionMap; - private final StrategySelector strategySelector; private final ReflectionOptimizer reflectionOptimizer; private final EmbeddableInstantiator instantiator; private final Map instantiatorsByDiscriminator; @@ -57,20 +56,23 @@ public EmbeddableRepresentationStrategyPojo( propertyAccesses = new PropertyAccess[propertySpan]; attributeNameToPositionMap = new HashMap<>( propertySpan ); + final var strategySelector = + creationContext.getServiceRegistry() + .getService( StrategySelector.class ); + // We need access to the Class objects, used only during initialization final var subclassesByName = getSubclassesByName( bootDescriptor, creationContext ); boolean foundCustomAccessor = false; for ( int i = 0; i < bootDescriptor.getProperties().size(); i++ ) { final var property = bootDescriptor.getProperty( i ); - final Class embeddableClass; - if ( subclassesByName != null ) { - final var subclass = subclassesByName.get( bootDescriptor.getPropertyDeclaringClass( property ) ); - embeddableClass = subclass != null ? subclass : getEmbeddableJavaType().getJavaTypeClass(); - } - else { - embeddableClass = getEmbeddableJavaType().getJavaTypeClass(); - } - propertyAccesses[i] = buildPropertyAccess( property, embeddableClass, customInstantiator == null ); + final var embeddableClass = getEmbeddableClass( bootDescriptor, subclassesByName, property ); + propertyAccesses[i] = + buildPropertyAccess( + property, + embeddableClass, + customInstantiator == null, + strategySelector + ); attributeNameToPositionMap.put( property.getName(), i ); if ( !property.isBasicPropertyAccessor() ) { @@ -78,11 +80,9 @@ public EmbeddableRepresentationStrategyPojo( } } - boolean hasCustomAccessors = foundCustomAccessor; - strategySelector = creationContext.getServiceRegistry().getService( StrategySelector.class ); reflectionOptimizer = buildReflectionOptimizer( bootDescriptor, - hasCustomAccessors, + foundCustomAccessor, propertyAccesses, creationContext ); @@ -120,13 +120,26 @@ public EmbeddableRepresentationStrategyPojo( } } - private static JavaType resolveEmbeddableJavaType( + private Class getEmbeddableClass( + Component bootDescriptor, + Map> subclassesByName, + Property property) { + if ( subclassesByName != null ) { + final var subclass = subclassesByName.get( bootDescriptor.getPropertyDeclaringClass( property ) ); + return subclass != null ? subclass : getEmbeddableJavaType().getJavaTypeClass(); + } + else { + return getEmbeddableJavaType().getJavaTypeClass(); + } + } + + private static JavaType resolveEmbeddableJavaType( Component bootDescriptor, CompositeUserType compositeUserType, RuntimeModelCreationContext creationContext) { final var javaTypeRegistry = creationContext.getTypeConfiguration().getJavaTypeRegistry(); return compositeUserType == null - ? javaTypeRegistry.getDescriptor( bootDescriptor.getComponentClass() ) + ? javaTypeRegistry.resolveDescriptor( bootDescriptor.getComponentClass() ) : javaTypeRegistry.resolveDescriptor( compositeUserType.returnedClass(), () -> new CompositeUserTypeJavaTypeWrapper<>( compositeUserType ) ); } @@ -165,7 +178,8 @@ private static ProxyFactoryFactory getProxyFactoryFactory(RuntimeModelCreationCo private PropertyAccess buildPropertyAccess( Property property, Class embeddableClass, - boolean requireSetters) { + boolean requireSetters, + StrategySelector strategySelector) { final var strategy = propertyAccessStrategy( property, embeddableClass, strategySelector ); if ( strategy == null ) { throw new HibernateException( diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EntityRepresentationStrategyPojoStandard.java b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EntityRepresentationStrategyPojoStandard.java index 32b3a1216e61..d452dbbd0bf0 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EntityRepresentationStrategyPojoStandard.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EntityRepresentationStrategyPojoStandard.java @@ -52,8 +52,6 @@ public class EntityRepresentationStrategyPojoStandard implements EntityRepresent private final ProxyFactory proxyFactory; private final EntityInstantiator instantiator; - private final StrategySelector strategySelector; - private final String identifierPropertyName; private final PropertyAccess identifierPropertyAccess; private final Map propertyAccessMap; @@ -73,17 +71,23 @@ public EntityRepresentationStrategyPojoStandard( isBytecodeEnhanced = isPersistentAttributeInterceptableType( mappedJavaType ); + final var strategySelector = + creationContext.getServiceRegistry() + .getService( StrategySelector.class ); + final var identifierProperty = bootDescriptor.getIdentifierProperty(); if ( identifierProperty == null ) { identifierPropertyName = null; identifierPropertyAccess = null; if ( bootDescriptor.getIdentifier() instanceof Component descriptorIdentifierComponent ) { - final Component identifierMapper = bootDescriptor.getIdentifierMapper(); + final var identifierMapper = bootDescriptor.getIdentifierMapper(); mapsIdRepresentationStrategy = new EmbeddableRepresentationStrategyPojo( identifierMapper == null ? descriptorIdentifierComponent : identifierMapper, () -> { - final var type = (CompositeTypeImplementor) bootDescriptor.getIdentifierMapper().getType(); + final var type = + (CompositeTypeImplementor) + bootDescriptor.getIdentifierMapper().getType(); return type.getMappingModelPart().getEmbeddableTypeDescriptor(); }, // we currently do not support custom instantiators for identifiers @@ -99,11 +103,9 @@ public EntityRepresentationStrategyPojoStandard( else { mapsIdRepresentationStrategy = null; identifierPropertyName = identifierProperty.getName(); - identifierPropertyAccess = makePropertyAccess( identifierProperty ); + identifierPropertyAccess = makePropertyAccess( identifierProperty, strategySelector ); } - strategySelector = creationContext.getServiceRegistry().getService( StrategySelector.class ); - final var bytecodeProvider = creationContext.getBootstrapContext().getServiceRegistry() .requireService( BytecodeProvider.class ); @@ -116,7 +118,7 @@ public EntityRepresentationStrategyPojoStandard( creationContext ); - propertyAccessMap = buildPropertyAccessMap( bootDescriptor ); + propertyAccessMap = buildPropertyAccessMap( bootDescriptor, strategySelector ); reflectionOptimizer = resolveReflectionOptimizer( bytecodeProvider ); instantiator = determineInstantiator( bootDescriptor, runtimeDescriptor ); @@ -129,7 +131,8 @@ private ProxyFactory resolveProxyFactory( BytecodeProvider bytecodeProvider, RuntimeModelCreationContext creationContext) { if ( entityPersister.isAbstract() && bootDescriptor.isConcreteProxy() ) { - // The entity class is abstract, but the hierarchy always gets entities loaded/proxied using their concrete type. + // The entity class is abstract, but the hierarchy always + // gets entities loaded/proxied using their concrete type. // So we do not need proxies for this entity class. return null; } @@ -143,9 +146,10 @@ else if ( entityPersister.getBytecodeEnhancementMetadata().isEnhancedForLazyLoad } else { if ( proxyJavaType != null && entityPersister.isLazy() ) { - final var proxyFactory = createProxyFactory( bootDescriptor, bytecodeProvider, creationContext ); + final var proxyFactory = + createProxyFactory( bootDescriptor, bytecodeProvider, creationContext ); if ( proxyFactory == null ) { - ((EntityMetamodel) entityPersister).setLazy( false ); + ( (EntityMetamodel) entityPersister ).setLazy( false ); } return proxyFactory; } @@ -155,10 +159,11 @@ else if ( entityPersister.getBytecodeEnhancementMetadata().isEnhancedForLazyLoad } } - private Map buildPropertyAccessMap(PersistentClass bootDescriptor) { + private Map buildPropertyAccessMap( + PersistentClass bootDescriptor, StrategySelector strategySelector) { final Map propertyAccessMap = new LinkedHashMap<>(); - for ( var property : bootDescriptor.getPropertyClosure() ) { - propertyAccessMap.put( property.getName(), makePropertyAccess( property ) ); + for ( var property : bootDescriptor.getAllPropertyClosure() ) { + propertyAccessMap.put( property.getName(), makePropertyAccess( property, strategySelector ) ); } return propertyAccessMap; } @@ -309,7 +314,7 @@ private ReflectionOptimizer resolveReflectionOptimizer(BytecodeProvider bytecode return bytecodeProvider.getReflectionOptimizer( mappedJtd.getJavaTypeClass(), propertyAccessMap ); } - private PropertyAccess makePropertyAccess(Property bootAttributeDescriptor) { + private PropertyAccess makePropertyAccess(Property bootAttributeDescriptor, StrategySelector strategySelector) { final var mappedClass = mappedJtd.getJavaTypeClass(); final String descriptorName = bootAttributeDescriptor.getName(); final var strategy = propertyAccessStrategy( bootAttributeDescriptor, mappedClass, strategySelector ); 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 c0c3949b7bf9..671708a1bf09 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 @@ -12,8 +12,8 @@ import org.hibernate.boot.registry.classloading.spi.ClassLoaderService; import org.hibernate.boot.registry.classloading.spi.ClassLoadingException; import org.hibernate.boot.spi.MetadataImplementor; -import org.hibernate.internal.util.collections.ArrayHelper; import org.hibernate.mapping.Component; +import org.hibernate.mapping.IdentifiableTypeClass; import org.hibernate.mapping.MappedSuperclass; import org.hibernate.mapping.PersistentClass; import org.hibernate.mapping.Property; @@ -49,7 +49,8 @@ import java.util.function.BiFunction; import static java.util.Collections.unmodifiableMap; -import static org.hibernate.internal.CoreMessageLogger.CORE_LOGGER; +import static org.hibernate.internal.util.collections.ArrayHelper.contains; +import static org.hibernate.metamodel.mapping.MappingModelCreationLogging.MAPPING_MODEL_CREATION_MESSAGE_LOGGER; import static org.hibernate.internal.util.collections.CollectionHelper.mapOfSize; import static org.hibernate.metamodel.internal.InjectionHelper.injectField; @@ -129,7 +130,7 @@ public TypeConfiguration getTypeConfiguration() { return typeConfiguration; } - public JavaTypeRegistry getJavaTypeRegistry(){ + public JavaTypeRegistry getJavaTypeRegistry() { return typeConfiguration.getJavaTypeRegistry(); } @@ -172,7 +173,7 @@ public Map, MappedSuperclassDomainType> getMappedSuperclassTypeMap() return mappedSuperClassTypeMap; } - public void registerEntityType(PersistentClass persistentClass, EntityTypeImpl entityType) { + public void registerEntityType(PersistentClass persistentClass, EntityTypeImpl entityType) { final var javaType = entityType.getJavaType(); if ( javaType != null && javaType != Map.class ) { entityTypes.put( javaType, entityType ); @@ -222,7 +223,6 @@ public void registerMappedSuperclassType( * implementation. May return null if the given {@link PersistentClass} has not yet been processed. * * @param persistentClass The Hibernate (config time) metamodel instance representing an entity. - * * @return Tne corresponding JPA {@link org.hibernate.type.EntityType}, or null if not yet processed. */ public EntityDomainType locateEntityType(PersistentClass persistentClass) { @@ -234,7 +234,6 @@ public EntityDomainType locateEntityType(PersistentClass persistentClass) { * return null which could mean that no such mapping exists at least at this time. * * @param javaType The java class. - * * @return The corresponding JPA {@link org.hibernate.type.EntityType}, or null. */ public EntityDomainType locateEntityType(Class javaType) { @@ -246,7 +245,6 @@ public EntityDomainType locateEntityType(Class javaType) { * return null which could means that no such mapping exists at least at this time. * * @param entityName The entity-name. - * * @return The corresponding JPA {@link org.hibernate.type.EntityType}, or null. */ public IdentifiableDomainType locateIdentifiableType(String entityName) { @@ -287,9 +285,7 @@ public Map> getIdentifiableTypesByName() { } public void wrapUp() { - if ( CORE_LOGGER.isTraceEnabled() ) { - CORE_LOGGER.trace( "Wrapping up metadata context..." ); - } + MAPPING_MODEL_CREATION_MESSAGE_LOGGER.wrappingUpMetadataContext(); final boolean staticMetamodelScanEnabled = jpaStaticMetaModelPopulationSetting != JpaStaticMetamodelPopulationSetting.DISABLED; @@ -298,8 +294,9 @@ public void wrapUp() { //we need to process types from superclasses to subclasses for ( Object mapping : orderedMappings ) { if ( mapping instanceof PersistentClass persistentClass ) { - if ( CORE_LOGGER.isTraceEnabled() ) { - CORE_LOGGER.trace( "Starting entity [" + persistentClass.getEntityName() + ']' ); + if ( MAPPING_MODEL_CREATION_MESSAGE_LOGGER.isTraceEnabled() ) { + MAPPING_MODEL_CREATION_MESSAGE_LOGGER.startingEntity( + persistentClass.getEntityName() ); } try { final var jpaMapping = entityTypesByPersistentClass.get( persistentClass ); @@ -309,17 +306,16 @@ public void wrapUp() { applyGenericProperties( persistentClass, jpaMapping ); for ( var property : persistentClass.getDeclaredProperties() ) { - if ( property.getValue() == persistentClass.getIdentifierMapper() ) { - // property represents special handling for id-class mappings but we have already - // accounted for the embedded property mappings in #applyIdMetadata && - // #buildIdClassAttributes - continue; - } - if ( persistentClass.isVersioned() && property == persistentClass.getVersion() ) { + // property represents special handling for @IdClass mappings, + // but we have already accounted for the embedded property mappings + // in #applyIdMetadata && #buildIdClassAttributes + if ( property.getValue() != persistentClass.getIdentifierMapper() // skip the version property, it was already handled previously. - continue; + && !isVersion( persistentClass, property ) + // Skip generic properties since they may only be declared on abstract classes + && !property.isGenericSpecialization() ) { + buildAttribute( property, jpaMapping ); } - buildAttribute( property, jpaMapping ); } ( (AttributeContainer) jpaMapping ).getInFlightAccess().finishUp(); @@ -329,14 +325,16 @@ public void wrapUp() { } } finally { - if ( CORE_LOGGER.isTraceEnabled() ) { - CORE_LOGGER.trace( "Completed entity [" + persistentClass.getEntityName() + ']' ); + if ( MAPPING_MODEL_CREATION_MESSAGE_LOGGER.isTraceEnabled() ) { + MAPPING_MODEL_CREATION_MESSAGE_LOGGER.completedEntity( + persistentClass.getEntityName() ); } } } else if ( mapping instanceof MappedSuperclass mappedSuperclass ) { - if ( CORE_LOGGER.isTraceEnabled() ) { - CORE_LOGGER.trace( "Starting mapped superclass [" + mappedSuperclass.getMappedClass().getName() + ']' ); + if ( MAPPING_MODEL_CREATION_MESSAGE_LOGGER.isTraceEnabled() ) { + MAPPING_MODEL_CREATION_MESSAGE_LOGGER.startingMappedSuperclass( + mappedSuperclass.getMappedClass().getName() ); } try { final var jpaType = mappedSuperclassByMappedSuperclassMapping.get( mappedSuperclass ); @@ -346,17 +344,14 @@ else if ( mapping instanceof MappedSuperclass mappedSuperclass ) { // applyNaturalIdAttribute( safeMapping, jpaType ); for ( var property : mappedSuperclass.getDeclaredProperties() ) { - if ( isIdentifierProperty( property, mappedSuperclass ) ) { - // property represents special handling for id-class mappings but we have already - // accounted for the embedded property mappings in #applyIdMetadata && - // #buildIdClassAttributes - continue; - } - else if ( mappedSuperclass.isVersioned() && property == mappedSuperclass.getVersion() ) { + // property represents special handling for @IdClass mappings, + // but we have already accounted for the embedded property mappings + // in #applyIdMetadata && #buildIdClassAttributes + if ( !isIdentifierProperty( property, mappedSuperclass ) // skip the version property, it was already handled previously. - continue; + && !isVersion( mappedSuperclass, property ) ) { + buildAttribute( property, jpaType ); } - buildAttribute( property, jpaType ); } ( (AttributeContainer) jpaType ).getInFlightAccess().finishUp(); @@ -366,8 +361,9 @@ else if ( mappedSuperclass.isVersioned() && property == mappedSuperclass.getVers } } finally { - if ( CORE_LOGGER.isTraceEnabled() ) { - CORE_LOGGER.trace( "Completed mapped superclass [" + mappedSuperclass.getMappedClass().getName() + ']' ); + if ( MAPPING_MODEL_CREATION_MESSAGE_LOGGER.isTraceEnabled() ) { + MAPPING_MODEL_CREATION_MESSAGE_LOGGER.completedMappedSuperclass( + mappedSuperclass.getMappedClass().getName() ); } } } @@ -377,7 +373,7 @@ else if ( mappedSuperclass.isVersioned() && property == mappedSuperclass.getVers } - while ( ! embeddablesToProcess.isEmpty() ) { + while ( !embeddablesToProcess.isEmpty() ) { final List> processingEmbeddables = new ArrayList<>( embeddablesToProcess.size() ); for ( var embeddableDomainTypes : embeddablesToProcess.values() ) { @@ -409,14 +405,18 @@ else if ( mappedSuperclass.isVersioned() && property == mappedSuperclass.getVers } } + private static boolean isVersion(IdentifiableTypeClass persistentClass, Property property) { + return persistentClass.isVersioned() && property == persistentClass.getVersion(); + } + private static boolean isIdentifierProperty(Property property, MappedSuperclass mappedSuperclass) { final var identifierMapper = mappedSuperclass.getIdentifierMapper(); return identifierMapper != null - && ArrayHelper.contains( identifierMapper.getPropertyNames(), property.getName() ); + && contains( identifierMapper.getPropertyNames(), property.getName() ); } private void addAttribute(EmbeddableDomainType embeddable, Property property, Component component) { - final var attribute = buildAttribute( embeddable, property); + final var attribute = buildAttribute( embeddable, property ); if ( attribute != null ) { final var superclassProperty = getMappedSuperclassProperty( property.getName(), @@ -445,26 +445,47 @@ private void buildAttribute(Property property, IdentifiableDomainType jpa } private void addAttribute(ManagedDomainType type, PersistentAttribute attribute) { - final var container = (AttributeContainer) type; - final var inFlightAccess = container.getInFlightAccess(); - final boolean virtual = - attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.EMBEDDED - && attribute.getAttributeJavaType() instanceof EntityJavaType; - if ( virtual ) { - @SuppressWarnings("unchecked") - final var embeddableDomainType = (EmbeddableDomainType) attribute.getValueGraphType(); - final var component = componentByEmbeddable.get( embeddableDomainType ); - for ( var property : component.getProperties() ) { - final var subAttribute = buildAttribute( embeddableDomainType, property ); - if ( subAttribute != null ) { - inFlightAccess.addAttribute( subAttribute ); - } + if ( isVirtual( attribute ) ) { + final var embeddableDomainType = (EmbeddableDomainType) attribute.getValueGraphType(); + if ( attribute.getValueGraphType().getJavaType() == type.getJavaType() ) { + // weird hbm.xml construct used as a target for property-ref + @SuppressWarnings("unchecked") // Safe, we just checked + final var virtualEmbeddedType = (EmbeddableDomainType) embeddableDomainType; + buildVirtualEmbeddedAttribute( virtualEmbeddedType, (AttributeContainer) type ); + } + else { + // occurs in a hbm.xml test where a class is mapped both as an entity and as embeddable + buildVirtualEmbeddedAttribute( embeddableDomainType ); } if ( jpaMetaModelPopulationSetting != JpaMetamodelPopulationSetting.ENABLED ) { return; } } - inFlightAccess.addAttribute( attribute ); + final var container = (AttributeContainer) type; + container.getInFlightAccess().addAttribute( attribute ); + } + + private static boolean isVirtual(PersistentAttribute attribute) { + return attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.EMBEDDED + && attribute.getAttributeJavaType() instanceof EntityJavaType; + } + + private void buildVirtualEmbeddedAttribute(EmbeddableDomainType embeddableDomainType) { + //noinspection UnnecessaryLocalVariable + final ManagedDomainType domainType = embeddableDomainType; + buildVirtualEmbeddedAttribute( embeddableDomainType, (AttributeContainer) domainType ); + } + + private void buildVirtualEmbeddedAttribute( + EmbeddableDomainType embeddableDomainType, AttributeContainer container) { + final var inFlightAccess = container.getInFlightAccess(); + final var component = componentByEmbeddable.get( embeddableDomainType ); + for ( var property : component.getProperties() ) { + final var subAttribute = buildAttribute( embeddableDomainType, property ); + if ( subAttribute != null ) { + inFlightAccess.addAttribute( subAttribute ); + } + } } // 1) create the part @@ -507,56 +528,51 @@ private void applyIdAttributes( PersistentClass persistentClass, IdentifiableDomainType identifiableType, Component compositeId) { + assert compositeId.isEmbedded(); // Handle the actual id attributes - final List cidProperties; - final int propertySpan; - final EmbeddableTypeImpl idClassType; final var identifierMapper = persistentClass.getIdentifierMapper(); - if ( identifierMapper != null ) { - cidProperties = identifierMapper.getProperties(); - propertySpan = identifierMapper.getPropertySpan(); - idClassType = - identifierMapper.getComponentClassName() == null - ? null // support for no id-class, especially for dynamic models - : applyIdClassMetadata( (Component) persistentClass.getIdentifier() ); - } - else { - cidProperties = compositeId.getProperties(); - propertySpan = compositeId.getPropertySpan(); - idClassType = null; - } - - assert compositeId.isEmbedded(); + final var idClassType = + identifierMapper == null || identifierMapper.getComponentClassName() == null + ? null // support for no @IdClass, especially for dynamic models + : applyIdClassMetadata( (Component) persistentClass.getIdentifier() ); + final var id = identifierMapper == null ? compositeId : identifierMapper; + final var compositeIdProperties = id.getProperties(); + final int propertySpan = id.getPropertySpan(); - final var idDomainType = identifiableTypesByName.get( compositeId.getOwner().getEntityName() ); - @SuppressWarnings("unchecked") - final var idType = (AbstractIdentifiableType) idDomainType; - applyIdAttributes( identifiableType, idType, propertySpan, cidProperties, idClassType ); + final var domainType = identifiableTypesByName.get( compositeId.getOwner().getEntityName() ); + if ( !domainType.getJavaType().isAssignableFrom( identifiableType.getJavaType() ) ) { + throw new AssertionFailure( "Type mismatch:" + + " expected " + identifiableType.getJavaType().getTypeName() + + " but was " + domainType.getJavaType().getTypeName() ); + } + @SuppressWarnings("unchecked") // Safe, we just checked + final var castDomainType = (AbstractIdentifiableType) domainType; + applyIdAttributes( identifiableType, castDomainType, propertySpan, compositeIdProperties, idClassType ); } private void applyIdAttributes( IdentifiableDomainType identifiableType, - AbstractIdentifiableType idType, + AbstractIdentifiableType idType, int propertySpan, - List cidProperties, + List compositeIdProperties, EmbeddableTypeImpl idClassType) { - final var idAttributes = idClassAttributes( idType, propertySpan, cidProperties ); + final var idAttributes = idClassAttributes( idType, propertySpan, compositeIdProperties ); final var managedType = (ManagedDomainType) identifiableType; final var container = (AttributeContainer) managedType; - container.getInFlightAccess().applyNonAggregatedIdAttributes( idAttributes, idClassType); + container.getInFlightAccess().applyNonAggregatedIdAttributes( idAttributes, idClassType ); } private Set> idClassAttributes( AbstractIdentifiableType idType, int propertySpan, - List cidProperties) { + List compositeIdProperties) { final var idAttributes = idType.getIdClassAttributesSafely(); if ( idAttributes != null ) { return idAttributes; } else { final Set> result = new HashSet<>( propertySpan ); - for ( Property cidSubproperty : cidProperties ) { + for ( var cidSubproperty : compositeIdProperties ) { result.add( buildIdAttribute( idType, cidSubproperty ) ); } return result; @@ -566,7 +582,7 @@ private void applyIdAttributes( private Property getMappedSuperclassIdentifier(PersistentClass persistentClass) { MappedSuperclass mappedSuperclass = getMappedSuperclass( persistentClass ); while ( mappedSuperclass != null ) { - final Property declaredIdentifierProperty = mappedSuperclass.getDeclaredIdentifierProperty(); + final var declaredIdentifierProperty = mappedSuperclass.getDeclaredIdentifierProperty(); if ( declaredIdentifierProperty != null ) { return declaredIdentifierProperty; } @@ -584,26 +600,39 @@ private EmbeddableTypeImpl applyIdClassMetadata(Component idClassComponent) { private EmbeddableTypeImpl embeddableType(Component idClassComponent, Class componentClass) { return new EmbeddableTypeImpl<>( getJavaTypeRegistry().resolveManagedTypeDescriptor( componentClass ), - getMappedSuperclassDomainType( idClassComponent ), + getMappedSuperclassDomainType( idClassComponent, componentClass ), null, false, getJpaMetamodel() ); } - @SuppressWarnings("unchecked") - private MappedSuperclassDomainType getMappedSuperclassDomainType(Component idClassComponent) { + private MappedSuperclassDomainType getMappedSuperclassDomainType( + Component idClassComponent, Class componentClass) { final var mappedSuperclass = idClassComponent.getMappedSuperclass(); - return mappedSuperclass == null ? null - : (MappedSuperclassDomainType) - locateMappedSuperclassType( mappedSuperclass ); + if ( mappedSuperclass == null ) { + return null; + } + else { + final var domainType = locateMappedSuperclassType( mappedSuperclass ); + final var mappedSuperclassClass = domainType.getJavaType(); + if ( !mappedSuperclassClass.isAssignableFrom( componentClass ) ) { + throw new IllegalStateException( + mappedSuperclassClass.getTypeName() + + " is not a supertype of " + componentClass.getTypeName() + ); + } + @SuppressWarnings("unchecked") // Safe, we just checked + final var castDomainType = (MappedSuperclassDomainType) domainType; + return castDomainType; + } } private void applyIdMetadata(MappedSuperclass mappingType, MappedSuperclassDomainType jpaMappingType) { final var managedType = (ManagedDomainType) jpaMappingType; final var attributeContainer = (AttributeContainer) managedType; if ( mappingType.hasIdentifierProperty() ) { - final Property declaredIdentifierProperty = mappingType.getDeclaredIdentifierProperty(); + final var declaredIdentifierProperty = mappingType.getDeclaredIdentifierProperty(); if ( declaredIdentifierProperty != null ) { final var attribute = (SingularPersistentAttribute) @@ -655,6 +684,17 @@ private void applyGenericProperties(PersistentClass persistentClass, EntityD } mappedSuperclass = getMappedSuperclass( mappedSuperclass ); } + final Boolean persistentClassAbstract = persistentClass.isAbstract(); + if ( persistentClassAbstract == null || !persistentClassAbstract ) { + for ( var property : persistentClass.getDeclaredProperties() ) { + if ( property.isGenericSpecialization() ) { + final var managedType = (ManagedDomainType) entityType; + final var attributeContainer = (AttributeContainer) managedType; + attributeContainer.getInFlightAccess() + .addConcreteGenericAttribute( buildAttribute( entityType, property ) ); + } + } + } } private MappedSuperclass getMappedSuperclass(PersistentClass persistentClass) { @@ -701,8 +741,9 @@ private Property getMappedSuperclassProperty(String propertyName, MappedSupercla private Set> buildIdClassAttributes( IdentifiableDomainType ownerType, List properties) { - if ( CORE_LOGGER.isTraceEnabled() ) { - CORE_LOGGER.trace( "Building old-school composite identifier [" + ownerType.getJavaType().getName() + ']' ); + if ( MAPPING_MODEL_CREATION_MESSAGE_LOGGER.isTraceEnabled() ) { + MAPPING_MODEL_CREATION_MESSAGE_LOGGER.buildingOldSchoolCompositeIdentifier( + ownerType.getJavaType().getName() ); } final Set> attributes = new HashSet<>(); for ( var property : properties ) { @@ -805,10 +846,7 @@ private void registerAttribute(Class metamodelClass, Attribute attr injectField( metamodelClass, name, attribute, allowNonDeclaredFieldReference ); } catch (NoSuchFieldException e) { - CORE_LOGGER.unableToLocateStaticMetamodelField( metamodelClass.getName(), name ); -// throw new AssertionFailure( -// "Unable to locate static metamodel field: " + metamodelClass.getName() + '#' + name -// ); + MAPPING_MODEL_CREATION_MESSAGE_LOGGER.unableToLocateStaticMetamodelField( metamodelClass.getName(), name ); } } @@ -844,11 +882,10 @@ public Set getUnusedMappedSuperclasses() { return new HashSet<>( knownMappedSuperclasses ); } - private final Map,BasicDomainType> basicDomainTypeMap = new HashMap<>(); + private final Map, BasicDomainType> basicDomainTypeMap = new HashMap<>(); public BasicDomainType resolveBasicType(Class javaType) { - @SuppressWarnings("unchecked") - final var domainType = (BasicDomainType) basicDomainTypeMap.get( javaType ); + final var domainType = basicDomainTypeMap.get( javaType ); if ( domainType == null ) { // we cannot use getTypeConfiguration().standardBasicTypeForJavaType(javaType) // because that doesn't return the right thing for primitive types @@ -856,14 +893,22 @@ public BasicDomainType resolveBasicType(Class javaType) { return basicDomainType( javaType ); } else { - return domainType; + if ( domainType.getJavaType() != javaType ) { + throw new AssertionFailure( "Basic type mismatch:" + + " expected " + javaType.getTypeName() + + " but was " + domainType.getJavaType().getTypeName() ); + } + @SuppressWarnings("unchecked") // Safe, we just checked + final var castDomainType = (BasicDomainType) domainType; + return castDomainType; } } private BasicDomainType basicDomainType(Class javaType) { final var javaTypeDescriptor = getJavaTypeRegistry().resolveDescriptor( javaType ); final var jdbcType = - javaTypeDescriptor.getRecommendedJdbcType( typeConfiguration.getCurrentBaseSqlTypeIndicators() ); + javaTypeDescriptor.getRecommendedJdbcType( + typeConfiguration.getCurrentBaseSqlTypeIndicators() ); return javaType.isPrimitive() ? new PrimitiveBasicTypeImpl<>( javaTypeDescriptor, jdbcType, javaType ) : new BasicTypeImpl<>( javaTypeDescriptor, jdbcType ); diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/AuditMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/AuditMapping.java new file mode 100644 index 000000000000..5c1761fb19ed --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/AuditMapping.java @@ -0,0 +1,25 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.metamodel.mapping; + +import org.hibernate.Incubating; + +/** + * Metadata about audit log tables for entities and collections enabled for audit logging. + * + * @see org.hibernate.annotations.Audited + * + * @author Gavin King + * + * @since 7.4 + */ +@Incubating +public interface AuditMapping extends AuxiliaryMapping { + + SelectableMapping getTransactionIdMapping(); + + SelectableMapping getModificationTypeMapping(); + +} diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/AuxiliaryMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/AuxiliaryMapping.java new file mode 100644 index 000000000000..8ff3e391a6d8 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/AuxiliaryMapping.java @@ -0,0 +1,74 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.metamodel.mapping; + +import org.hibernate.Incubating; +import org.hibernate.engine.spi.LoadQueryInfluencers; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.spi.NavigablePath; +import org.hibernate.sql.ast.spi.SqlAliasBaseGenerator; +import org.hibernate.sql.ast.spi.SqlAstCreationState; +import org.hibernate.sql.ast.tree.from.LazyTableGroup; +import org.hibernate.sql.ast.tree.from.NamedTableReference; +import org.hibernate.sql.ast.tree.from.StandardTableGroup; +import org.hibernate.sql.ast.tree.from.TableGroup; +import org.hibernate.sql.ast.tree.from.TableGroupJoin; +import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.sql.model.ast.builder.MutationGroupBuilder; + +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * Unified mapping contract for state management strategies (soft-delete, temporal, audit). + * + * @author Gavin King + * + * @since 7.4 + */ +@Incubating +public interface AuxiliaryMapping { + /** + * The name of the table to which this auxiliary mapping applies. + */ + String getTableName(); + + default void addToInsertGroup(MutationGroupBuilder insertGroupBuilder, EntityPersister persister) {} + + void applyPredicate( + EntityMappingType associatedEntityMappingType, + Consumer predicateConsumer, + LazyTableGroup lazyTableGroup, + NavigablePath navigablePath, + SqlAstCreationState creationState); + + void applyPredicate( + EntityMappingType associatedEntityDescriptor, + Consumer predicateConsumer, + TableGroup tableGroup, + SqlAliasBaseGenerator sqlAliasBaseGenerator, + LoadQueryInfluencers influencers); + + void applyPredicate( + PluralAttributeMapping associatedEntityDescriptor, + Consumer predicateConsumer, + TableGroup tableGroup, + SqlAliasBaseGenerator sqlAliasBaseGenerator, + LoadQueryInfluencers influencers); + + void applyPredicate(TableGroupJoin tableGroupJoin, LoadQueryInfluencers loadQueryInfluencers); + + void applyPredicate( + Supplier> predicateCollector, + SqlAstCreationState creationState, + StandardTableGroup tableGroup, + NamedTableReference rootTableReference, EntityMappingType entityMappingType); + + JdbcMapping getJdbcMapping(); + + boolean useAuxiliaryTable(LoadQueryInfluencers influencers); + + boolean isAffectedByInfluencers(LoadQueryInfluencers influencers); +} diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/Bindable.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/Bindable.java index e3bb99e58b79..3f9b77e4826f 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/Bindable.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/Bindable.java @@ -43,8 +43,7 @@ default int forEachJdbcType(IndexedConsumer action) { *

      * Generally speaking, this is the form in which entity state is kept relative to a * Session via {@code EntityEntry}. - *

      - *

      +	 * 
      {@code
       	 * @Entity class Person {
       	 *     @Id Integer id;
       	 *     @Embedded Name name;
      @@ -55,27 +54,23 @@ default int forEachJdbcType(IndexedConsumer action) {
       	 *     String familiarName;
       	 *     String familyName;
       	 * }
      -	 * 
      + * }
      *

      * At the top level, we would want to disassemble a {@code Person} value, so we'd * ask the {@code Bindable} for the {@code Person} entity to disassemble. Given a * {@code Person} value: - *

      *

       	 * Person( id=1, name=Name( 'Steve', 'Ebersole' ), 28 )
       	 * 
      - *

      * this disassemble would result in a multidimensional array: - *

      - *

      +	 * 
      {@code
       	 * [ ["Steve", "Ebersole"], 28 ]
      -	 * 
      + * }
      *

      * Note that the identifier is not part of this disassembled state. Note also * how the embedded value results in a sub-array. * * @see org.hibernate.engine.spi.EntityEntry - *

      */ Object disassemble(Object value, SharedSessionContractImplementor session); @@ -94,12 +89,11 @@ default int forEachJdbcType(IndexedConsumer action) { *

      * Given the example in {@link #disassemble}, this results in the consumer being * called for each simple value. E.g.: - *

      - *

      +	 * 
      {@code
       	 * consumer.consume( "Steve" );
       	 * consumer.consume( "Ebersole" );
       	 * consumer.consume( 28 );
      -	 * 
      + * }
      *

      * Think of it as breaking the multidimensional array into a visitable flat array. * Additionally, it passes through the values {@code X} and {@code Y} to the consumer. diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/DiscriminatorConverter.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/DiscriminatorConverter.java index e9c24d6c58ff..7452af89019c 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/DiscriminatorConverter.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/DiscriminatorConverter.java @@ -72,7 +72,8 @@ public R toRelationalValue(O domainForm) { else { final var discriminatorValueDetails = getDetailsForEntityName( entityName ); //noinspection unchecked - return (R) discriminatorValueDetails.getValue(); + Object value = discriminatorValueDetails.getValue(); + return (R) value; } } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/DiscriminatorMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/DiscriminatorMapping.java index 2d5dfeb55676..6b6faacb965e 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/DiscriminatorMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/DiscriminatorMapping.java @@ -19,7 +19,7 @@ /** * Mapping of a discriminator, for either {@linkplain EntityMappingType#getDiscriminatorMapping() entity} or * {@linkplain DiscriminatedAssociationModelPart#getDiscriminatorMapping() association} (ANY) discrimination. - *

      + *

      * Represents a composition of

        *
      • a {@linkplain #getValueConverter() converter} between the domain and relational form
      • *
      • a {@linkplain #getUnderlyingJdbcMapping JDBC mapping} to read and write the relational values
      • diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/DiscriminatorValue.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/DiscriminatorValue.java new file mode 100644 index 000000000000..0a79762f0b34 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/DiscriminatorValue.java @@ -0,0 +1,64 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.metamodel.mapping; + +import java.io.Serializable; +import java.util.Objects; + +/** + * Represents the discriminator value used for entity mappings. + */ +public sealed interface DiscriminatorValue extends Serializable { + + Object value(); + + static DiscriminatorValue of(Object value) { + return value == null ? Special.NULL : new Literal( value ); + } + + record Literal(Object value) implements DiscriminatorValue { + public Literal { + Objects.requireNonNull( value, "discriminator literal value must not be null" ); + } + + @Override + public String toString() { + return value.toString(); + } + + @Override + public boolean equals(Object object) { + return this == object + || object instanceof Literal that && this.value.equals(that.value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + } + + enum Special implements DiscriminatorValue { + NULL, + NOT_NULL; + + @Override + public Object value() { + return switch ( this ) { + case NULL -> null; + case NOT_NULL -> + throw new IllegalStateException( "Cannot obtain a discriminator value for NOT_NULL mapping" ); + }; + } + + @Override + public String toString() { + return switch ( this ) { + case NULL -> ""; + case NOT_NULL -> ""; + }; + } + } +} 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 57f9b8762b89..c89dc3fa0e7a 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 @@ -5,7 +5,11 @@ package org.hibernate.metamodel.mapping; import jakarta.persistence.Entity; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.hibernate.AssertionFailure; import org.hibernate.Filter; +import org.hibernate.HibernateException; import org.hibernate.Incubating; import org.hibernate.Internal; import org.hibernate.annotations.ConcreteProxy; @@ -45,6 +49,7 @@ import java.util.function.Consumer; import java.util.function.Supplier; +import static java.lang.String.format; import static org.hibernate.bytecode.enhance.spi.LazyPropertyInitializer.UNFETCHED_PROPERTY; /** @@ -61,7 +66,7 @@ public interface EntityMappingType /** * The entity name. - *

        + *

        * For most entities, this will be the fully-qualified name * of the entity class. The alternative is an explicit * {@linkplain org.hibernate.boot.jaxb.mapping.spi.JaxbEntity#getName() entity-name} which takes precedence if provided @@ -248,10 +253,22 @@ default Set getSubclassEntityNames() { /** * The discriminator value which indicates this entity mapping */ - Object getDiscriminatorValue(); + DiscriminatorValue getDiscriminatorValue(); default String getDiscriminatorSQLValue() { - return getDiscriminatorValue().toString(); + final DiscriminatorValue discriminatorValue = getDiscriminatorValue(); + if ( discriminatorValue instanceof DiscriminatorValue.Literal literal ) { + return String.valueOf( literal.value() ); + } + else if ( discriminatorValue instanceof DiscriminatorValue.Special special ) { + return switch ( special ) { + case NULL -> "null"; + case NOT_NULL -> throw new IllegalStateException( "Illegal call for NOT_NULL discriminator" ); + }; + } + else { + throw new AssertionFailure( "Unrecognized DiscriminatorValue" ); + } } default EntityMappingType getRootEntityDescriptor() { @@ -363,7 +380,16 @@ default OptimisticLockStyle optimisticLockStyle() { /** * The mapping for the natural-id of the entity, if one is defined */ - NaturalIdMapping getNaturalIdMapping(); + @Nullable NaturalIdMapping getNaturalIdMapping(); + + @NonNull + default NaturalIdMapping requireNaturalIdMapping() { + var mapping = getNaturalIdMapping(); + if ( mapping == null ) { + throw new HibernateException( format( "Entity %s does not specify a natural id", getEntityName() ) ); + } + return mapping; + } /** * The mapping for the row-id of the entity, if one is defined. @@ -373,10 +399,31 @@ default OptimisticLockStyle optimisticLockStyle() { /** * Mapping for soft-delete support, or {@code null} if soft-delete not defined */ + @Incubating default SoftDeleteMapping getSoftDeleteMapping() { return null; } + /** + * Mapping for temporal entity support, or {@code null} if not defined. + */ + @Incubating + default TemporalMapping getTemporalMapping() { + return null; + } + + /** + * Mapping for audit support, or {@code null} if not defined. + */ + @Incubating + default AuditMapping getAuditMapping() { + return null; + } + + default AuxiliaryMapping getAuxiliaryMapping() { + return null; + } + @Override default TableDetails getSoftDeleteTableDetails() { return getIdentifierTableDetails(); diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/MappingModelCreationLogging.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/MappingModelCreationLogging.java index 87863523a66c..d12a3e09cbfb 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/MappingModelCreationLogging.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/MappingModelCreationLogging.java @@ -9,10 +9,16 @@ import org.jboss.logging.BasicLogger; import org.jboss.logging.Logger; +import org.jboss.logging.annotations.LogMessage; +import org.jboss.logging.annotations.Message; import org.jboss.logging.annotations.MessageLogger; import org.jboss.logging.annotations.ValidIdRange; import java.lang.invoke.MethodHandles; +import java.util.Locale; + +import static org.jboss.logging.Logger.Level.TRACE; +import static org.jboss.logging.Logger.Level.WARN; /** * Logger used during mapping-model creation @@ -31,5 +37,33 @@ public interface MappingModelCreationLogging extends BasicLogger { Logger MAPPING_MODEL_CREATION_LOGGER = Logger.getLogger( LOGGER_NAME ); MappingModelCreationLogging MAPPING_MODEL_CREATION_MESSAGE_LOGGER = - Logger.getMessageLogger( MethodHandles.lookup(), MappingModelCreationLogging.class, LOGGER_NAME ); + Logger.getMessageLogger( MethodHandles.lookup(), MappingModelCreationLogging.class, LOGGER_NAME, Locale.ROOT ); + + @LogMessage(level = TRACE) + @Message(id = 90005701, value = "Wrapping up metadata context...") + void wrappingUpMetadataContext(); + + @LogMessage(level = TRACE) + @Message(id = 90005702, value = "Starting entity [%s]") + void startingEntity(String entityName); + + @LogMessage(level = TRACE) + @Message(id = 90005703, value = "Completed entity [%s]") + void completedEntity(String entityName); + + @LogMessage(level = TRACE) + @Message(id = 90005704, value = "Starting mapped superclass [%s]") + void startingMappedSuperclass(String name); + + @LogMessage(level = TRACE) + @Message(id = 90005705, value = "Completed mapped superclass [%s]") + void completedMappedSuperclass(String name); + + @LogMessage(level = TRACE) + @Message(id = 90005706, value = "Building old-school composite identifier [%s]") + void buildingOldSchoolCompositeIdentifier(String name); + + @LogMessage(level = WARN) + @Message(id = 90005707, value = "Unable to locate static metamodel field: %s.%s") + void unableToLocateStaticMetamodelField(String className, String fieldName); } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/ModelPart.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/ModelPart.java index 6c9060842dfb..4fc3a3d76495 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/ModelPart.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/ModelPart.java @@ -19,74 +19,57 @@ import org.checkerframework.checker.nullness.qual.Nullable; -/** - * Base descriptor, within the mapping model, for any part of the - * application's domain model: an attribute, an entity identifier, - * collection elements, and so on. - * - * @see DomainResultProducer - * @see jakarta.persistence.metamodel.Bindable - * - * @author Steve Ebersole - */ +/// Base descriptor, within the mapping model, for any part of the +/// application's domain model: an attribute, an entity identifier, +/// collection elements, and so on. +/// +/// @see DomainResultProducer +/// @see jakarta.persistence.metamodel.Bindable +/// +/// @author Steve Ebersole public interface ModelPart extends MappingModelExpressible { - /** - * @asciidoc - * - * The path for this fetchable back to an entity in the domain model. Acts as a unique - * identifier for individual parts. - * - * Some examples: - * - * For an entity, the role name is simply the entity name. - * - * For embeddable the role name is the path back to the root entity. E.g. a Person's address - * would be a path `Person#address`. - * - * For a collection the path would be the same as the "collection role". E.g. an Order's lineItems - * would be `Order#lineItems`. This is the same as the historical `CollectionPersister#getRoleName`. - * - * For the (model)parts of a collection the role is either `{element}` or `{index}` depending. E.g. - * `Order#lineItems.{element}`. Attributes of the element or index type (embeddable or entity typed) - * would be based on this role. E.g. `Order#lineItems.{element}.quantity` - * - * For an attribute of an embedded, the role would be relative to its "container". E.g. `Person#address.city` or - * `Person#addresses.{element}.city` - * - * @apiNote Whereas {@link #getPartName()} is local to this part, NavigableRole can be a compound path - * - * @see #getPartName() - */ + /// The path for this fetchable back to an entity in the domain model. Acts as a unique + /// identifier for individual parts. + /// Some examples: + /// * For an entity, the role name is simply the entity name. + /// * For embeddable the role name is the path back to the root entity. E.g. a Person's address + /// would be a path `Person#address`. + /// + /// For a collection the path would be the same as the "collection role". E.g. an Order's lineItems + /// would be `Order#lineItems`. This is the same as the historical `CollectionPersister#getRoleName`. + /// + /// For the (model)parts of a collection the role is either `{element}` or `{index}` depending. E.g. + /// `Order#lineItems.{element}`. Attributes of the element or index type (embeddable or entity typed) + /// would be based on this role. E.g. `Order#lineItems.{element}.quantity` + /// + /// For an attribute of an embedded, the role would be relative to its "container". E.g. `Person#address.city` or + /// `Person#addresses.{element}.city` + /// + /// @apiNote Whereas [#getPartName()] is local to this part, NavigableRole can be a compound path + /// + /// @see #getPartName() NavigableRole getNavigableRole(); - /** - * The local part name, which is generally the unqualified role name - */ + /// The local part name, which is generally the unqualified role name String getPartName(); - /** - * The type for this part. - */ + /// The type for this part. MappingType getPartMappingType(); - /** - * The Java type for this part. Generally equivalent to - * {@link MappingType#getMappedJavaType()} relative to - * {@link #getPartMappingType()} - */ + /// The Java type for this part. Generally equivalent to + /// `getMappedJavaType()` relative to + /// [#getPartMappingType()] JavaType getJavaType(); - /** - * Whether this model part describes something that physically - * exists in the domain model. - *

        - * For example, an entity's {@linkplain EntityDiscriminatorMapping discriminator} - * is part of the model, but is not a physical part of the domain model - there - * is no "discriminator attribute". - *

        - * Also indicates whether the part is castable to {@link VirtualModelPart} - */ + /// Whether this model part describes something that physically + /// exists in the domain model. + /// + /// For example, an entity's {@linkplain EntityDiscriminatorMapping discriminator} + /// is part of the model, but is not a physical part of the domain model - there + /// is no "discriminator attribute". + /// + /// Also indicates whether the part is castable to [VirtualModelPart] default boolean isVirtual() { return false; } @@ -97,43 +80,35 @@ default boolean isEntityIdentifierMapping() { boolean hasPartitionedSelectionMapping(); - /** - * Create a DomainResult for a specific reference to this ModelPart. - */ + /// Create a [DomainResult] for a specific reference to this [ModelPart]. DomainResult createDomainResult( NavigablePath navigablePath, TableGroup tableGroup, String resultVariable, DomainResultCreationState creationState); - /** - * Apply SQL selections for a specific reference to this ModelPart outside the domain query's root select clause. - */ + /// Apply SQL selections for a specific reference to this [ModelPart] + /// outside the domain query's root select clause. void applySqlSelections( NavigablePath navigablePath, TableGroup tableGroup, DomainResultCreationState creationState); - /** - * Apply SQL selections for a specific reference to this ModelPart outside the domain query's root select clause. - */ + /// Apply SQL selections for a specific reference to this [ModelPart] + /// outside the domain query's root select clause. void applySqlSelections( NavigablePath navigablePath, TableGroup tableGroup, DomainResultCreationState creationState, BiConsumer selectionConsumer); - /** - * A short hand form of {@link #forEachSelectable(int, SelectableConsumer)}, that passes 0 as offset. - */ + /// A short hand form of [#forEachSelectable(int,SelectableConsumer)], that passes `0` as offset. default int forEachSelectable(SelectableConsumer consumer) { return forEachSelectable( 0, consumer ); } - /** - * Visits each selectable mapping with the selectable index offset by the given value. - * Returns the amount of jdbc types that have been visited. - */ + /// Visits each selectable mapping with the selectable index offset by the given value. + /// Returns the amount of jdbc types that have been visited. default int forEachSelectable(int offset, SelectableConsumer consumer) { return 0; } @@ -150,10 +125,8 @@ default EntityMappingType asEntityMappingType(){ return null; } - /** - * A short hand form of {@link #breakDownJdbcValues(Object, int, Object, Object, JdbcValueBiConsumer, SharedSessionContractImplementor)}, - * that passes 0 as offset and null for the two values {@code X} and {@code Y}. - */ + /// A short hand form of [#breakDownJdbcValues(Object,int,Object,Object,JdbcValueBiConsumer,SharedSessionContractImplementor)], + /// that passes `0` as offset and null for the two values `X` and `Y`. default int breakDownJdbcValues( Object domainValue, JdbcValueConsumer valueConsumer, @@ -161,13 +134,11 @@ default int breakDownJdbcValues( return breakDownJdbcValues( domainValue, 0, null, null, valueConsumer, session ); } - /** - * Breaks down the domain value to its constituent JDBC values. - * - * Think of it as breaking the multi-dimensional array into a visitable flat array. - * Additionally, it passes through the values {@code X} and {@code Y} to the consumer. - * Returns the amount of jdbc types that have been visited. - */ + /// Breaks down the domain value to its constituent JDBC values. + /// + /// Think of it as breaking the multi-dimensional array into a visitable flat array. + /// Additionally, it passes through the values `X` and `Y` to the consumer. + /// Returns the amount of jdbc types that have been visited. int breakDownJdbcValues( Object domainValue, int offset, @@ -176,10 +147,8 @@ int breakDownJdbcValues( JdbcValueBiConsumer valueConsumer, SharedSessionContractImplementor session); - /** - * A short hand form of {@link #decompose(Object, int, Object, Object, JdbcValueBiConsumer, SharedSessionContractImplementor)}, - * that passes 0 as offset and null for the two values {@code X} and {@code Y}. - */ + /// A short hand form of [#decompose(Object,int,Object,Object,JdbcValueBiConsumer,SharedSessionContractImplementor)], + /// that passes `0` as offset and null for the two values `X` and `Y`. default int decompose( Object domainValue, JdbcValueConsumer valueConsumer, @@ -187,11 +156,9 @@ default int decompose( return decompose( domainValue, 0, null, null, valueConsumer, session ); } - /** - * Similar to {@link #breakDownJdbcValues(Object, int, Object, Object, JdbcValueBiConsumer, SharedSessionContractImplementor)}, - * but this method is supposed to be used for decomposing values for assignment expressions. - * Returns the amount of jdbc types that have been visited. - */ + /// Similar to [#breakDownJdbcValues(Object,int,Object,Object,JdbcValueBiConsumer,SharedSessionContractImplementor)], + /// but this method is supposed to be used for decomposing values for assignment expressions. + /// Returns the amount of jdbc types that have been visited. default int decompose( Object domainValue, int offset, @@ -209,9 +176,7 @@ default boolean areEqual(@Nullable Object one, @Nullable Object other, SharedSes return Objects.deepEquals( one, other ); } - /** - * Functional interface for consuming the JDBC values. - */ + /// Functional interface for consuming the JDBC values. @FunctionalInterface interface JdbcValueConsumer extends JdbcValueBiConsumer { @Override @@ -219,20 +184,14 @@ default void consume(int valueIndex, Object x, Object y, Object value, Selectabl consume( valueIndex, value, jdbcValueMapping ); } - /** - * Consume a JDBC-level jdbcValue. The JDBC jdbcMapping descriptor is also passed in - */ + /// Consume a JDBC-level jdbcValue. The JDBC jdbcMapping descriptor is also passed in void consume(int valueIndex, Object value, SelectableMapping jdbcValueMapping); } - /** - * Functional interface for consuming the JDBC values, along with two values of type {@code X} and {@code Y}. - */ + /// Functional interface for consuming the JDBC values, along with two values of type `X` and `Y`. @FunctionalInterface interface JdbcValueBiConsumer { - /** - * Consume a JDBC-level jdbcValue. The JDBC jdbcMapping descriptor is also passed in - */ + /// Consume a JDBC-level jdbcValue. The JDBC jdbcMapping descriptor is also passed in void consume(int valueIndex, X x, Y y, Object value, SelectableMapping jdbcValueMapping); } } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/NaturalIdMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/NaturalIdMapping.java index 8ecf1ef79562..e066da830037 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/NaturalIdMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/NaturalIdMapping.java @@ -6,52 +6,49 @@ import java.util.List; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.Incubating; import org.hibernate.cache.spi.access.NaturalIdDataAccess; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.loader.ast.spi.MultiNaturalIdLoader; import org.hibernate.loader.ast.spi.NaturalIdLoader; -/** - * Mapping for an entity's natural-id, if one is defined. - *

        - * Natural identifiers are an alternative form of uniquely - * identifying a specific row. In this sense, they are similar - * to a primary key. In fact most natural identifiers will also - * be classified as "candidate keys" (as in a column or group of - * columns that are considered candidates for primary-key). - *

        - * However, a natural id has fewer restrictions than a primary - * key. While these lessened restrictions make them inappropriate - * for use as a primary key, they are still generally usable as - * unique locators with caveats. General reasons a natural id - * might be inappropriate for use as a primary key are

          - *
        • it contains nullable values
        • - *
        • it contains modifiable values
        • - *
        - *

        - * See other sources for a more complete discussion of data modeling. - * - * @see org.hibernate.Session#byNaturalId - * @see org.hibernate.Session#bySimpleNaturalId - * @see org.hibernate.Session#byMultipleNaturalId - * - * @author Steve Ebersole - */ +/// Mapping for an entity's natural-id, if one is defined. +/// +/// Natural identifiers are an alternative form of uniquely +/// identifying a specific row. In this sense, they are similar +/// to a primary key. In fact most natural identifiers will also +/// be classified as "candidate keys" (as in a column or group of +/// columns that are considered candidates for primary-key). +/// +/// However, a natural id has fewer restrictions than a primary +/// key. While these lessened restrictions make them inappropriate +/// for use as a primary key, they are still generally usable as +/// unique locators with caveats. General reasons a natural id +/// might be inappropriate for use as a primary key are +/// - it contains nullable values +/// - it contains modifiable values +/// +/// @see org.hibernate.annotations.NaturalId +/// @see org.hibernate.annotations.NaturalIdCache +/// @see org.hibernate.annotations.NaturalIdClass +/// @see org.hibernate.KeyType#NATURAL +/// +/// @author Steve Ebersole @Incubating public interface NaturalIdMapping extends VirtualModelPart { String PART_NAME = "{natural-id}"; - /** - * The attribute(s) making up the natural-id. - */ + /// A [class][org.hibernate.annotations.NaturalIdClass] which used + /// as a wrapper for natural-id values. + @Nullable Class getNaturalIdClass(); + + /// The attribute(s) making up the natural-id. List getNaturalIdAttributes(); - /** - * Whether the natural-id is mutable. - * - * @apiNote For compound natural-ids, this is true if any of the attributes are mutable. - */ + /// Whether the natural-id is mutable. + /// + /// @apiNote For compound natural-ids, this is true if any of the attributes are mutable. boolean isMutable(); @Override @@ -59,67 +56,54 @@ default String getPartName() { return PART_NAME; } - /** - * Access to the natural-id's L2 cache access. Returns null if the natural-id is not - * configured for caching - */ + /// Access to the natural-id's L2 cache access. + /// Returns null if the natural-id is not configured for caching. NaturalIdDataAccess getCacheAccess(); - /** - * Verify the natural-id value(s) we are about to flush to the database - */ + /// Verify the natural-id value(s) we are about to flush to the database void verifyFlushState( Object id, Object[] currentState, Object[] loadedState, SharedSessionContractImplementor session); - /** - * Given an array of "full entity state", extract the normalized natural id representation - * - * @param state The attribute state array - * - * @return The extracted natural id values. This is a normalized - */ + /// Given an array of "full entity state", extract the normalized natural id representation. + /// + /// @param state The attribute state array + /// + /// @return The extracted natural id values. Object extractNaturalIdFromEntityState(Object[] state); - /** - * Given an entity instance, extract the normalized natural id representation - * - * @param entity The entity instance - * - * @return The extracted natural id values - */ + /// Given an entity instance, extract the normalized natural-id representation. + /// + /// @param entity The entity instance + /// + /// @return The extracted natural-id values. Object extractNaturalIdFromEntity(Object entity); - /** - * Normalize a user-provided natural-id value into the representation Hibernate uses internally - * - * @param incoming The user-supplied value - * @return The normalized, internal representation - */ + /// Normalize a user-provided natural-id value into the representation Hibernate uses internally. + /// + /// @param incoming The user-supplied value + /// @return The normalized, internal representation Object normalizeInput(Object incoming); - /** - * Validates a natural id value(s) for the described natural-id based on the expected internal representation - */ + /// Whether the incoming value is in normalized internal form. + /// + /// @see #normalizeInput + boolean isNormalized(Object incoming); + + /// Validates a natural id value(s) for the described natural-id based on the expected internal representation void validateInternalForm(Object naturalIdValue); - /** - * Calculate the hash-code of a natural-id value - * - * @param value The natural-id value - * @return The hash-code - */ + /// Calculate the hash-code of a natural-id value + /// + /// @param value The natural-id value + /// @return The hash-code int calculateHashCode(Object value); - /** - * Make a loader capable of loading a single entity by natural-id - */ + /// Make a loader capable of loading a single entity by natural-id NaturalIdLoader makeLoader(EntityMappingType entityDescriptor); - /** - * Make a loader capable of loading multiple entities by natural-id - */ + /// Make a loader capable of loading multiple entities by natural-id MultiNaturalIdLoader makeMultiLoader(EntityMappingType entityDescriptor); } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/PluralAttributeMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/PluralAttributeMapping.java index 3220cfcfe067..14f33467d6b8 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/PluralAttributeMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/PluralAttributeMapping.java @@ -9,6 +9,8 @@ import java.util.function.Consumer; import org.hibernate.Filter; +import org.hibernate.Incubating; +import org.hibernate.engine.spi.LoadQueryInfluencers; import org.hibernate.internal.util.IndexedConsumer; import org.hibernate.loader.ast.spi.Loadable; import org.hibernate.metamodel.mapping.internal.ToOneAttributeMapping; @@ -16,6 +18,7 @@ import org.hibernate.persister.collection.CollectionPersister; import org.hibernate.spi.NavigablePath; import org.hibernate.sql.ast.spi.SqlAstCreationState; +import org.hibernate.sql.ast.spi.SqlAliasBaseGenerator; import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.ast.tree.from.TableGroupJoinProducer; import org.hibernate.sql.ast.tree.predicate.Predicate; @@ -47,7 +50,14 @@ interface PredicateConsumer { void applyPredicate(Predicate predicate); } - void applySoftDeleteRestrictions(TableGroup tableGroup, PredicateConsumer predicateConsumer); + /** + * Apply auxiliary restrictions (soft delete, temporal, audit) in a single pass. + */ + void applyAuxiliaryRestrictions( + TableGroup tableGroup, + PredicateConsumer predicateConsumer, + LoadQueryInfluencers influencers, + SqlAliasBaseGenerator sqlAliasBaseGenerator); interface IndexMetadata { CollectionPart getIndexDescriptor(); @@ -64,10 +74,27 @@ interface IndexMetadata { /** * Mapping for soft-delete support, or {@code null} if soft-delete not defined */ + @Incubating default SoftDeleteMapping getSoftDeleteMapping() { return null; } + /** + * Mapping for temporal support, or {@code null} if temporal not defined + */ + @Incubating + default TemporalMapping getTemporalMapping() { + return null; + } + + /** + * Mapping for audit support, or {@code null} if audit not defined + */ + @Incubating + default AuditMapping getAuditMapping() { + return null; + } + OrderByFragment getOrderByFragment(); OrderByFragment getManyToManyOrderByFragment(); diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/SoftDeleteMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/SoftDeleteMapping.java index 4f1b02afe8dc..40c4d4118cf4 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/SoftDeleteMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/SoftDeleteMapping.java @@ -5,6 +5,7 @@ package org.hibernate.metamodel.mapping; import org.checkerframework.checker.nullness.qual.Nullable; +import org.hibernate.Internal; import org.hibernate.annotations.SoftDeleteType; import org.hibernate.sql.ast.spi.SqlExpressionResolver; import org.hibernate.sql.ast.tree.expression.ColumnReference; @@ -14,7 +15,6 @@ import org.hibernate.sql.model.ast.ColumnValueBinding; /** - * * Metadata about the indicator column for entities and collections enabled * for soft delete * @@ -22,17 +22,19 @@ * * @author Steve Ebersole */ -public interface SoftDeleteMapping extends SelectableMapping, VirtualModelPart, SqlExpressible { +public interface SoftDeleteMapping extends AuxiliaryMapping, SelectableMapping, VirtualModelPart, SqlExpressible { String ROLE_NAME = "{soft-delete}"; /** * The soft-delete strategy - how to interpret indicator values */ + @Internal // only used in tests! SoftDeleteType getSoftDeleteStrategy(); /** * The name of the soft-delete indicator column. */ + @Internal // only used in tests! String getColumnName(); /** diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/TableDetails.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/TableDetails.java index d7509fc159ec..6ac3f0c06cc5 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/TableDetails.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/TableDetails.java @@ -31,7 +31,7 @@ public interface TableDetails { /** * Whether this table is the root for a given {@link ModelPartContainer}. - *

        + *

        * Only relevant for entity-mappings where this indicates whether this * table holds the entity's identifier. */ diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/TemporalMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/TemporalMapping.java new file mode 100644 index 000000000000..c9f1941f0775 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/TemporalMapping.java @@ -0,0 +1,33 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.metamodel.mapping; + + +import org.hibernate.Incubating; +import org.hibernate.sql.ast.tree.expression.ColumnReference; +import org.hibernate.sql.model.ast.ColumnValueBinding; + +/** + * Metadata about temporal columns for entities enabled for temporal history. + * + * @see org.hibernate.annotations.Temporal + * + * @author Gavin King + * + * @since 7.4 + */ +@Incubating +public interface TemporalMapping extends AuxiliaryMapping { + + SelectableMapping getStartingColumnMapping(); + + SelectableMapping getEndingColumnMapping(); + + ColumnValueBinding createStartingValueBinding(ColumnReference startingColumnReference); + + ColumnValueBinding createEndingValueBinding(ColumnReference endingColumnReference); + + ColumnValueBinding createNullEndingValueBinding(ColumnReference endingColumnReference); +} diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/ValueMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/ValueMapping.java index d2069e238121..e2f730dd19f3 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/ValueMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/ValueMapping.java @@ -40,8 +40,7 @@ default JavaType getExpressibleJavaType() { */ default X treatAs(Class targetType) { if ( targetType.isInstance( this ) ) { - //noinspection unchecked - return (X) this; + return targetType.cast( this ); } throw new IllegalArgumentException( diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/ValuedModelPart.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/ValuedModelPart.java index a99386a77b73..daed3951c928 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/ValuedModelPart.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/ValuedModelPart.java @@ -6,7 +6,7 @@ /** * Describes a ModelPart that is also a ValueMapping (and therefore also a SelectableMappings). - *

        + *

        * {@linkplain BasicValuedModelPart Basic} and {@linkplain EmbeddableValuedModelPart embedded} * model-parts fall into this category. * @@ -41,11 +41,9 @@ default int forEachSelectable(SelectableConsumer consumer) { default void forEachInsertable(SelectableConsumer consumer) { ModelPart.super.forEachSelectable( (selectionIndex, selectableMapping) -> { - if ( ! selectableMapping.isInsertable() || selectableMapping.isFormula() ) { - return; + if ( selectableMapping.isInsertable() && !selectableMapping.isFormula() ) { + consumer.accept( selectionIndex, selectableMapping ); } - - consumer.accept( selectionIndex, selectableMapping ); } ); } @@ -53,11 +51,9 @@ default void forEachInsertable(SelectableConsumer consumer) { default void forEachNonFormula(SelectableConsumer consumer) { ModelPart.super.forEachSelectable( (selectionIndex, selectableMapping) -> { - if ( selectableMapping.isFormula() ) { - return; + if ( !selectableMapping.isFormula() ) { + consumer.accept( selectionIndex, selectableMapping ); } - - consumer.accept( selectionIndex, selectableMapping ); } ); } @@ -65,11 +61,9 @@ default void forEachNonFormula(SelectableConsumer consumer) { default void forEachUpdatable(SelectableConsumer consumer) { ModelPart.super.forEachSelectable( (selectionIndex, selectableMapping) -> { - if ( ! selectableMapping.isUpdateable() || selectableMapping.isFormula() ) { - return; + if ( selectableMapping.isUpdateable() && !selectableMapping.isFormula() ) { + consumer.accept( selectionIndex, selectableMapping ); } - - consumer.accept( selectionIndex, selectableMapping ); } ); } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AbstractDiscriminatorMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AbstractDiscriminatorMapping.java index 451b1f45099f..ee3b840217c9 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AbstractDiscriminatorMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AbstractDiscriminatorMapping.java @@ -168,7 +168,6 @@ public BasicFetch generateFetch( discriminatorType.getValueConverter(), fetchTiming, true, - creationState, false, !sqlSelection.isVirtual() ); diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AbstractDomainPath.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AbstractDomainPath.java index be1ce030c509..0a6cc88cdfd4 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AbstractDomainPath.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AbstractDomainPath.java @@ -14,7 +14,6 @@ import org.hibernate.metamodel.mapping.ModelPart; import org.hibernate.metamodel.mapping.SelectableMapping; import org.hibernate.metamodel.mapping.ordering.ast.DomainPath; -import org.hibernate.metamodel.mapping.ordering.ast.OrderingExpression; import org.hibernate.query.SortDirection; import org.hibernate.sql.ast.spi.SqlAstCreationState; import org.hibernate.sql.ast.tree.SqlAstNode; @@ -28,6 +27,7 @@ import org.hibernate.sql.results.internal.SqlSelectionImpl; import static org.hibernate.internal.util.NullnessUtil.castNonNull; +import static org.hibernate.metamodel.mapping.ordering.ast.OrderingExpression.applyCollation; import static org.hibernate.sql.ast.spi.SqlExpressionResolver.createColumnReferenceKey; /** @@ -209,9 +209,11 @@ private void addSortSpecification( ); } else { - final var subPart = embeddableValuedModelPart.findSubPart( modelPartName, null ); + final var subPart = + embeddableValuedModelPart.findSubPart( modelPartName, null ) + .asBasicValuedModelPart(); addSortSpecification( - castNonNull( subPart.asBasicValuedModelPart() ), + castNonNull( subPart ), ast, tableGroup, collation, @@ -260,8 +262,7 @@ && selectClauseDoesNotContainOrderExpression( expression, selectClause ) ) { selectClause.addSqlSelection( new SqlSelectionImpl( valuesArrayPosition, expression ) ); } - final var sortExpression = - OrderingExpression.applyCollation( expression, collation, creationState ); + final var sortExpression = applyCollation( expression, collation, creationState ); ast.addSortSpecification( new SortSpecification( sortExpression, sortOrder, nullPrecedence ) ); } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AbstractEmbeddableMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AbstractEmbeddableMapping.java index 79a61fe5037d..50152c6c16b2 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AbstractEmbeddableMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AbstractEmbeddableMapping.java @@ -58,6 +58,9 @@ import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.descriptor.java.MutabilityPlan; +import static org.hibernate.metamodel.mapping.internal.MappingModelCreationHelper.buildBasicAttributeMapping; +import static org.hibernate.metamodel.mapping.internal.MappingModelCreationHelper.getPropertyPath; + /** * Base support for EmbeddableMappingType implementations */ @@ -351,7 +354,7 @@ protected boolean finishInitialization( selectablePath = new SelectablePath( determineEmbeddablePrefix() + bootPropertyDescriptor.getName() ); } - attributeMapping = MappingModelCreationHelper.buildBasicAttributeMapping( + attributeMapping = buildBasicAttributeMapping( bootPropertyDescriptor.getName(), role, attributeIndex, @@ -502,11 +505,7 @@ else if ( subtype instanceof EntityType ) { } protected String determineEmbeddablePrefix() { - var root = getNavigableRole().getParent(); - while ( !root.isRoot() ) { - root = root.getParent(); - } - return getNavigableRole().getFullPath().substring( root.getFullPath().length() + 1 ) + "."; + return getPropertyPath( getNavigableRole() ); } @Override 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 e7418e75dd38..954e4f4a4ce0 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 @@ -24,7 +24,6 @@ import org.hibernate.metamodel.model.domain.NavigableRole; import org.hibernate.persister.collection.CollectionPersister; import org.hibernate.spi.NavigablePath; -import org.hibernate.sql.ast.spi.FromClauseAccess; import org.hibernate.sql.ast.spi.SqlAliasBase; import org.hibernate.sql.ast.spi.SqlAstCreationState; import org.hibernate.sql.ast.tree.from.PluralTableGroup; @@ -42,6 +41,8 @@ import org.hibernate.type.Type; import static org.hibernate.internal.util.StringHelper.isEmpty; +import static org.hibernate.metamodel.mapping.internal.ToOneAttributeMapping.addPrefixedPropertyNames; +import static org.hibernate.metamodel.mapping.internal.ToOneAttributeMapping.addPrefixedPropertyPaths; import static org.hibernate.metamodel.mapping.internal.ToOneAttributeMapping.findMapsIdPropertyName; /** @@ -188,18 +189,15 @@ public DomainResult createDomainResult( TableGroup tableGroup, String resultVariable, DomainResultCreationState creationState) { - final TableGroup partTableGroup = resolveTableGroup( navigablePath, creationState ); + final var partTableGroup = resolveTableGroup( navigablePath, creationState ); return associatedEntityTypeDescriptor.createDomainResult( navigablePath, partTableGroup, resultVariable, creationState ); } @Override public Object disassemble(Object value, SharedSessionContractImplementor session) { - if ( value == null ) { - return null; - } - - // should be an instance of the associated entity - return getAssociatedEntityMappingType().getIdentifierMapping().getIdentifier( value ); + return value == null ? null + // should be an instance of the associated entity + : getAssociatedEntityMappingType().getIdentifierMapping().getIdentifier( value ); } @@ -214,11 +212,10 @@ public EntityFetch generateFetch( final var associationKey = resolveFetchAssociationKey(); final boolean added = creationState.registerVisitedAssociationKey( associationKey ); - final var partTableGroup = resolveTableGroup( fetchablePath, creationState ); final var fetch = buildEntityFetchJoined( fetchParent, this, - partTableGroup, + resolveTableGroup( fetchablePath, creationState ), fetchablePath, creationState ); @@ -271,25 +268,18 @@ protected EntityFetch buildEntityFetchJoined( private TableGroup resolveTableGroup(NavigablePath fetchablePath, DomainResultCreationState creationState) { final var fromClauseAccess = creationState.getSqlAstCreationState().getFromClauseAccess(); - return fromClauseAccess.resolveTableGroup( fetchablePath, (np) -> { - final var parentTableGroup = (PluralTableGroup) fromClauseAccess.getTableGroup( np.getParent() ); + return fromClauseAccess.resolveTableGroup( fetchablePath, navigablePath -> { + final var parentTableGroup = + (PluralTableGroup) + fromClauseAccess.getTableGroup( navigablePath.getParent() ); return switch ( nature ) { case ELEMENT -> parentTableGroup.getElementTableGroup(); - case INDEX -> resolveIndexTableGroup( parentTableGroup, fetchablePath, fromClauseAccess, creationState ); - default -> throw new IllegalStateException( "Could not find table group for: " + np ); + case INDEX -> parentTableGroup.getIndexTableGroup(); + default -> throw new IllegalStateException( "Could not find table group for: " + navigablePath ); }; - } ); } - private TableGroup resolveIndexTableGroup( - PluralTableGroup collectionTableGroup, - NavigablePath fetchablePath, - FromClauseAccess fromClauseAccess, - DomainResultCreationState creationState) { - return collectionTableGroup.getIndexTableGroup(); - } - // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // TableGroupProducer @@ -316,7 +306,7 @@ public TableGroup createTableGroupInternal( getEntityMappingType() .createPrimaryTableReference( sqlAliasBase, creationState ); - final TableGroup tableGroup = new StandardTableGroup( + final var tableGroup = new StandardTableGroup( canUseInnerJoins, navigablePath, this, @@ -383,13 +373,13 @@ else if ( bootModelValue instanceof ToOne toOne ) { if ( propertyType instanceof ComponentType compositeType && compositeType.isEmbedded() && compositeType.getPropertyNames().length == 1 ) { - ToOneAttributeMapping.addPrefixedPropertyPaths( + addPrefixedPropertyPaths( targetKeyPropertyNames, compositeType.getPropertyNames()[0], compositeType.getSubtypes()[0], creationProcess.getCreationContext().getSessionFactory() ); - ToOneAttributeMapping.addPrefixedPropertyNames( + addPrefixedPropertyNames( targetKeyPropertyNames, EntityIdentifierMapping.ID_ROLE_NAME, propertyType, @@ -397,7 +387,7 @@ else if ( bootModelValue instanceof ToOne toOne ) { ); } else { - ToOneAttributeMapping.addPrefixedPropertyPaths( + addPrefixedPropertyPaths( targetKeyPropertyNames, null, propertyType, @@ -406,7 +396,7 @@ else if ( bootModelValue instanceof ToOne toOne ) { } } else { - ToOneAttributeMapping.addPrefixedPropertyPaths( + addPrefixedPropertyPaths( targetKeyPropertyNames, entityBinding.getIdentifierProperty().getName(), propertyType, @@ -422,7 +412,7 @@ else if ( bootModelValue instanceof OneToMany ) { targetKeyPropertyNames.add( referencedPropertyName.substring( 0, dotIndex ) ); } // todo (PropertyMapping) : the problem here is timing. this needs to be delayed. - ToOneAttributeMapping.addPrefixedPropertyPaths( + addPrefixedPropertyPaths( targetKeyPropertyNames, referencedPropertyName, elementTypeDescriptor.getEntityPersister().getPropertyType( referencedPropertyName ), @@ -436,13 +426,13 @@ else if ( bootModelValue instanceof OneToMany ) { && compositeType.isEmbedded() && compositeType.getPropertyNames().length == 1 ) { final Set targetKeyPropertyNames = new HashSet<>( 2 ); - ToOneAttributeMapping.addPrefixedPropertyPaths( + addPrefixedPropertyPaths( targetKeyPropertyNames, compositeType.getPropertyNames()[0], compositeType.getSubtypes()[0], creationProcess.getCreationContext().getSessionFactory() ); - ToOneAttributeMapping.addPrefixedPropertyNames( + addPrefixedPropertyNames( targetKeyPropertyNames, EntityIdentifierMapping.ID_ROLE_NAME, propertyType, @@ -457,7 +447,7 @@ else if ( bootModelValue instanceof OneToMany ) { final String mapsIdAttributeName = findMapsIdPropertyName( elementTypeDescriptor, referencedPropertyName ); if ( mapsIdAttributeName != null ) { - ToOneAttributeMapping.addPrefixedPropertyPaths( + addPrefixedPropertyPaths( targetKeyPropertyNames, mapsIdAttributeName, elementTypeDescriptor.getEntityPersister().getIdentifierType(), @@ -465,7 +455,7 @@ else if ( bootModelValue instanceof OneToMany ) { ); } else { - ToOneAttributeMapping.addPrefixedPropertyPaths( + addPrefixedPropertyPaths( targetKeyPropertyNames, null, propertyType, diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AbstractNaturalIdMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AbstractNaturalIdMapping.java index 52254432816e..307519db477c 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AbstractNaturalIdMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AbstractNaturalIdMapping.java @@ -19,12 +19,12 @@ public abstract class AbstractNaturalIdMapping implements NaturalIdMapping { private final NavigableRole role; - public AbstractNaturalIdMapping(EntityMappingType declaringType, boolean mutable) { + public AbstractNaturalIdMapping( + EntityMappingType declaringType, + boolean mutable) { this.declaringType = declaringType; this.mutable = mutable; - this.cachesAccess = declaringType.getEntityPersister().getNaturalIdCacheAccessStrategy(); - this.role = declaringType.getNavigableRole().append( PART_NAME ); } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AnyDiscriminatorPart.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AnyDiscriminatorPart.java index 15199d208562..a06775efee11 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AnyDiscriminatorPart.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AnyDiscriminatorPart.java @@ -13,6 +13,7 @@ import org.hibernate.metamodel.mapping.DiscriminatedAssociationModelPart; import org.hibernate.metamodel.mapping.DiscriminatorConverter; import org.hibernate.metamodel.mapping.DiscriminatorMapping; +import org.hibernate.metamodel.mapping.DiscriminatorValue; import org.hibernate.metamodel.mapping.EntityDiscriminatorMapping; import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.JdbcMapping; @@ -86,7 +87,7 @@ public AnyDiscriminatorPart( boolean updateable, boolean partitioned, BasicType underlyingJdbcMapping, - Map valueToEntityNameMap, + Map valueToEntityNameMap, ImplicitDiscriminatorStrategy implicitValueStrategy, MappingMetamodelImplementor mappingMetamodel) { this( @@ -129,7 +130,7 @@ public AnyDiscriminatorPart( boolean updateable, boolean partitioned, BasicType underlyingJdbcMapping, - Map valueToEntityNameMap, + Map valueToEntityNameMap, ImplicitDiscriminatorStrategy implicitValueStrategy, MappingMetamodelImplementor mappingMetamodel) { this.navigableRole = partRole; @@ -161,7 +162,7 @@ public AnyDiscriminatorPart( public static DiscriminatorConverter determineDiscriminatorConverter( NavigableRole partRole, BasicType underlyingJdbcMapping, - Map valueToEntityNameMap, + Map valueToEntityNameMap, ImplicitDiscriminatorStrategy implicitValueStrategy, MappingMetamodelImplementor mappingMetamodel) { return new UnifiedAnyDiscriminatorConverter<>( @@ -399,7 +400,6 @@ public BasicFetch generateFetch( fetchablePath, this, fetchTiming, - creationState, !sqlSelection.isVirtual() ); } @@ -437,11 +437,9 @@ public Expression resolveSqlExpression( JdbcMapping jdbcMappingToUse, TableGroup tableGroup, SqlAstCreationState creationState) { - var tableReference = tableGroup.resolveTableReference( - navigablePath, - this, - getContainingTableExpression() - ); + final var tableReference = + tableGroup.resolveTableReference( navigablePath, this, + getContainingTableExpression() ); return creationState.getSqlExpressionResolver() .resolveSqlExpression( tableReference, this ); } @@ -472,7 +470,7 @@ private SqlSelection resolveSqlSelection( resolveSqlExpression( navigablePath, null, tableGroup, sqlAstCreationState ), jdbcMapping().getJdbcJavaType(), null, - creationState.getSqlAstCreationState().getCreationContext().getTypeConfiguration() + sqlAstCreationState.getCreationContext().getTypeConfiguration() ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AnyKeyPart.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AnyKeyPart.java index c5b473ef1665..521db9b6737e 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AnyKeyPart.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AnyKeyPart.java @@ -270,23 +270,20 @@ public Fetch generateFetch( String resultVariable, DomainResultCreationState creationState) { final var sqlAstCreationState = creationState.getSqlAstCreationState(); - final var fromClauseAccess = sqlAstCreationState.getFromClauseAccess(); final var sqlExpressionResolver = sqlAstCreationState.getSqlExpressionResolver(); - final var tableGroup = fromClauseAccess.getTableGroup( fetchParent.getNavigablePath().getParent() ); - final var tableReference = tableGroup.resolveTableReference( fetchablePath, table ); + final var tableReference = + sqlAstCreationState.getFromClauseAccess() + .getTableGroup( fetchParent.getNavigablePath().getParent() ) + .resolveTableReference( fetchablePath, table ); - final var columnReference = sqlExpressionResolver.resolveSqlExpression( - tableReference, - this - ); - - final var sqlSelection = sqlExpressionResolver.resolveSqlSelection( - columnReference, - jdbcMapping.getJdbcJavaType(), - fetchParent, - sqlAstCreationState.getCreationContext().getTypeConfiguration() - ); + final var sqlSelection = + sqlExpressionResolver.resolveSqlSelection( + sqlExpressionResolver.resolveSqlExpression( tableReference, this ), + jdbcMapping.getJdbcJavaType(), + fetchParent, + sqlAstCreationState.getCreationContext().getTypeConfiguration() + ); return new BasicFetch<>( sqlSelection.getValuesArrayPosition(), @@ -294,7 +291,6 @@ public Fetch generateFetch( fetchablePath, this, fetchTiming, - creationState, !sqlSelection.isVirtual() ); } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AuditMappingImpl.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AuditMappingImpl.java new file mode 100644 index 000000000000..96b74e43041c --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AuditMappingImpl.java @@ -0,0 +1,416 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.metamodel.mapping.internal; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import org.hibernate.engine.spi.LoadQueryInfluencers; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.mapping.Stateful; +import org.hibernate.metamodel.mapping.AuditMapping; +import org.hibernate.metamodel.mapping.EntityMappingType; +import org.hibernate.metamodel.mapping.JdbcMapping; +import org.hibernate.metamodel.mapping.PluralAttributeMapping; +import org.hibernate.metamodel.mapping.SelectableMapping; +import org.hibernate.persister.state.internal.AuditStateManagement; +import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor; +import org.hibernate.query.sqm.function.FunctionRenderer; +import org.hibernate.query.sqm.function.SelfRenderingAggregateFunctionSqlAstExpression; +import org.hibernate.spi.NavigablePath; +import org.hibernate.sql.ast.spi.SqlAliasBaseGenerator; +import org.hibernate.sql.ast.spi.SqlAstCreationState; +import org.hibernate.sql.ast.tree.expression.AggregateFunctionExpression; +import org.hibernate.sql.ast.tree.expression.ColumnReference; +import org.hibernate.sql.ast.tree.expression.JdbcLiteral; +import org.hibernate.sql.ast.tree.expression.SelfRenderingSqlFragmentExpression; +import org.hibernate.sql.ast.tree.from.LazyTableGroup; +import org.hibernate.sql.ast.tree.from.NamedTableReference; +import org.hibernate.sql.ast.tree.from.StandardTableGroup; +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.ComparisonPredicate; +import org.hibernate.sql.ast.tree.predicate.Junction; +import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.sql.ast.tree.select.QuerySpec; +import org.hibernate.sql.ast.tree.select.SelectStatement; +import org.hibernate.sql.exec.internal.TemporalJdbcParameter; +import org.hibernate.sql.results.internal.SqlSelectionImpl; +import org.hibernate.type.BasicType; +import org.hibernate.type.spi.TypeConfiguration; + +import static java.util.Collections.singletonList; +import static org.hibernate.boot.model.internal.AuditHelper.MODIFICATION_TYPE; +import static org.hibernate.boot.model.internal.AuditHelper.TRANSACTION_ID; +import static org.hibernate.query.sqm.ComparisonOperator.EQUAL; +import static org.hibernate.query.sqm.ComparisonOperator.LESS_THAN_OR_EQUAL; +import static org.hibernate.query.sqm.ComparisonOperator.NOT_EQUAL; + +/** + * Audit mapping implementation. + * + * @author Gavin King + * + * @since 7.4 + */ +public class AuditMappingImpl implements AuditMapping { + private static final String SUBQUERY_ALIAS_STEM = "audit"; + public static final String MAX = "max"; + + private final String tableName; + private final SelectableMapping transactionIdMapping; + private final SelectableMapping modificationTypeMapping; + private final JdbcMapping jdbcMapping; + private final BasicType transactionIdBasicType; + private final String currentTimestampFunctionName; + private final FunctionRenderer maxFunctionDescriptor; + + public AuditMappingImpl( + Stateful auditable, + String tableName, + MappingModelCreationProcess creationProcess) { + this.tableName = tableName; + + final var transactionIdColumnName = auditable.getAuxiliaryColumn( TRANSACTION_ID ); + final var modificationTypeColumnName = auditable.getAuxiliaryColumn( MODIFICATION_TYPE ); + + final var creationContext = creationProcess.getCreationContext(); + final var typeConfiguration = creationContext.getTypeConfiguration(); + final var dialect = creationContext.getDialect(); + final var sessionFactory = creationContext.getSessionFactory(); + final var transactionIdJavaType = sessionFactory.getTransactionIdentifierService().getIdentifierType(); + final var sqmFunctionRegistry = sessionFactory.getQueryEngine().getSqmFunctionRegistry(); + + jdbcMapping = resolveJdbcMapping( typeConfiguration, transactionIdJavaType ); + transactionIdBasicType = resolveBasicType( typeConfiguration, transactionIdJavaType ); + + transactionIdMapping = SelectableMappingImpl.from( + tableName, + transactionIdColumnName, + null, + null, + jdbcMapping, + typeConfiguration, + true, + false, + false, + false, + dialect, + sqmFunctionRegistry, + creationContext + ); + + modificationTypeMapping = SelectableMappingImpl.from( + tableName, + modificationTypeColumnName, + null, + null, + jdbcMapping, + typeConfiguration, + true, + false, + false, + false, + dialect, + sqmFunctionRegistry, + creationContext + ); + + currentTimestampFunctionName = + sessionFactory.getTransactionIdentifierService().useServerTimestamp( dialect ) + ? dialect.currentTimestamp() + : null; + + maxFunctionDescriptor = resolveMaxFunction( sessionFactory ); + } + + @Override + public String getTableName() { + return tableName; + } + + @Override + public SelectableMapping getTransactionIdMapping() { + return transactionIdMapping; + } + + @Override + public SelectableMapping getModificationTypeMapping() { + return modificationTypeMapping; + } + + @Override + public JdbcMapping getJdbcMapping() { + return jdbcMapping; + } + + private Predicate createRestriction( + TableGroupProducer tableGroupProducer, + TableReference tableReference, + List keySelectables, + SqlAliasBaseGenerator sqlAliasBaseGenerator) { + final var subQueryExpression = + new SelectStatement( buildSubquery( tableGroupProducer, tableReference, keySelectables, sqlAliasBaseGenerator ) ); + final var revisionPredicate = + new ComparisonPredicate( + new ColumnReference( tableReference, transactionIdMapping ), + EQUAL, + subQueryExpression + ); + final var modificationTypePredicate = + new ComparisonPredicate( + new ColumnReference( tableReference, modificationTypeMapping ), + NOT_EQUAL, + new JdbcLiteral<>( + AuditStateManagement.ModificationType.DEL.ordinal(), + modificationTypeMapping.getJdbcMapping() + ) + ); + + final var auditPredicate = new Junction( Junction.Nature.CONJUNCTION ); + auditPredicate.add( revisionPredicate ); + auditPredicate.add( modificationTypePredicate ); + return auditPredicate; + } + + private QuerySpec buildSubquery( + TableGroupProducer tableGroupProducer, + TableReference tableReference, + List keySelectables, + SqlAliasBaseGenerator sqlAliasBaseGenerator) { + final var subQuerySpec = new QuerySpec( false, 1 ); + final String stem = tableGroupProducer.getSqlAliasStem(); + final String aliasStem = stem == null ? SUBQUERY_ALIAS_STEM : stem; + final var subTableReference = + new NamedTableReference( tableName, + sqlAliasBaseGenerator.createSqlAliasBase( aliasStem ) + .generateNewAlias() ); + final var subTableGroup = new StandardTableGroup( + true, + new NavigablePath( stem == null ? "audit-subquery" : stem + "#audit" ), + tableGroupProducer, + subTableReference.getIdentificationVariable(), + subTableReference, + null, + null + ); + subQuerySpec.getFromClause().addRoot( subTableGroup ); + + final var transactionId = + new ColumnReference( subTableReference, transactionIdMapping ); + subQuerySpec.getSelectClause() + .addSqlSelection( new SqlSelectionImpl( buildMaxExpression( transactionId ) ) ); + + subQuerySpec.applyPredicate( + buildSubqueryPredicate( tableReference, keySelectables, subTableReference, transactionId ) ); + + return subQuerySpec; + } + + private Junction buildSubqueryPredicate( + TableReference tableReference, + List keySelectables, + NamedTableReference subTableReference, + ColumnReference transactionId) { + final var predicate = new Junction( Junction.Nature.CONJUNCTION ); + for ( var selectableMapping : keySelectables ) { + predicate.add( new ComparisonPredicate( + new ColumnReference( subTableReference, selectableMapping ), + EQUAL, + new ColumnReference( tableReference, selectableMapping ) + ) ); + } + predicate.add( new ComparisonPredicate( + transactionId, + LESS_THAN_OR_EQUAL, + currentTimestampFunctionName != null + ? new SelfRenderingSqlFragmentExpression( currentTimestampFunctionName, jdbcMapping ) + : new TemporalJdbcParameter( transactionIdMapping ) + ) ); + return predicate; + } + + private AggregateFunctionExpression buildMaxExpression(ColumnReference expression) { + return new SelfRenderingAggregateFunctionSqlAstExpression<>( + MAX, + maxFunctionDescriptor, + singletonList( expression ), + null, + transactionIdBasicType, + transactionIdBasicType + ); + } + + private static FunctionRenderer resolveMaxFunction(SessionFactoryImplementor sessionFactory) { + final var functionDescriptor = + sessionFactory.getQueryEngine().getSqmFunctionRegistry() + .findFunctionDescriptor( MAX ); + if ( functionDescriptor instanceof AbstractSqmSelfRenderingFunctionDescriptor selfRendering ) { + return selfRendering; + } + throw new IllegalStateException( "Function 'max' is not a self rendering function" ); + } + + private static JdbcMapping resolveJdbcMapping( + TypeConfiguration typeConfiguration, + Class javaType) { + final var basicType = typeConfiguration.getBasicTypeForJavaType( javaType ); + return basicType != null + ? basicType + : typeConfiguration.standardBasicTypeForJavaType( javaType ); + } + + private static BasicType resolveBasicType( + TypeConfiguration typeConfiguration, + Class javaType) { + final var basicType = typeConfiguration.getBasicTypeForJavaType( javaType ); + return basicType == null + ? typeConfiguration.standardBasicTypeForJavaType( javaType ) + : basicType; + } + + @Override + public void applyPredicate( + EntityMappingType associatedEntityMappingType, + Consumer predicateConsumer, + LazyTableGroup lazyTableGroup, + NavigablePath navigablePath, + SqlAstCreationState creationState) { + if ( creationState.getLoadQueryInfluencers().getTemporalIdentifier() != null ) { + predicateConsumer.accept( createRestriction( + associatedEntityMappingType.getEntityPersister(), + lazyTableGroup.resolveTableReference( navigablePath, getTableName() ), + collectEntityKeySelectables( associatedEntityMappingType ), + creationState.getSqlAliasBaseGenerator() + ) ); + } + } + + @Override + public void applyPredicate( + EntityMappingType associatedEntityDescriptor, + Consumer predicateConsumer, + TableGroup tableGroup, + SqlAliasBaseGenerator sqlAliasBaseGenerator, + LoadQueryInfluencers influencers) { + if ( influencers.getTemporalIdentifier() != null ) { + predicateConsumer.accept( createRestriction( + associatedEntityDescriptor.getEntityPersister(), + tableGroup.resolveTableReference( getTableName() ), + collectEntityKeySelectables( associatedEntityDescriptor ), + sqlAliasBaseGenerator + ) ); + } + } + + @Override + public void applyPredicate( + PluralAttributeMapping collectionDescriptor, + Consumer predicateConsumer, + TableGroup tableGroup, + SqlAliasBaseGenerator sqlAliasBaseGenerator, + LoadQueryInfluencers influencers) { + if ( influencers.getTemporalIdentifier() != null ) { + predicateConsumer.accept( createRestriction( + collectionDescriptor, + tableGroup.resolveTableReference( getTableName() ), + collectCollectionRowKeySelectables( collectionDescriptor ), + sqlAliasBaseGenerator + ) ); + } + } + + @Override + public void applyPredicate(TableGroupJoin tableGroupJoin, LoadQueryInfluencers loadQueryInfluencers) { + //TODO!! + } + + private static List collectEntityKeySelectables(EntityMappingType entityDescriptor) { + final var keySelectables = new ArrayList(); + entityDescriptor.getIdentifierMapping().forEachSelectable( + (selectionIndex, selectableMapping) -> { + if ( !selectableMapping.isFormula() ) { + keySelectables.add( selectableMapping ); + } + } + ); + return keySelectables; + } + + private List collectCollectionRowKeySelectables(PluralAttributeMapping collectionDescriptor) { + final var keySelectables = new ArrayList(); + final var identifierDescriptor = collectionDescriptor.getIdentifierDescriptor(); + if ( identifierDescriptor != null ) { + identifierDescriptor.forEachSelectable( + (selectionIndex, selectableMapping) -> { + if ( !selectableMapping.isFormula() ) { + keySelectables.add( selectableMapping ); + } + } + ); + return keySelectables; + } + + collectionDescriptor.getKeyDescriptor().getKeyPart().forEachSelectable( + (selectionIndex, selectableMapping) -> { + if ( !selectableMapping.isFormula() ) { + keySelectables.add( selectableMapping ); + } + } + ); + + final var indexDescriptor = collectionDescriptor.getIndexDescriptor(); + if ( indexDescriptor != null ) { + indexDescriptor.forEachSelectable( + (selectionIndex, selectableMapping) -> { + if ( !selectableMapping.isFormula() ) { + keySelectables.add( selectableMapping ); + } + } + ); + } + else { + collectionDescriptor.getElementDescriptor().forEachSelectable( + (selectionIndex, selectableMapping) -> { + if ( !selectableMapping.isFormula() ) { + keySelectables.add( selectableMapping ); + } + } + ); + } + return keySelectables; + } + + @Override + public void applyPredicate( + Supplier> predicateCollector, + SqlAstCreationState creationState, + StandardTableGroup tableGroup, + NamedTableReference rootTableReference, + EntityMappingType entityMappingType) { + if ( creationState.getLoadQueryInfluencers().getTemporalIdentifier() != null ) { + predicateCollector.get().accept( createRestriction( + entityMappingType, + tableGroup.resolveTableReference( getTableName() ), + collectEntityKeySelectables( entityMappingType ), + creationState.getSqlAliasBaseGenerator() + ) ); + } + } + + @Override + public boolean useAuxiliaryTable(LoadQueryInfluencers influencers) { + return influencers.getTemporalIdentifier() != null; + } + + @Override + public boolean isAffectedByInfluencers(LoadQueryInfluencers influencers) { + return influencers.getTemporalIdentifier() != null; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/BasicAttributeMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/BasicAttributeMapping.java index de0e545e9543..6427846cbd81 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/BasicAttributeMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/BasicAttributeMapping.java @@ -165,9 +165,10 @@ public BasicAttributeMapping( this.tableExpression = tableExpression; this.mappedColumnExpression = mappedColumnExpression; this.temporalPrecision = temporalPrecision; - this.selectablePath = selectablePath == null - ? new SelectablePath( mappedColumnExpression ) - : selectablePath; + this.selectablePath = + selectablePath == null + ? new SelectablePath( isFormula ? attributeName : mappedColumnExpression ) + : selectablePath; this.isFormula = isFormula; this.columnDefinition = columnDefinition; this.length = length; @@ -181,16 +182,10 @@ public BasicAttributeMapping( this.partitioned = partitioned; this.jdbcMapping = jdbcMapping; this.domainTypeDescriptor = jdbcMapping.getJavaTypeDescriptor(); - this.customReadExpression = customReadExpression; - - if ( isFormula ) { - this.customWriteExpression = null; - } - else { - this.customWriteExpression = customWriteExpression; - } - this.isLazy = navigableRole.getParent().getParent() == null + this.customWriteExpression = isFormula ? null : customWriteExpression; + this.isLazy = + navigableRole.getParent().getParent() == null && declaringType.findContainingEntityMapping() .getEntityPersister() .getBytecodeEnhancementMetadata() @@ -380,7 +375,8 @@ public NavigableRole getNavigableRole() { @Override public String toString() { - return "BasicAttributeMapping(" + navigableRole + ")@" + System.identityHashCode( this ); + return "BasicAttributeMapping(" + navigableRole + ")@" + + System.identityHashCode( this ); } @Override @@ -389,10 +385,9 @@ public DomainResult createDomainResult( TableGroup tableGroup, String resultVariable, DomainResultCreationState creationState) { - final SqlSelection sqlSelection = resolveSqlSelection( navigablePath, tableGroup, null, creationState ); - - //noinspection unchecked - return new BasicResult( + final var sqlSelection = + resolveSqlSelection( navigablePath, tableGroup, null, creationState ); + return new BasicResult<>( sqlSelection.getValuesArrayPosition(), resultVariable, jdbcMapping, @@ -409,12 +404,9 @@ private SqlSelection resolveSqlSelection( DomainResultCreationState creationState) { final var sqlAstCreationState = creationState.getSqlAstCreationState(); final var expressionResolver = sqlAstCreationState.getSqlExpressionResolver(); - final var tableReference = tableGroup.resolveTableReference( - navigablePath, - this, - getContainingTableExpression() - ); - + final var tableReference = + tableGroup.resolveTableReference( navigablePath, this, + getContainingTableExpression() ); return expressionResolver.resolveSqlSelection( expressionResolver.resolveSqlExpression( tableReference, this ), jdbcMapping.getJdbcJavaType(), @@ -486,7 +478,6 @@ public Fetch generateFetch( getJdbcMapping().getValueConverter(), fetchTiming, true, - creationState, coerceResultType, sqlSelection != null && !sqlSelection.isVirtual() ); diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/BasicEntityIdentifierMappingImpl.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/BasicEntityIdentifierMappingImpl.java index 6b28e756d836..c2f338be8f66 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/BasicEntityIdentifierMappingImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/BasicEntityIdentifierMappingImpl.java @@ -21,14 +21,13 @@ import org.hibernate.metamodel.mapping.EntityIdentifierMapping; import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.JdbcMapping; +import org.hibernate.metamodel.mapping.JdbcMappingContainer; import org.hibernate.metamodel.mapping.MappingType; import org.hibernate.metamodel.model.domain.NavigableRole; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.property.access.spi.PropertyAccess; -import org.hibernate.proxy.HibernateProxy; import org.hibernate.spi.NavigablePath; import org.hibernate.sql.ast.spi.SqlSelection; -import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.ast.tree.from.TableReference; import org.hibernate.sql.results.graph.DomainResult; @@ -41,6 +40,8 @@ import org.hibernate.type.BasicType; import org.hibernate.type.descriptor.java.JavaType; +import static org.hibernate.proxy.HibernateProxy.extractLazyInitializer; + /** * Mapping of a simple identifier * @@ -180,7 +181,7 @@ public IdentifierValue getUnsavedStrategy() { @Override public Object getIdentifier(Object entity) { - final var lazyInitializer = HibernateProxy.extractLazyInitializer( entity ); + final var lazyInitializer = extractLazyInitializer( entity ); if ( lazyInitializer != null ) { return lazyInitializer.getInternalIdentifier(); } @@ -246,7 +247,8 @@ public DomainResult createDomainResult( TableGroup tableGroup, String resultVariable, DomainResultCreationState creationState) { - final var sqlSelection = resolveSqlSelection( navigablePath, tableGroup, null, creationState ); + final var sqlSelection = + resolveSqlSelection( navigablePath, tableGroup, null, creationState ); return new BasicResult<>( sqlSelection.getValuesArrayPosition(), resultVariable, @@ -283,9 +285,18 @@ private SqlSelection resolveSqlSelection( FetchParent fetchParent, DomainResultCreationState creationState) { final var expressionResolver = creationState.getSqlAstCreationState().getSqlExpressionResolver(); - final TableReference rootTableReference; + final var rootTableReference = rootTableReference( navigablePath, tableGroup ); + return expressionResolver.resolveSqlSelection( + expressionResolver.resolveSqlExpression( rootTableReference, this ), + idType.getJdbcJavaType(), + fetchParent, + sessionFactory.getTypeConfiguration() + ); + } + + private TableReference rootTableReference(NavigablePath navigablePath, TableGroup tableGroup) { try { - rootTableReference = tableGroup.resolveTableReference( navigablePath, rootTable ); + return tableGroup.resolveTableReference( navigablePath, rootTable ); } catch (Exception e) { throw new IllegalStateException( @@ -299,18 +310,6 @@ private SqlSelection resolveSqlSelection( e ); } - - final Expression expression = expressionResolver.resolveSqlExpression( - rootTableReference, - this - ); - - return expressionResolver.resolveSqlSelection( - expression, - idType.getJdbcJavaType(), - fetchParent, - sessionFactory.getTypeConfiguration() - ); } @Override @@ -437,14 +436,14 @@ public Fetch generateFetch( boolean selected, String resultVariable, DomainResultCreationState creationState) { - final var sqlAstCreationState = creationState.getSqlAstCreationState(); final var tableGroup = - sqlAstCreationState.getFromClauseAccess() + creationState.getSqlAstCreationState() + .getFromClauseAccess() .getTableGroup( fetchParent.getNavigablePath() ); assert tableGroup != null; - final var sqlSelection = resolveSqlSelection( fetchablePath, tableGroup, fetchParent, creationState ); - final var selectionType = sqlSelection.getExpressionType(); + final var sqlSelection = + resolveSqlSelection( fetchablePath, tableGroup, fetchParent, creationState ); return new BasicFetch<>( sqlSelection.getValuesArrayPosition(), fetchParent, @@ -453,14 +452,18 @@ public Fetch generateFetch( getJdbcMapping().getValueConverter(), FetchTiming.IMMEDIATE, true, - creationState, // if the expression type is different that the expected type coerce the value - selectionType != null - && selectionType.getSingleJdbcMapping().getJdbcJavaType() != getJdbcMapping().getJdbcJavaType(), + mustCoerceResultType( sqlSelection.getExpressionType() ), !sqlSelection.isVirtual() ); } + private boolean mustCoerceResultType(JdbcMappingContainer selectionType) { + return selectionType != null + && selectionType.getSingleJdbcMapping().getJdbcJavaType() + != getJdbcMapping().getJdbcJavaType(); + } + @Override public FetchStyle getStyle() { return FetchStyle.JOIN; diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/BasicValuedCollectionPart.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/BasicValuedCollectionPart.java index ace237b91190..d59e5ef17332 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/BasicValuedCollectionPart.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/BasicValuedCollectionPart.java @@ -19,6 +19,7 @@ import org.hibernate.metamodel.mapping.PluralAttributeMapping; import org.hibernate.metamodel.mapping.SelectableConsumer; import org.hibernate.metamodel.mapping.SelectableMapping; +import org.hibernate.metamodel.mapping.SelectablePath; import org.hibernate.metamodel.model.domain.NavigableRole; import org.hibernate.persister.collection.CollectionPersister; import org.hibernate.spi.EntityIdentifierNavigablePath; @@ -26,7 +27,6 @@ import org.hibernate.sql.ast.spi.SqlSelection; import org.hibernate.sql.ast.tree.from.PluralTableGroup; import org.hibernate.sql.ast.tree.from.TableGroup; -import org.hibernate.sql.results.ResultsLogger; import org.hibernate.sql.results.graph.DomainResult; import org.hibernate.sql.results.graph.DomainResultCreationState; import org.hibernate.sql.results.graph.Fetch; @@ -36,6 +36,8 @@ import org.hibernate.sql.results.graph.basic.BasicResult; import org.hibernate.type.descriptor.java.JavaType; +import static org.hibernate.sql.results.ResultsLogger.RESULTS_LOGGER; + /** * Models a basic collection element/value or index/key * @@ -84,6 +86,16 @@ public String getSelectionExpression() { return selectableMapping.getSelectionExpression(); } + @Override + public SelectablePath getSelectablePath() { + return selectableMapping.getSelectablePath(); + } + + @Override + public String getSelectableName() { + return selectableMapping.getSelectableName(); + } + @Override public boolean isFormula() { return selectableMapping.isFormula(); @@ -261,8 +273,8 @@ public Fetch generateFetch( boolean selected, String resultVariable, DomainResultCreationState creationState) { - if ( ResultsLogger.RESULTS_LOGGER.isTraceEnabled() ) { - ResultsLogger.RESULTS_LOGGER.tracef( + if ( RESULTS_LOGGER.isTraceEnabled() ) { + RESULTS_LOGGER.tracef( "Generating Fetch for collection-part: `%s` -> `%s`", collectionDescriptor.getRole(), nature.getName() @@ -285,7 +297,6 @@ public Fetch generateFetch( fetchablePath, this, FetchTiming.IMMEDIATE, - creationState, !sqlSelection.isVirtual() ); } 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 a4ca7680880b..3831a11093b8 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 @@ -28,7 +28,6 @@ import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.ast.tree.from.TableReference; import org.hibernate.sql.ast.tree.predicate.NullnessPredicate; -import org.hibernate.sql.ast.tree.predicate.Predicate; import org.hibernate.sql.results.graph.DomainResult; import org.hibernate.sql.results.graph.DomainResultCreationState; import org.hibernate.sql.results.graph.FetchParent; @@ -62,14 +61,13 @@ public CaseStatementDiscriminatorMappingImpl( final String tableName = tableNames[notNullColumnTableNumbers[i]]; final String oneSubEntityColumn = notNullColumnNames[i]; final String discriminatorValue = discriminatorValues[i]; - tableDiscriminatorDetailsMap.put( - tableName, + tableDiscriminatorDetailsMap.put( tableName, new TableDiscriminatorDetails( tableName, oneSubEntityColumn, - getUnderlyingJdbcMapping().getJavaTypeDescriptor().wrap( discriminatorValue, null ) - ) - ); + getUnderlyingJdbcMapping().getJavaTypeDescriptor() + .wrap( discriminatorValue, null ) + ) ); } } } @@ -125,15 +123,15 @@ public Expression resolveSqlExpression( JdbcMapping jdbcMappingToUse, TableGroup tableGroup, SqlAstCreationState creationState) { - final var expressionResolver = creationState.getSqlExpressionResolver(); - return expressionResolver.resolveSqlExpression( - createColumnReferenceKey( - tableGroup.getPrimaryTableReference(), - getSelectionExpression(), - jdbcMappingToUse - ), - sqlAstProcessingState -> createCaseSearchedExpression( tableGroup ) - ); + return creationState.getSqlExpressionResolver() + .resolveSqlExpression( + createColumnReferenceKey( + tableGroup.getPrimaryTableReference(), + getSelectionExpression(), + jdbcMappingToUse + ), + sqlAstProcessingState -> createCaseSearchedExpression( tableGroup ) + ); } private Expression createCaseSearchedExpression(TableGroup entityTableGroup) { @@ -263,7 +261,8 @@ public CaseStatementDiscriminatorExpression(TableGroup entityTableGroup) { } public List getUsedTableReferences() { - final ArrayList usedTableReferences = new ArrayList<>( tableDiscriminatorDetailsMap.size() ); + final ArrayList usedTableReferences = + new ArrayList<>( tableDiscriminatorDetailsMap.size() ); tableDiscriminatorDetailsMap.forEach( (tableName, tableDiscriminatorDetails) -> { final var tableReference = entityTableGroup.getTableReference( @@ -287,7 +286,8 @@ public void renderToSql( SessionFactoryImplementor sessionFactory) { if ( caseSearchedExpression == null ) { // todo (6.0): possible optimization is to omit cases for table reference joins, that touch a super class, where a subclass is inner joined due to pruning - caseSearchedExpression = new CaseSearchedExpression( CaseStatementDiscriminatorMappingImpl.this ); + caseSearchedExpression = + new CaseSearchedExpression( CaseStatementDiscriminatorMappingImpl.this ); tableDiscriminatorDetailsMap.forEach( (tableName, tableDiscriminatorDetails) -> { final var tableReference = entityTableGroup.getTableReference( @@ -296,29 +296,26 @@ public void renderToSql( false ); - if ( tableReference == null ) { - // assume this is because it is a table that is not part of the processing entity's sub-hierarchy - return; + if ( tableReference != null ) { + caseSearchedExpression.when( + new NullnessPredicate( + new ColumnReference( + tableReference, + tableDiscriminatorDetails.getCheckColumnName(), + false, + null, + getJdbcMapping() + ), + true + ), + new QueryLiteral<>( + tableDiscriminatorDetails.getDiscriminatorValue(), + getUnderlyingJdbcMapping() + ) + ); } - - final Predicate predicate = new NullnessPredicate( - new ColumnReference( - tableReference, - tableDiscriminatorDetails.getCheckColumnName(), - false, - null, - getJdbcMapping() - ), - true - ); - - caseSearchedExpression.when( - predicate, - new QueryLiteral<>( - tableDiscriminatorDetails.getDiscriminatorValue(), - getUnderlyingJdbcMapping() - ) - ); + // else assume this is because it is a table that is + // not part of the processing entity's sub-hierarchy } ); } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/CollectionIdentifierDescriptorImpl.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/CollectionIdentifierDescriptorImpl.java index 9bb5843f01d9..de41785a65d1 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/CollectionIdentifierDescriptorImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/CollectionIdentifierDescriptorImpl.java @@ -47,7 +47,9 @@ public CollectionIdentifierDescriptorImpl( String containingTableName, String columnName, BasicType type) { - this.navigableRole = collectionDescriptor.getNavigableRole().append( Nature.ID.getName() ); + this.navigableRole = + collectionDescriptor.getNavigableRole() + .append( Nature.ID.getName() ); this.collectionDescriptor = collectionDescriptor; this.containingTableName = containingTableName; this.columnName = columnName; @@ -274,7 +276,6 @@ public Fetch generateFetch( fetchablePath, this, FetchTiming.IMMEDIATE, - creationState, !sqlSelection.isVirtual() ); } @@ -284,7 +285,6 @@ public DomainResult createDomainResult( TableGroup tableGroup, DomainResultCreationState creationState) { final var astCreationState = creationState.getSqlAstCreationState(); - final var astCreationContext = astCreationState.getCreationContext(); final var sqlExpressionResolver = astCreationState.getSqlExpressionResolver(); final var sqlSelection = sqlExpressionResolver.resolveSqlSelection( @@ -294,7 +294,8 @@ public DomainResult createDomainResult( ), type.getJdbcJavaType(), null, - astCreationContext.getTypeConfiguration() + astCreationState.getCreationContext() + .getTypeConfiguration() ); return new BasicResult<>( 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 395328a6595e..be0a05893e42 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 @@ -4,15 +4,12 @@ */ package org.hibernate.metamodel.mapping.internal; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.function.BiConsumer; -import java.util.function.Consumer; - +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.AssertionFailure; import org.hibernate.HibernateException; +import org.hibernate.MappingException; import org.hibernate.cache.MutableCacheKeyBuilder; +import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.internal.util.IndexedConsumer; import org.hibernate.loader.ast.internal.CompoundNaturalIdLoader; @@ -20,6 +17,7 @@ import org.hibernate.loader.ast.spi.MultiNaturalIdLoader; import org.hibernate.loader.ast.spi.NaturalIdLoader; import org.hibernate.metamodel.UnsupportedMappingException; +import org.hibernate.metamodel.mapping.AttributeMapping; import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.JdbcMapping; import org.hibernate.metamodel.mapping.MappingType; @@ -27,6 +25,11 @@ import org.hibernate.metamodel.mapping.NaturalIdMapping; import org.hibernate.metamodel.mapping.SelectableConsumer; import org.hibernate.metamodel.mapping.SingularAttributeMapping; +import org.hibernate.models.spi.ClassDetails; +import org.hibernate.models.spi.ModelsContext; +import org.hibernate.property.access.spi.Getter; +import org.hibernate.property.access.spi.GetterFieldImpl; +import org.hibernate.property.access.spi.GetterMethodImpl; import org.hibernate.spi.NavigablePath; import org.hibernate.sql.ast.spi.SqlSelection; import org.hibernate.sql.ast.tree.from.TableGroup; @@ -44,14 +47,27 @@ import org.hibernate.sql.results.jdbc.spi.RowProcessingState; import org.hibernate.type.descriptor.java.JavaType; +import java.lang.reflect.Method; +import java.lang.reflect.RecordComponent; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; + +import static org.hibernate.internal.util.StringHelper.decapitalize; +import static java.lang.reflect.Modifier.isStatic; +import static java.util.Collections.emptyMap; + /** * Multi-attribute NaturalIdMapping implementation */ public class CompoundNaturalIdMapping extends AbstractNaturalIdMapping implements MappingType, FetchableContainer { - // todo (6.0) : create a composite MappingType for this descriptor's Object[]? - private final List attributes; + private final ValueNormalizer valueNormalizer; private List jdbcMappings; /* @@ -60,11 +76,15 @@ This value is used to determine the size of the array used to create the Immutab */ private final int maxFetchableKeyIndex; + private final SessionFactoryImplementor sessionFactory; + public CompoundNaturalIdMapping( EntityMappingType declaringType, + ClassDetails naturalIdClass, List attributes, MappingModelCreationProcess creationProcess) { super( declaringType, isMutable( attributes ) ); + this.valueNormalizer = createValueNormalizer( naturalIdClass, attributes, creationProcess ); this.attributes = attributes; int maxIndex = 0; @@ -79,15 +99,15 @@ public CompoundNaturalIdMapping( "Determine compound natural-id JDBC mappings ( " + declaringType.getEntityName() + ")", () -> { final List jdbcMappings = new ArrayList<>(); - attributes.forEach( - (attribute) -> attribute.forEachJdbcType( - (index, jdbcMapping) -> jdbcMappings.add( jdbcMapping ) - ) - ); + attributes.forEach( attribute -> attribute.forEachJdbcType( + (index, jdbcMapping) -> jdbcMappings.add( jdbcMapping ) + ) ); this.jdbcMappings = jdbcMappings; return true; } ); + + this.sessionFactory = creationProcess.getCreationContext().getSessionFactory(); } private static boolean isMutable(List attributes) { @@ -99,6 +119,12 @@ private static boolean isMutable(List attributes) { return false; } + @Override + @Nullable + public Class getNaturalIdClass() { + return valueNormalizer.getIdClassType(); + } + @Override public Object[] extractNaturalIdFromEntityState(Object[] state) { if ( state == null ) { @@ -119,7 +145,7 @@ else if ( state.length == attributes.size() ) { @Override public Object[] extractNaturalIdFromEntity(Object entity) { - final var values = new Object[ attributes.size() ]; + final var values = new Object[attributes.size()]; for ( int i = 0; i < attributes.size(); i++ ) { values[i] = attributes.get( i ).getPropertyAccess().getGetter().get( entity ); } @@ -128,22 +154,22 @@ public Object[] extractNaturalIdFromEntity(Object entity) { @Override public Object[] normalizeInput(Object incoming) { + sessionFactory.getStatistics().normalizeNaturalId( getDeclaringType().getEntityName() ); + if ( incoming instanceof Object[] array ) { + // already normalized return array; } - else if ( incoming instanceof Map valueMap ) { - final var attributes = getNaturalIdAttributes(); - final var values = new Object[ attributes.size() ]; - for ( int i = 0; i < attributes.size(); i++ ) { - values[ i ] = valueMap.get( attributes.get( i ).getAttributeName() ); - } - return values; - } else { - throw new UnsupportedMappingException( "Could not normalize compound natural id value: " + incoming ); + return valueNormalizer.normalize( incoming ); } } + @Override + public boolean isNormalized(Object incoming) { + return incoming instanceof Object[]; + } + @Override public void validateInternalForm(Object naturalIdValue) { if ( naturalIdValue != null ) { @@ -198,8 +224,7 @@ public void verifyFlushState(Object id, Object[] currentState, Object[] loadedSt for ( int i = 0; i < getNaturalIdAttributes().size(); i++ ) { final var attributeMapping = getNaturalIdAttributes().get( i ); - final boolean updatable = attributeMapping.getAttributeMetadata().isUpdatable(); - if ( !updatable ) { + if ( !attributeMapping.getAttributeMetadata().isUpdatable() ) { final Object currentValue = naturalId[i]; final Object previousValue = previousNaturalId[i]; if ( !attributeMapping.areEqual( currentValue, previousValue, session ) ) { @@ -343,7 +368,7 @@ else if ( domainValue instanceof Object[] values ) { } } else { - throw new AssertionFailure("Unexpected domain value type"); + throw new AssertionFailure( "Unexpected domain value type" ); } return span; } @@ -394,7 +419,7 @@ else if ( value instanceof Object[] incoming ) { return outgoing; } else { - throw new AssertionFailure("Unexpected value"); + throw new AssertionFailure( "Unexpected value" ); } } @@ -412,7 +437,7 @@ else if ( value instanceof Object[] values ) { } } else { - throw new AssertionFailure("Unexpected value"); + throw new AssertionFailure( "Unexpected value" ); } } @@ -453,7 +478,7 @@ else if ( value instanceof Object[] incoming ) { } } else { - throw new AssertionFailure("Unexpected value"); + throw new AssertionFailure( "Unexpected value" ); } return span; } @@ -481,7 +506,7 @@ else if ( value instanceof Object[] incoming ) { } } else { - throw new AssertionFailure("Unexpected value"); + throw new AssertionFailure( "Unexpected value" ); } return span; } @@ -509,7 +534,7 @@ public ModelPart findSubPart(String name, EntityMappingType treatTargetType) { @Override public void forEachSubPart(IndexedConsumer consumer, EntityMappingType treatTarget) { for ( int i = 0; i < attributes.size(); i++ ) { - consumer.accept( i, attributes.get(i) ); + consumer.accept( i, attributes.get( i ) ); } } @@ -630,16 +655,16 @@ private AssemblerImpl(ImmutableFetchList fetches, JavaType jtd, Assemb @Override public Object[] assemble(RowProcessingState rowProcessingState) { - final var result = new Object[ subAssemblers.length ]; + final var result = new Object[subAssemblers.length]; for ( int i = 0; i < subAssemblers.length; i++ ) { - result[ i ] = subAssemblers[i].assemble( rowProcessingState ); + result[i] = subAssemblers[i].assemble( rowProcessingState ); } return result; } @Override public void resolveState(RowProcessingState rowProcessingState) { - for ( DomainResultAssembler subAssembler : subAssemblers ) { + for ( var subAssembler : subAssemblers ) { subAssembler.resolveState( rowProcessingState ); } } @@ -662,4 +687,232 @@ public JavaType getAssembledJavaType() { } } + interface ValueNormalizer { + boolean isInstance(Object value); + Object[] normalize(Object value); + Class getIdClassType(); + } + + private static ValueNormalizer createValueNormalizer( + ClassDetails naturalIdClassDetails, + List keyAttributes, + MappingModelCreationProcess creationProcess) { + if ( naturalIdClassDetails == null ) { + return new ValueNormalizerSupport( keyAttributes ); + } + + final var modelsContext = + creationProcess.getCreationContext().getBootstrapContext() + .getModelsContext(); + + var naturalIdClass = naturalIdClassDetails.toJavaClass( modelsContext.getClassLoading(), modelsContext ); + var naturalIdClassComponents = extractComponents( naturalIdClass ); + var naturalIdClassGetterAccess = createNaturalIdClassGetterAccess( naturalIdClass ); + + final List> attributeMappers = new ArrayList<>(); + keyAttributes.forEach( keyAttribute -> { + // find the matching MemberDetails on the `naturalIdClass`... + final var extractor = resolveMatchingExtractor( + naturalIdClass, + keyAttribute, + naturalIdClassGetterAccess, + naturalIdClassComponents, + modelsContext + ); + // todo (natural-id-class) : atm there is functionally no difference + // between BasicAttributeMapperImpl and ToOneAttributeMapperImpl. + // ideally we'd eventually support usage of the associated key entity's + // id and then there would. see the note in ToOneAttributeMapperImpl#extractFrom + final var attrMapper = + keyAttribute instanceof ToOneAttributeMapping + ? new ToOneAttributeMapperImpl( keyAttribute, extractor ) + : new BasicAttributeMapperImpl( keyAttribute, extractor ); + attributeMappers.add( attrMapper ); + } ); + + //noinspection unchecked,rawtypes + return new KeyClassNormalizer( keyAttributes, naturalIdClass, attributeMappers ); + } + + static class ValueNormalizerSupport implements ValueNormalizer { + private final List naturalIdAttributes; + + public ValueNormalizerSupport(List naturalIdAttributes) { + this.naturalIdAttributes = naturalIdAttributes; + } + + @Override + public boolean isInstance(Object value) { + return value instanceof Map; + } + + @Override + public Object[] normalize(Object incoming) { + if ( !isInstance( incoming ) ) { + throw new UnsupportedMappingException( "Could not normalize compound natural id value: " + incoming ); + } + final var values = new Object[naturalIdAttributes.size()]; + //noinspection unchecked + final Map valuesMap = (Map) incoming; + for ( int i = 0; i < naturalIdAttributes.size(); i++ ) { + values[i] = valuesMap.get( naturalIdAttributes.get( i ).getAttributeName() ); + } + return values; + } + + @Override + public Class getIdClassType() { + return null; + } + } + + /// Responsible for decomposing a value of the NaturalIdClass into the internal array format + static class KeyClassNormalizer extends ValueNormalizerSupport { + private final Class idClassType; + private final List> idClassAttributeMappers; + + public KeyClassNormalizer( + List naturalIdAttributes, + Class idClassType, + List> idClassAttributeMappers) { + super( naturalIdAttributes ); + this.idClassType = idClassType; + this.idClassAttributeMappers = idClassAttributeMappers; + } + + @Override + public Class getIdClassType() { + return idClassType; + } + + @Override + public Object[] normalize(Object value) { + if ( idClassType.isInstance( value ) ) { + return doNormalize( idClassType.cast( value ) ); + } + + return super.normalize( value ); + } + + public Object[] doNormalize(T idClassValue) { + final var result = new Object[idClassAttributeMappers.size()]; + for ( int i = 0; i < idClassAttributeMappers.size(); i++ ) { + var value = idClassAttributeMappers.get( i ).extractFrom( idClassValue ); + result[i] = value; + } + return result; + } + + public boolean isInstance(Object value) { + return idClassType.isInstance( value ) || super.isInstance( value ); + } + } + + private static Function createNaturalIdClassGetterAccess(Class naturalIdClass) { + return new Function<>() { + private Map getterMethods; + @Override + public Method apply(String name) { + if ( getterMethods == null ) { + getterMethods = extractGetterMethods( naturalIdClass ); + } + return getterMethods.get( name ); + } + }; + } + + private static Getter resolveMatchingExtractor( + Class naturalIdClass, + AttributeMapping keyAttribute, + Function getterMethodAccess, + Map naturalIdClassComponents, + ModelsContext modelsContext) { + // first, if the `naturalIdClass` is a record, look for a component + final String keyName = keyAttribute.getAttributeName(); + + if ( naturalIdClass.isRecord() ) { + final var component = naturalIdClassComponents.get( keyName ); + if ( component != null ) { + return new GetterMethodImpl( naturalIdClass, keyName, component.getAccessor() ); + } + } + + // next look for a getter method + final var getterMethod = getterMethodAccess.apply( keyName ); + if ( getterMethod != null ) { + return new GetterMethodImpl( naturalIdClass, keyName, getterMethod ); + } + + // lastly, look for a field + try { + return new GetterFieldImpl( naturalIdClass, keyName, + naturalIdClass.getDeclaredField( keyName ) ); + } + catch (NoSuchFieldException ignore) { + } + + throw new MappingException( "Unable to find NaturalIdClass accessor for natural id attribute: " + keyName ); + } + + private static Map extractGetterMethods(Class naturalIdClass) { + final Map result = new HashMap<>(); + for ( var declaredMethod : naturalIdClass.getDeclaredMethods() ) { + if ( declaredMethod.getParameterCount() == 0 + && declaredMethod.getReturnType() != void.class + && !isStatic( declaredMethod.getModifiers() ) ) { + var methodName = declaredMethod.getName(); + if ( methodName.startsWith( "is" ) ) { + result.put( decapitalize( methodName.substring( 2 ) ), + declaredMethod ); + } + else if ( methodName.startsWith( "get" ) ) { + result.put( decapitalize( methodName.substring( 3 ) ), + declaredMethod ); + } + } + } + return result; + } + + private static Map extractComponents(Class naturalIdClass) { + if ( !naturalIdClass.isRecord() ) { + return emptyMap(); + } + + final var recordComponents = naturalIdClass.getRecordComponents(); + final Map result = new HashMap<>(); + for ( RecordComponent recordComponent : recordComponents ) { + result.put( recordComponent.getName(), recordComponent ); + } + return result; + } + + public interface AttributeMapper { + V extractFrom(T keyValue); + } + + /// AttributeMapper for both basic and embedded values + public record BasicAttributeMapperImpl(AttributeMapping entityAttribute, Getter keyClassExtractor) + implements AttributeMapper { + @Override + public Object extractFrom(T keyValue) { + return keyClassExtractor.get( keyValue ); + } + } + + /// AttributeMapper for to-one values + public record ToOneAttributeMapperImpl(AttributeMapping entityAttribute, Getter keyClassExtractor) + implements AttributeMapper { + @Override + public Object extractFrom(T keyValue) { + // todo (natural-id-class) : handle "key -> to-one" resolutions + // this requires some contract changes though to pass Session + // to be able to resolve key -> entity for the to-one. + // + + /// the other difficulty is handling "derived id" structures + // + // see `NaturalIdMapping#normalizeInput` + return keyClassExtractor.get( keyValue ); + } + } } 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 48809f49bd69..497edd11af3d 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 @@ -36,7 +36,6 @@ import org.hibernate.sql.ast.spi.SqlAliasBase; import org.hibernate.sql.ast.spi.SqlAstCreationState; import org.hibernate.sql.ast.spi.SqlSelection; -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.predicate.Predicate; @@ -84,7 +83,7 @@ public DiscriminatedAssociationAttributeMapping( fetchableIndex, attributeMetadata, fetchTiming, - FetchStyle.SELECT, + fetchTiming == FetchTiming.IMMEDIATE ? FetchStyle.JOIN : FetchStyle.SELECT, declaringType, propertyAccess ); @@ -140,8 +139,17 @@ public Fetch generateFetch( fetchTiming, selected, resultVariable, - creationState - ); + creationState + ); + } + + @Override + public Fetch resolveCircularFetch( + NavigablePath fetchablePath, + FetchParent fetchParent, + FetchTiming fetchTiming, + DomainResultCreationState creationState) { + return discriminatorMapping.resolveCircularFetch( fetchParent, fetchablePath, fetchTiming, creationState ); } @Override @@ -466,7 +474,7 @@ public Serializable disassemble(Object value, SharedSessionContract session) { // this ^^ is what we want eventually, but for the time-being to ensure compatibility with // writing just reuse the AnyType - final SharedSessionContractImplementor persistenceContext = (SharedSessionContractImplementor) session; + final var persistenceContext = (SharedSessionContractImplementor) session; return anyType.disassemble( value, persistenceContext, null ); } @@ -477,7 +485,7 @@ public Object assemble(Serializable cached, SharedSessionContract session) { // again, what we want eventually ^^ versus what we should do now vv - final SharedSessionContractImplementor persistenceContext = (SharedSessionContractImplementor) session; + final var persistenceContext = (SharedSessionContractImplementor) session; return anyType.assemble( cached, persistenceContext, null ); } } @@ -517,7 +525,14 @@ public TableGroup createRootTableGroupJoin( boolean fetched, @Nullable Consumer predicateConsumer, SqlAstCreationState creationState) { - return new StandardVirtualTableGroup( navigablePath, this, lhs, fetched ); + return discriminatorMapping.createRootTableGroupJoin( + navigablePath, + lhs, + fetched, + sqlAstJoinType, + predicateConsumer, + creationState + ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/DiscriminatedAssociationMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/DiscriminatedAssociationMapping.java index d5dbbe343c6a..a9bdfd0d6b06 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/DiscriminatedAssociationMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/DiscriminatedAssociationMapping.java @@ -4,7 +4,12 @@ */ package org.hibernate.metamodel.mapping.internal; +import java.util.ArrayList; +import java.util.List; import java.util.Locale; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Supplier; import org.hibernate.engine.FetchStyle; import org.hibernate.engine.FetchTiming; @@ -12,6 +17,7 @@ import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.mapping.Any; import org.hibernate.mapping.Column; +import org.hibernate.metamodel.mapping.AssociationKey; import org.hibernate.metamodel.mapping.BasicValuedModelPart; import org.hibernate.metamodel.mapping.DiscriminatedAssociationModelPart; import org.hibernate.metamodel.mapping.DiscriminatorMapping; @@ -22,12 +28,26 @@ import org.hibernate.metamodel.mapping.SelectablePath; import org.hibernate.metamodel.model.domain.NavigableRole; import org.hibernate.spi.NavigablePath; +import org.hibernate.sql.ast.SqlAstJoinType; +import org.hibernate.sql.ast.spi.FromClauseAccess; import org.hibernate.sql.ast.tree.from.TableGroup; +import org.hibernate.sql.ast.tree.from.TableGroupJoin; +import org.hibernate.sql.ast.tree.from.TableReference; +import org.hibernate.sql.ast.tree.from.TableGroupJoinProducer; +import org.hibernate.sql.ast.tree.from.StandardVirtualTableGroup; +import org.hibernate.sql.ast.tree.expression.ColumnReference; +import org.hibernate.sql.ast.tree.expression.QueryLiteral; +import org.hibernate.sql.ast.tree.predicate.ComparisonPredicate; +import org.hibernate.sql.ast.tree.predicate.Junction; +import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.sql.ast.tree.predicate.PredicateCollector; import org.hibernate.sql.results.graph.DomainResult; import org.hibernate.sql.results.graph.DomainResultCreationState; 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.entity.internal.JoinedDiscriminatedEntityFetch; +import org.hibernate.sql.results.graph.entity.internal.JoinedDiscriminatedEntityResult; import org.hibernate.sql.results.graph.entity.internal.DiscriminatedEntityFetch; import org.hibernate.sql.results.graph.entity.internal.DiscriminatedEntityResult; import org.hibernate.type.AnyType; @@ -36,6 +56,8 @@ import org.hibernate.type.descriptor.java.JavaType; import static org.hibernate.metamodel.mapping.internal.MappingModelCreationHelper.getSelectablePath; +import static org.hibernate.metamodel.mapping.internal.MappingModelCreationHelper.getTableIdentifierExpression; +import static org.hibernate.query.sqm.ComparisonOperator.EQUAL; /** * Represents the "type" of an any-valued mapping @@ -52,11 +74,11 @@ public static DiscriminatedAssociationMapping from( Any bootValueMapping, MappingModelCreationProcess creationProcess) { - final var dialect = creationProcess.getCreationContext().getDialect(); - final String tableName = MappingModelCreationHelper.getTableIdentifierExpression( - bootValueMapping.getTable(), - creationProcess - ); + final var creationContext = creationProcess.getCreationContext(); + final var sessionFactory = creationContext.getSessionFactory(); + final var dialect = creationContext.getDialect(); + final String tableName = + getTableIdentifierExpression( bootValueMapping.getTable(), creationProcess ); assert bootValueMapping.getColumnSpan() == 2; final var columnIterator = bootValueMapping.getSelectables().iterator(); @@ -70,7 +92,7 @@ public static DiscriminatedAssociationMapping from( assert !keySelectable.isFormula(); final var metaColumn = (Column) metaSelectable; final var keyColumn = (Column) keySelectable; - final SelectablePath parentSelectablePath = + final var parentSelectablePath = declaringModelPart.asAttributeMapping() != null ? getSelectablePath( declaringModelPart.asAttributeMapping().getDeclaringType() ) : null; @@ -81,7 +103,8 @@ public static DiscriminatedAssociationMapping from( declaringModelPart, tableName, metaColumn.getText( dialect ), - parentSelectablePath != null ? parentSelectablePath.append( metaColumn.getQuotedName( dialect ) ) + parentSelectablePath != null + ? parentSelectablePath.append( metaColumn.getQuotedName( dialect ) ) : new SelectablePath( metaColumn.getQuotedName( dialect ) ), metaColumn.getCustomReadExpression(), metaColumn.getCustomWriteExpression(), @@ -96,7 +119,7 @@ public static DiscriminatedAssociationMapping from( (BasicType) metaType.getBaseType(), metaType.getDiscriminatorValuesToEntityNameMap(), metaType.getImplicitValueStrategy(), - creationProcess.getCreationContext().getSessionFactory().getMappingMetamodel() + sessionFactory.getMappingMetamodel() ); @@ -106,7 +129,8 @@ public static DiscriminatedAssociationMapping from( declaringModelPart, tableName, keyColumn.getText( dialect ), - parentSelectablePath != null ? parentSelectablePath.append( keyColumn.getQuotedName( dialect ) ) + parentSelectablePath != null + ? parentSelectablePath.append( keyColumn.getQuotedName( dialect ) ) : new SelectablePath( keyColumn.getQuotedName( dialect ) ), keyColumn.getCustomReadExpression(), keyColumn.getCustomWriteExpression(), @@ -130,7 +154,7 @@ public static DiscriminatedAssociationMapping from( bootValueMapping.isLazy() ? FetchTiming.DELAYED : FetchTiming.IMMEDIATE, - creationProcess.getCreationContext().getSessionFactory() + sessionFactory ); } @@ -140,6 +164,7 @@ public static DiscriminatedAssociationMapping from( private final JavaType baseAssociationJtd; private final FetchTiming fetchTiming; private final SessionFactoryImplementor sessionFactory; + private AssociationKey associationKey; public DiscriminatedAssociationMapping( DiscriminatedAssociationModelPart modelPart, @@ -169,12 +194,10 @@ public BasicValuedModelPart getKeyPart() { } public Object resolveDiscriminatorValueToEntityMapping(EntityMappingType entityMappingType) { - final DiscriminatorValueDetails details = + final var details = discriminatorPart.getValueConverter() .getDetailsForEntityName( entityMappingType.getEntityName() ); - return details != null - ? details.getValue() - : null; + return details == null ? null : details.getValue(); } public EntityMappingType resolveDiscriminatorValueToEntityMapping(Object discriminatorValue) { @@ -294,7 +317,9 @@ private ModelPart resolveAssociatedSubPart(String name, EntityMappingType entity private void ensureMapped(EntityMappingType treatTarget) { assert treatTarget != null; - final DiscriminatorValueDetails details = discriminatorPart.getValueConverter().getDetailsForEntityName( treatTarget.getEntityName() ); + final var details = + discriminatorPart.getValueConverter() + .getDetailsForEntityName( treatTarget.getEntityName() ); if ( details == null ) { throw new IllegalArgumentException( String.format( @@ -322,7 +347,7 @@ public JavaType getMappedJavaType() { @Override public FetchStyle getStyle() { - return FetchStyle.SELECT; + return fetchTiming == FetchTiming.IMMEDIATE ? FetchStyle.JOIN : FetchStyle.SELECT; } @Override @@ -330,6 +355,230 @@ public FetchTiming getTiming() { return fetchTiming; } + AssociationKey getAssociationKey() { + if ( associationKey == null ) { + final List columns = new ArrayList<>( discriminatorPart.getJdbcTypeCount() + keyPart.getJdbcTypeCount() ); + discriminatorPart.forEachSelectable( (selectionIndex, selectableMapping) -> columns.add( selectableMapping.getSelectionExpression() ) ); + keyPart.forEachSelectable( (selectionIndex, selectableMapping) -> columns.add( selectableMapping.getSelectionExpression() ) ); + associationKey = new AssociationKey( discriminatorPart.getContainingTableExpression(), columns ); + } + return associationKey; + } + + Fetch resolveCircularFetch( + FetchParent fetchParent, + NavigablePath fetchablePath, + FetchTiming fetchTiming, + DomainResultCreationState creationState) { + if ( creationState.isAssociationKeyVisited( getAssociationKey() ) ) { + return new DiscriminatedEntityFetch( + fetchablePath, + baseAssociationJtd, + modelPart, + fetchTiming, + fetchParent, + creationState + ); + } + return null; + } + + private Fetch withRegisteredAssociationKey( + Supplier fetchCreator, + DomainResultCreationState creationState) { + final boolean added = creationState.registerVisitedAssociationKey( getAssociationKey() ); + try { + return fetchCreator.get(); + } + finally { + if ( added ) { + creationState.removeVisitedAssociationKey( getAssociationKey() ); + } + } + } + + List getMappedEntityValueDetails() { + final var valueDetails = new ArrayList(); + discriminatorPart.getValueConverter().forEachValueDetail( valueDetails::add ); + return valueDetails; + } + + public static NavigablePath concreteEntityPath(NavigablePath associationPath, EntityMappingType entityMappingType) { + return associationPath.treatAs( entityMappingType.getEntityName() ); + } + + TableGroup createRootTableGroupJoin( + NavigablePath navigablePath, + TableGroup lhs, + boolean fetched, + SqlAstJoinType requestedJoinType, + Consumer predicateConsumer, + org.hibernate.sql.ast.spi.SqlAstCreationState creationState) { + final var virtualTableGroup = new StandardVirtualTableGroup( navigablePath, modelPart, lhs, fetched ); + final var valueDetails = getMappedEntityValueDetails(); + final SqlAstJoinType effectiveJoinType = + valueDetails.size() == 1 && requestedJoinType == SqlAstJoinType.INNER + ? SqlAstJoinType.INNER + : SqlAstJoinType.LEFT; + + for ( DiscriminatorValueDetails valueDetail : valueDetails ) { + addConcreteEntityTableGroupJoin( + virtualTableGroup, + lhs, + navigablePath, + valueDetail, + effectiveJoinType, + predicateConsumer, + creationState + ); + } + + return virtualTableGroup; + } + + private void addConcreteEntityTableGroupJoin( + StandardVirtualTableGroup virtualTableGroup, + TableGroup lhs, + NavigablePath associationPath, + DiscriminatorValueDetails valueDetail, + SqlAstJoinType joinType, + Consumer predicateConsumer, + org.hibernate.sql.ast.spi.SqlAstCreationState creationState) { + final var entityMapping = valueDetail.getIndicatedEntity(); + final var concretePath = concreteEntityPath( associationPath, entityMapping ); + final var joinPredicateCollector = new PredicateCollector(); + + final var entityTableGroup = entityMapping.createRootTableGroup( + joinType == SqlAstJoinType.INNER && lhs.canUseInnerJoins(), + concretePath, + null, + null, + () -> joinPredicateCollector::applyPredicate, + creationState + ); + joinPredicateCollector.applyPredicate( + createAssociationPredicate( lhs, entityTableGroup, entityMapping, valueDetail ) + ); + + applyEntityRestrictions( joinPredicateCollector, entityMapping, entityTableGroup, creationState ); + + final var tableGroupJoin = new TableGroupJoin( + concretePath, + joinType, + entityTableGroup, + joinPredicateCollector.getPredicate() + ); + virtualTableGroup.addNestedTableGroupJoin( tableGroupJoin ); + creationState.getFromClauseAccess().registerTableGroup( concretePath, entityTableGroup ); + + if ( predicateConsumer != null && joinPredicateCollector.getPredicate() != null ) { + predicateConsumer.accept( joinPredicateCollector.getPredicate() ); + } + } + + private void applyEntityRestrictions( + PredicateCollector predicateCollector, + EntityMappingType entityMappingType, + TableGroup entityTableGroup, + org.hibernate.sql.ast.spi.SqlAstCreationState creationState) { + final Map enabledFilters = creationState.getLoadQueryInfluencers().getEnabledFilters(); + if ( entityMappingType.getEntityPersister().hasFilterForLoadByKey() ) { + entityMappingType.applyBaseRestrictions( + predicateCollector::applyPredicate, + entityTableGroup, + true, + enabledFilters, + creationState.applyOnlyLoadByKeyFilters(), + null, + creationState + ); + } + entityMappingType.applyWhereRestrictions( + predicateCollector::applyPredicate, + entityTableGroup, + true, + creationState + ); + if ( entityMappingType.getSuperMappingType() != null && !creationState.supportsEntityNameUsage() ) { + entityMappingType.applyDiscriminator( null, null, entityTableGroup, creationState ); + } + final var auxiliaryMapping = entityMappingType.getAuxiliaryMapping(); + if ( auxiliaryMapping != null ) { + auxiliaryMapping.applyPredicate( + entityMappingType, + predicateCollector::applyPredicate, + entityTableGroup, + creationState.getSqlAliasBaseGenerator(), + creationState.getLoadQueryInfluencers() + ); + } + } + + private Predicate createAssociationPredicate( + TableGroup lhs, + TableGroup entityTableGroup, + EntityMappingType entityMappingType, + DiscriminatorValueDetails valueDetail) { + final var identifierMapping = entityMappingType.getIdentifierMapping(); + final BasicValuedModelPart identifierPart = identifierMapping.asBasicValuedModelPart(); + if ( identifierPart == null ) { + throw new UnsupportedOperationException( + "Join fetching an @Any association is not supported for entity '" + entityMappingType.getEntityName() + + "' because it does not use a basic identifier" + ); + } + + final TableReference discriminatorTableReference = + lhs.resolveTableReference( null, discriminatorPart.getContainingTableExpression() ); + final TableReference keyTableReference = + lhs.resolveTableReference( null, keyPart.getContainingTableExpression() ); + final TableReference identifierTableReference = + entityTableGroup.resolveTableReference( entityTableGroup.getNavigablePath(), identifierPart.getContainingTableExpression() ); + + final Junction predicate = new Junction( Junction.Nature.CONJUNCTION ); + predicate.add( + new ComparisonPredicate( + new ColumnReference( discriminatorTableReference, discriminatorPart ), + EQUAL, + new QueryLiteral<>( valueDetail.getValue(), discriminatorPart ) + ) + ); + predicate.add( + new ComparisonPredicate( + new ColumnReference( identifierTableReference, identifierPart ), + EQUAL, + new ColumnReference( keyTableReference, keyPart ) + ) + ); + return predicate; + } + + private TableGroup resolveJoinedFetchTableGroup( + FetchParent fetchParent, + NavigablePath fetchablePath, + String resultVariable, + DomainResultCreationState creationState) { + final FromClauseAccess fromClauseAccess = creationState.getSqlAstCreationState().getFromClauseAccess(); + return fromClauseAccess.resolveTableGroup( + fetchablePath, + navigablePath -> { + final TableGroup parentTableGroup = fromClauseAccess.getTableGroup( fetchParent.getNavigablePath() ); + final TableGroupJoin tableGroupJoin = ( (TableGroupJoinProducer) modelPart ).createTableGroupJoin( + navigablePath, + parentTableGroup, + resultVariable, + null, + SqlAstJoinType.LEFT, + true, + false, + creationState.getSqlAstCreationState() + ); + parentTableGroup.addTableGroupJoin( tableGroupJoin ); + return tableGroupJoin.getJoinedGroup(); + } + ); + } + public Fetch generateFetch( FetchParent fetchParent, NavigablePath fetchablePath, @@ -337,6 +586,23 @@ public Fetch generateFetch( boolean selected, String resultVariable, DomainResultCreationState creationState) { + if ( selected ) { + return withRegisteredAssociationKey( + () -> { + resolveJoinedFetchTableGroup( fetchParent, fetchablePath, resultVariable, creationState ); + return new JoinedDiscriminatedEntityFetch( + fetchablePath, + baseAssociationJtd, + modelPart, + fetchTiming, + fetchParent, + getMappedEntityValueDetails(), + creationState + ); + }, + creationState + ); + } return new DiscriminatedEntityFetch( fetchablePath, baseAssociationJtd, @@ -352,13 +618,49 @@ public DomainResult createDomainResult( TableGroup tableGroup, String resultVariable, DomainResultCreationState creationState) { - return new DiscriminatedEntityResult<>( - navigablePath, - baseAssociationJtd, - modelPart, - resultVariable, - creationState - ); + if ( resolveJoinedResultTableGroup( navigablePath, tableGroup, creationState ) != null ) { + return new JoinedDiscriminatedEntityResult<>( + navigablePath, + baseAssociationJtd, + modelPart, + resultVariable, + getMappedEntityValueDetails(), + creationState + ); + } + else { + return new DiscriminatedEntityResult<>( + navigablePath, + baseAssociationJtd, + modelPart, + resultVariable, + creationState + ); + } + } + + private TableGroup resolveJoinedResultTableGroup( + NavigablePath navigablePath, + TableGroup tableGroup, + DomainResultCreationState creationState) { + final TableGroup joinedTableGroup = + creationState.getSqlAstCreationState().getFromClauseAccess() + .findTableGroup( navigablePath ); + if ( joinedTableGroup != null + && isModelPartWithRealJoins( joinedTableGroup ) ) { + return joinedTableGroup; + } + else if ( isModelPartWithRealJoins( tableGroup ) ) { + return tableGroup; + } + else { + return null; + } + } + + private boolean isModelPartWithRealJoins(TableGroup joinedTableGroup) { + return joinedTableGroup.getModelPart() == modelPart + && joinedTableGroup.hasRealJoins(); } } 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 d16c3806ee66..fbc5d72c2d38 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 @@ -30,7 +30,6 @@ import org.hibernate.sql.ast.spi.SqlAliasBase; import org.hibernate.sql.ast.spi.SqlAstCreationState; import org.hibernate.sql.ast.spi.SqlSelection; -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.predicate.Predicate; @@ -369,7 +368,14 @@ public TableGroup createRootTableGroupJoin( boolean fetched, @Nullable Consumer predicateConsumer, SqlAstCreationState creationState) { - return new StandardVirtualTableGroup( navigablePath, this, lhs, fetched ); + return associationMapping.createRootTableGroupJoin( + navigablePath, + lhs, + fetched, + sqlAstJoinType, + predicateConsumer, + creationState + ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/DiscriminatorValueDetailsImpl.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/DiscriminatorValueDetailsImpl.java index 8a68a66d5dbd..4e9767ea69b2 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/DiscriminatorValueDetailsImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/DiscriminatorValueDetailsImpl.java @@ -4,6 +4,7 @@ */ package org.hibernate.metamodel.mapping.internal; +import org.hibernate.metamodel.mapping.DiscriminatorValue; import org.hibernate.metamodel.mapping.DiscriminatorValueDetails; import org.hibernate.metamodel.mapping.EntityMappingType; @@ -11,17 +12,17 @@ * @author Steve Ebersole */ public class DiscriminatorValueDetailsImpl implements DiscriminatorValueDetails { - private final Object value; + private final DiscriminatorValue value; private final EntityMappingType matchedEntityDescriptor; - public DiscriminatorValueDetailsImpl(Object value, EntityMappingType matchedEntityDescriptor) { + public DiscriminatorValueDetailsImpl(DiscriminatorValue value, EntityMappingType matchedEntityDescriptor) { this.value = value; this.matchedEntityDescriptor = matchedEntityDescriptor; } @Override public Object getValue() { - return value; + return value.value(); } @Override 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 b957478d6c88..118659129f02 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 @@ -17,12 +17,8 @@ import org.hibernate.MappingException; import org.hibernate.SharedSessionContract; -import org.hibernate.dialect.Dialect; import org.hibernate.engine.FetchTiming; -import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; -import org.hibernate.engine.jdbc.spi.JdbcServices; import org.hibernate.engine.spi.SharedSessionContractImplementor; -import org.hibernate.mapping.AggregateColumn; import org.hibernate.mapping.Any; import org.hibernate.mapping.BasicValue; import org.hibernate.mapping.Column; @@ -65,12 +61,14 @@ import org.hibernate.type.descriptor.java.ImmutableMutabilityPlan; import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.descriptor.java.MutabilityPlan; -import org.hibernate.type.descriptor.jdbc.AggregateJdbcType; -import org.hibernate.type.descriptor.jdbc.JdbcTypeConstructor; -import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; import org.hibernate.type.spi.CompositeTypeImplementor; -import org.hibernate.type.spi.TypeConfiguration; +import static java.lang.System.arraycopy; +import static org.hibernate.metamodel.mapping.internal.MappingModelCreationHelper.buildBasicAttributeMapping; +import static org.hibernate.metamodel.mapping.internal.MappingModelCreationHelper.buildEmbeddedAttributeMapping; +import static org.hibernate.metamodel.mapping.internal.MappingModelCreationHelper.buildPluralAttributeMapping; +import static org.hibernate.metamodel.mapping.internal.MappingModelCreationHelper.buildSingularAssociationAttributeMapping; +import static org.hibernate.metamodel.mapping.internal.MappingModelCreationHelper.getTableIdentifierExpression; import static org.hibernate.type.SqlTypes.ARRAY; import static org.hibernate.type.SqlTypes.JSON; import static org.hibernate.type.SqlTypes.JSON_ARRAY; @@ -203,9 +201,9 @@ private EmbeddableMappingTypeImpl( this.concreteEmbeddableBySubclass = null; } - final AggregateColumn aggregateColumn = bootDescriptor.getAggregateColumn(); + final var aggregateColumn = bootDescriptor.getAggregateColumn(); if ( aggregateColumn != null ) { - final Dialect dialect = creationContext.getDialect(); + final var dialect = creationContext.getDialect(); final boolean insertable; final boolean updatable; if ( componentProperty == null ) { @@ -220,6 +218,7 @@ private EmbeddableMappingTypeImpl( bootDescriptor.getOwner().getTable() .getQualifiedName( creationContext.getSqlStringGenerationContext() ), aggregateColumn, + null, bootDescriptor.getParentAggregateColumn() != null ? bootDescriptor.getParentAggregateColumn().getSelectablePath() : null, @@ -228,13 +227,18 @@ private EmbeddableMappingTypeImpl( insertable, updatable, false, + false, dialect, null, creationContext ); - final int defaultSqlTypeCode = aggregateMapping.getJdbcMapping().getJdbcType().getDefaultSqlTypeCode(); + final int defaultSqlTypeCode = + aggregateMapping.getJdbcMapping().getJdbcType().getDefaultSqlTypeCode(); final var aggregateSupport = dialect.getAggregateSupport(); - final int sqlTypeCode = defaultSqlTypeCode == ARRAY ? aggregateColumn.getTypeCode() : defaultSqlTypeCode; + final int sqlTypeCode = + defaultSqlTypeCode == ARRAY + ? aggregateColumn.getTypeCode() + : defaultSqlTypeCode; this.aggregateMappingRequiresColumnWriter = aggregateSupport .requiresAggregateCustomWriteExpressionRenderer( sqlTypeCode ); this.preferSelectAggregateMapping = aggregateSupport.preferSelectAggregateMapping( sqlTypeCode ); @@ -296,36 +300,36 @@ private JdbcMapping resolveJdbcMapping(Component bootDescriptor, RuntimeModelCre aggregateSqlTypeCode = aggregateColumnSqlTypeCode; break; } - final JdbcTypeRegistry jdbcTypeRegistry = typeConfiguration.getJdbcTypeRegistry(); - final AggregateJdbcType aggregateJdbcType = jdbcTypeRegistry.resolveAggregateDescriptor( + final var jdbcTypeRegistry = typeConfiguration.getJdbcTypeRegistry(); + final var aggregateJdbcType = jdbcTypeRegistry.resolveAggregateDescriptor( aggregateSqlTypeCode, structTypeName, this, creationContext ); - final BasicType basicType = basicTypeRegistry.resolve( - getMappedJavaType(), - aggregateJdbcType - ); + final var basicType = basicTypeRegistry.resolve( getMappedJavaType(), aggregateJdbcType ); // Register the resolved type under its struct name and java class name - if ( bootDescriptor.getStructName() != null ) { - basicTypeRegistry.register( basicType, bootDescriptor.getStructName().render() ); + final var structName = bootDescriptor.getStructName(); + if ( structName != null ) { + basicTypeRegistry.register( basicType, structName.render() ); basicTypeRegistry.register( basicType, getMappedJavaType().getJavaTypeClass().getName() ); } final BasicType resolvedJdbcMapping; if ( isArray ) { - final JdbcTypeConstructor arrayConstructor = jdbcTypeRegistry.getConstructor( aggregateColumnSqlTypeCode ); + final var arrayConstructor = jdbcTypeRegistry.getConstructor( aggregateColumnSqlTypeCode ); if ( arrayConstructor == null ) { throw new IllegalArgumentException( "No JdbcTypeConstructor registered for SqlTypes." + JdbcTypeNameMapper.getTypeName( aggregateColumnSqlTypeCode ) ); } //noinspection rawtypes,unchecked - final BasicType arrayType = ( (BasicPluralJavaType) resolution.getDomainJavaType() ).resolveType( - typeConfiguration, - creationContext.getDialect(), - basicType, - aggregateColumn, - typeConfiguration.getCurrentBaseSqlTypeIndicators() - ); + final BasicType arrayType = + ( (BasicPluralJavaType) resolution.getDomainJavaType() ) + .resolveType( + typeConfiguration, + creationContext.getDialect(), + basicType, + aggregateColumn, + typeConfiguration.getCurrentBaseSqlTypeIndicators() + ); basicTypeRegistry.register( arrayType ); resolvedJdbcMapping = arrayType; } @@ -356,7 +360,9 @@ public EmbeddableMappingTypeImpl( this.preferBindAggregateMapping = false; this.selectableMappings = selectableMappings; creationProcess.registerInitializationCallback( - "EmbeddableMappingType(" + inverseMappingType.getNavigableRole().getFullPath() + ".{inverse})#finishInitialization", + "EmbeddableMappingType(" + + inverseMappingType.getNavigableRole().getFullPath() + + ".{inverse})#finishInitialization", () -> inverseInitializeCallback( declaringTableGroupProducer, selectableMappings, @@ -418,13 +424,14 @@ private boolean finishInitialization( // ); // todo (6.0) - get this ^^ to work, or drop the comment - final TypeConfiguration typeConfiguration = creationProcess.getCreationContext().getTypeConfiguration(); - final JdbcServices jdbcServices = creationProcess.getCreationContext().getJdbcServices(); - final JdbcEnvironment jdbcEnvironment = jdbcServices.getJdbcEnvironment(); - final Dialect dialect = jdbcEnvironment.getDialect(); + final var creationContext = creationProcess.getCreationContext(); + final var typeConfiguration = creationContext.getTypeConfiguration(); + final var dialect = + creationContext.getJdbcServices() + .getJdbcEnvironment().getDialect(); final String baseTableExpression = valueMapping.getContainingTableExpression(); - final Type[] subtypes = compositeType.getSubtypes(); + final var subtypes = compositeType.getSubtypes(); int attributeIndex = 0; int columnPosition = 0; @@ -432,7 +439,7 @@ private boolean finishInitialization( // Reset the attribute mappings that were added in previous attempts attributeMappings.clear(); - for ( final Property bootPropertyDescriptor : bootDescriptor.getProperties() ) { + for ( final var bootPropertyDescriptor : bootDescriptor.getProperties() ) { final AttributeMapping attributeMapping; final Type subtype = subtypes[attributeIndex]; @@ -446,18 +453,13 @@ private boolean finishInitialization( final String containingTableExpression; final String columnExpression; if ( rootTableKeyColumnNames == null ) { - if ( selectable.isFormula() ) { - columnExpression = selectable.getTemplate( dialect, - creationProcess.getCreationContext().getTypeConfiguration() ); - } - else { - columnExpression = selectable.getText( dialect ); - } + columnExpression = + selectable.isFormula() + ? selectable.getTemplate( dialect, typeConfiguration ) + : selectable.getText( dialect ); if ( selectable instanceof Column column ) { - containingTableExpression = MappingModelCreationHelper.getTableIdentifierExpression( - column.getValue().getTable(), - creationProcess - ); + containingTableExpression = + getTableIdentifierExpression( column.getValue().getTable(), creationProcess ); } else { containingTableExpression = baseTableExpression; @@ -467,7 +469,9 @@ private boolean finishInitialization( containingTableExpression = rootTableExpression; columnExpression = rootTableKeyColumnNames[columnPosition]; } - final var role = valueMapping.getNavigableRole().append( bootPropertyDescriptor.getName() ); + final var role = + valueMapping.getNavigableRole() + .append( bootPropertyDescriptor.getName() ); final SelectablePath selectablePath; final String columnDefinition; final Long length; @@ -484,7 +488,7 @@ private boolean finishInitialization( precision = column.getPrecision(); scale = column.getScale(); temporalPrecision = column.getTemporalPrecision(); - isLob = column.isSqlTypeLob( creationProcess.getCreationContext().getMetadata() ); + isLob = column.isSqlTypeLob( creationContext.getMetadata() ); nullable = bootPropertyDescriptor.isOptional() && column.isNullable() ; selectablePath = basicValue.createSelectablePath( column.getQuotedName( dialect ) ); MappingModelCreationHelper.resolveAggregateColumnBasicType( creationProcess, role, column ); @@ -500,7 +504,7 @@ private boolean finishInitialization( nullable = bootPropertyDescriptor.isOptional(); selectablePath = new SelectablePath( determineEmbeddablePrefix() + bootPropertyDescriptor.getName() ); } - attributeMapping = MappingModelCreationHelper.buildBasicAttributeMapping( + attributeMapping = buildBasicAttributeMapping( bootPropertyDescriptor.getName(), role, attributeIndex, @@ -516,7 +520,7 @@ private boolean finishInitialization( selectable.getWriteExpr( basicValue.getResolution().getJdbcMapping(), dialect, - creationProcess.getCreationContext().getBootModel() + creationContext.getBootModel() ), columnDefinition, length, @@ -537,28 +541,24 @@ private boolean finishInitialization( } else if ( subtype instanceof AnyType anyType ) { final var bootValueMapping = (Any) value; - - final var propertyAccess = representationStrategy.resolvePropertyAccess( bootPropertyDescriptor ); - final boolean nullable = bootValueMapping.isNullable(); - final boolean insertable = insertability[columnPosition]; - final boolean updateable = updateability[columnPosition]; - final boolean includeInOptimisticLocking = bootPropertyDescriptor.isOptimisticLocked(); - final var cascadeStyle = compositeType.getCascadeStyle( attributeIndex ); - - SimpleAttributeMetadata attributeMetadataAccess = new SimpleAttributeMetadata( + final var propertyAccess = + representationStrategy.resolvePropertyAccess( bootPropertyDescriptor ); + final var attributeMetadataAccess = new SimpleAttributeMetadata( propertyAccess, - getMutabilityPlan( updateable ), - nullable, - insertable, - updateable, - includeInOptimisticLocking, + getMutabilityPlan( updateability[columnPosition] ), + bootValueMapping.isNullable(), + insertability[columnPosition], + updateability[columnPosition], + bootPropertyDescriptor.isOptimisticLocked(), true, - cascadeStyle + compositeType.getCascadeStyle( attributeIndex ) ); attributeMapping = new DiscriminatedAssociationAttributeMapping( - valueMapping.getNavigableRole().append( bootPropertyDescriptor.getName() ), - typeConfiguration.getJavaTypeRegistry().resolveDescriptor( Object.class ), + valueMapping.getNavigableRole() + .append( bootPropertyDescriptor.getName() ), + typeConfiguration.getJavaTypeRegistry() + .resolveDescriptor( Object.class ), this, attributeIndex, attributeIndex, @@ -572,7 +572,7 @@ else if ( subtype instanceof AnyType anyType ) { ); } else if ( subtype instanceof CompositeType subCompositeType ) { - final int columnSpan = subCompositeType.getColumnSpan( creationProcess.getCreationContext().getMetadata() ); + final int columnSpan = subCompositeType.getColumnSpan( creationContext.getMetadata() ); final String subTableExpression; final String[] subRootTableKeyColumnNames; if ( rootTableKeyColumnNames == null ) { @@ -582,10 +582,10 @@ else if ( subtype instanceof CompositeType subCompositeType ) { else { subTableExpression = rootTableExpression; subRootTableKeyColumnNames = new String[columnSpan]; - System.arraycopy( rootTableKeyColumnNames, columnPosition, subRootTableKeyColumnNames, 0, columnSpan ); + arraycopy( rootTableKeyColumnNames, columnPosition, subRootTableKeyColumnNames, 0, columnSpan ); } - attributeMapping = MappingModelCreationHelper.buildEmbeddedAttributeMapping( + attributeMapping = buildEmbeddedAttributeMapping( bootPropertyDescriptor.getName(), attributeIndex, attributeIndex, @@ -604,7 +604,7 @@ else if ( subtype instanceof CompositeType subCompositeType ) { columnPosition += columnSpan; } else if ( subtype instanceof CollectionType ) { - attributeMapping = MappingModelCreationHelper.buildPluralAttributeMapping( + attributeMapping = buildPluralAttributeMapping( bootPropertyDescriptor.getName(), attributeIndex, attributeIndex, @@ -617,7 +617,7 @@ else if ( subtype instanceof CollectionType ) { ); } else if ( subtype instanceof EntityType subentityType ) { - attributeMapping = MappingModelCreationHelper.buildSingularAssociationAttributeMapping( + attributeMapping = buildSingularAssociationAttributeMapping( bootPropertyDescriptor.getName(), valueMapping.getNavigableRole().append( bootPropertyDescriptor.getName() ), attributeIndex, @@ -708,7 +708,7 @@ public Object assemble(Serializable cached, SharedSessionContract session) { private EmbeddableDiscriminatorMapping generateDiscriminatorMapping( Component bootDescriptor, RuntimeModelCreationContext creationContext) { - final Value discriminator = bootDescriptor.getDiscriminator(); + final var discriminator = bootDescriptor.getDiscriminator(); if ( discriminator == null ) { return null; } @@ -723,7 +723,7 @@ private EmbeddableDiscriminatorMapping generateDiscriminatorMapping( final Integer scale; final boolean isFormula = discriminator.hasFormula(); if ( isFormula ) { - final Formula formula = (Formula) selectable; + final var formula = (Formula) selectable; discriminatorColumnExpression = name = formula.getTemplate( creationContext.getDialect(), creationContext.getTypeConfiguration() @@ -735,7 +735,7 @@ private EmbeddableDiscriminatorMapping generateDiscriminatorMapping( scale = null; } else { - final Column column = discriminator.getColumns().get( 0 ); + final var column = discriminator.getColumns().get( 0 ); assert column != null : "Embeddable discriminators require a column"; discriminatorColumnExpression = column.getReadExpr( creationContext.getDialect() ); columnDefinition = column.getSqlType(); @@ -1097,7 +1097,7 @@ public void forEachUpdatable(int offset, SelectableConsumer consumer) { else { final int jdbcTypeCount = selectableMappings.getJdbcTypeCount(); for ( int i = 0; i < jdbcTypeCount; i++ ) { - final SelectableMapping selectable = selectableMappings.getSelectable( i ); + final var selectable = selectableMappings.getSelectable( i ); if ( selectable.isUpdateable() ) { consumer.accept( offset + i, selectable ); } 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 68e0aab1a2f6..f892b95e7e8a 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 @@ -153,7 +153,9 @@ public EmbeddedAttributeMapping( : null ); - navigableRole = inverseModelPart.getNavigableRole().getParent().append( inverseModelPart.getFetchableName() ); + navigableRole = + inverseModelPart.getNavigableRole().getParent() + .append( inverseModelPart.getFetchableName() ); tableExpression = selectableMappings.getSelectable( 0 ).getContainingTableExpression(); embeddableMappingType = embeddableTypeDescriptor.createInverseMappingType( @@ -386,18 +388,20 @@ public boolean isSelectable() { @Override public boolean containsTableReference(String tableExpression) { - final var declaringType = getDeclaringType(); - final TableGroupProducer producer; + return tableGroupProducer( getDeclaringType() ) + .containsTableReference( tableExpression ); + } + + private static TableGroupProducer tableGroupProducer(ManagedMappingType declaringType) { if ( declaringType instanceof TableGroupProducer tableGroupProducer ) { - producer = tableGroupProducer; + return tableGroupProducer; } else if ( declaringType instanceof EmbeddableMappingType embeddableMappingType ) { - producer = embeddableMappingType.getEmbeddedValueMapping(); + return embeddableMappingType.getEmbeddedValueMapping(); } else { throw new AssertionFailure( "Unexpected declaring type" ); } - return producer.containsTableReference( tableExpression ); } @Override 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 c5d19af063b7..55f568eca153 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 @@ -28,7 +28,6 @@ import org.hibernate.sql.ast.SqlAstJoinType; import org.hibernate.sql.ast.spi.SqlAliasBase; import org.hibernate.sql.ast.spi.SqlAstCreationState; -import org.hibernate.sql.ast.spi.SqlExpressionResolver; import org.hibernate.sql.ast.spi.SqlSelection; import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.SqlTuple; @@ -199,8 +198,7 @@ public SqlTuple toSqlExpression( Clause clause, SqmToSqlAstConverter walker, SqlAstCreationState sqlAstCreationState) { - final SqlExpressionResolver sqlExpressionResolver = sqlAstCreationState.getSqlExpressionResolver(); - + final var sqlExpressionResolver = sqlAstCreationState.getSqlExpressionResolver(); final List expressions = new ArrayList<>(); getEmbeddableTypeDescriptor().forEachSelectable( (columnIndex, selection) -> { @@ -244,7 +242,7 @@ public TableGroupJoin createTableGroupJoin( creationState ); - return new TableGroupJoin( navigablePath, joinType, tableGroup, null ); + return new TableGroupJoin( navigablePath, joinType, tableGroup ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EmbeddedForeignKeyDescriptor.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EmbeddedForeignKeyDescriptor.java index ba2b89492cc7..cde0a6e48820 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EmbeddedForeignKeyDescriptor.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EmbeddedForeignKeyDescriptor.java @@ -34,10 +34,8 @@ import org.hibernate.sql.ast.spi.SqlAstCreationState; import org.hibernate.sql.ast.spi.SqlSelection; import org.hibernate.sql.ast.tree.expression.ColumnReference; -import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.from.OneToManyTableGroup; 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.from.VirtualTableGroup; @@ -50,6 +48,8 @@ import org.hibernate.sql.results.graph.embeddable.internal.EmbeddableForeignKeyResultImpl; import org.hibernate.type.descriptor.java.JavaType; +import static org.hibernate.metamodel.mapping.internal.MappingModelCreationHelper.createInverseModelPart; + /** * @author Andrea Boriero */ @@ -100,7 +100,8 @@ public EmbeddedForeignKeyDescriptor( this.targetSelectableMappings = targetSelectableMappings; this.targetSide = new EmbeddedForeignKeyDescriptorSide( Nature.TARGET, targetMappingType ); this.keySide = new EmbeddedForeignKeyDescriptorSide( Nature.KEY, keyMappingType ); - final List columns = new ArrayList<>( keySelectableMappings.getJdbcTypeCount() ); + final List columns = + new ArrayList<>( keySelectableMappings.getJdbcTypeCount() ); keySelectableMappings.forEachSelectable( (columnIndex, selection) -> columns.add( selection.getSelectionExpression() ) ); @@ -132,7 +133,7 @@ private EmbeddedForeignKeyDescriptor( this.targetSide = original.targetSide; this.keySide = new EmbeddedForeignKeyDescriptorSide( Nature.KEY, - MappingModelCreationHelper.createInverseModelPart( + createInverseModelPart( original.targetSide.getModelPart(), keyDeclaringType, keyDeclaringTableGroupProducer, @@ -140,11 +141,10 @@ private EmbeddedForeignKeyDescriptor( creationProcess ) ); - final List columns = new ArrayList<>( keySelectableMappings.getJdbcTypeCount() ); + final List columns = + new ArrayList<>( keySelectableMappings.getJdbcTypeCount() ); keySelectableMappings.forEachSelectable( - (columnIndex, selection) -> { - columns.add( selection.getSelectionExpression() ); - } + (columnIndex, selection) -> columns.add( selection.getSelectionExpression() ) ); this.associationKey = new AssociationKey( keyTable, columns ); this.hasConstraint = original.hasConstraint; @@ -156,10 +156,7 @@ private EmbeddedForeignKeyDescriptor(EmbeddedForeignKeyDescriptor original, Embe this.keySide = original.keySide; this.targetTable = targetPart.getContainingTableExpression(); this.targetSelectableMappings = targetPart; - this.targetSide = new EmbeddedForeignKeyDescriptorSide( - Nature.TARGET, - targetPart - ); + this.targetSide = new EmbeddedForeignKeyDescriptorSide( Nature.TARGET, targetPart ); this.associationKey = original.associationKey; this.hasConstraint = original.hasConstraint; } @@ -317,19 +314,17 @@ public DomainResult createDomainResult( private boolean isTargetTableGroup(TableGroup tableGroup) { tableGroup = getUnderlyingTableGroup( tableGroup ); - final TableGroupProducer tableGroupProducer; - if ( tableGroup instanceof OneToManyTableGroup oneToManyTableGroup ) { - tableGroupProducer = (TableGroupProducer) oneToManyTableGroup.getElementTableGroup().getModelPart(); - } - else { - tableGroupProducer = (TableGroupProducer) tableGroup.getModelPart(); - } + final var tableGroupProducer = + tableGroup instanceof OneToManyTableGroup oneToManyTableGroup + ? (TableGroupProducer) oneToManyTableGroup.getElementTableGroup().getModelPart() + : (TableGroupProducer) tableGroup.getModelPart(); return tableGroupProducer.containsTableReference( targetSide.getModelPart().getContainingTableExpression() ); } private static TableGroup getUnderlyingTableGroup(TableGroup tableGroup) { if ( tableGroup.isVirtual() ) { - tableGroup = getUnderlyingTableGroup( ( (VirtualTableGroup) tableGroup ).getUnderlyingTableGroup() ); + final var virtualTableGroup = (VirtualTableGroup) tableGroup; + tableGroup = getUnderlyingTableGroup( virtualTableGroup.getUnderlyingTableGroup() ); } return tableGroup; } @@ -372,7 +367,7 @@ private DomainResult createDomainResult( creationState.getSqlAstCreationState().getFromClauseAccess().resolveTableGroup( resultNavigablePath, np -> { - final TableGroupJoin tableGroupJoin = modelPart.createTableGroupJoin( + final var tableGroupJoin = modelPart.createTableGroupJoin( resultNavigablePath, tableGroup, null, @@ -387,7 +382,8 @@ private DomainResult createDomainResult( } ); - final Nature currentForeignKeyResolvingKey = creationState.getCurrentlyResolvingForeignKeyPart(); + final Nature currentForeignKeyResolvingKey = + creationState.getCurrentlyResolvingForeignKeyPart(); try { creationState.setCurrentlyResolvingForeignKeyPart( nature ); return new EmbeddableForeignKeyResultImpl<>( @@ -408,15 +404,14 @@ public Predicate generateJoinPredicate( TableGroup targetSideTableGroup, TableGroup keySideTableGroup, SqlAstCreationState creationState) { - final TableReference lhsTableReference = targetSideTableGroup.resolveTableReference( + final var lhsTableReference = targetSideTableGroup.resolveTableReference( targetSideTableGroup.getNavigablePath(), targetTable ); - final TableReference rhsTableKeyReference = keySideTableGroup.resolveTableReference( + final var rhsTableKeyReference = keySideTableGroup.resolveTableReference( null, keyTable ); - return generateJoinPredicate( lhsTableReference, rhsTableKeyReference, creationState ); } @@ -428,7 +423,7 @@ public Predicate generateJoinPredicate( final var predicate = new Junction( Junction.Nature.CONJUNCTION ); targetSelectableMappings.forEachSelectable( (i, selection) -> { - final ComparisonPredicate comparisonPredicate = new ComparisonPredicate( + final var comparisonPredicate = new ComparisonPredicate( new ColumnReference( targetSideReference, selection ), ComparisonOperator.EQUAL, new ColumnReference( keySideReference, keySelectableMappings.getSelectable( i ) ) @@ -460,9 +455,9 @@ public boolean isSimpleJoinPredicate(Predicate predicate) { if ( comparisonPredicate.getOperator() != ComparisonOperator.EQUAL ) { return false; } - final Expression lhsExpr = comparisonPredicate.getLeftHandExpression(); - final Expression rhsExpr = comparisonPredicate.getRightHandExpression(); - if ( !(lhsExpr instanceof ColumnReference lhs) || !(rhsExpr instanceof ColumnReference rhs) ) { + final var lhsExpr = comparisonPredicate.getLeftHandExpression(); + final var rhsExpr = comparisonPredicate.getRightHandExpression(); + if ( !( lhsExpr instanceof ColumnReference lhs ) || !( rhsExpr instanceof ColumnReference rhs ) ) { return false; } if ( lhsIsKey == null ) { diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EmbeddedIdentifierMappingImpl.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EmbeddedIdentifierMappingImpl.java index 0ed88928c633..3c9cf6ff83c4 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EmbeddedIdentifierMappingImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EmbeddedIdentifierMappingImpl.java @@ -13,13 +13,13 @@ import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.JdbcMapping; import org.hibernate.property.access.spi.PropertyAccess; -import org.hibernate.proxy.HibernateProxy; -import org.hibernate.proxy.LazyInitializer; import org.hibernate.spi.NavigablePath; import org.hibernate.sql.ast.spi.SqlSelection; import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.results.graph.DomainResultCreationState; +import static org.hibernate.proxy.HibernateProxy.extractLazyInitializer; + /** * Support for {@link jakarta.persistence.EmbeddedId} * @@ -78,12 +78,13 @@ public void applySqlSelections( TableGroup tableGroup, DomainResultCreationState creationState, BiConsumer selectionConsumer) { - getEmbeddableTypeDescriptor().applySqlSelections( navigablePath, tableGroup, creationState, selectionConsumer ); + getEmbeddableTypeDescriptor() + .applySqlSelections( navigablePath, tableGroup, creationState, selectionConsumer ); } @Override public Object getIdentifier(Object entity) { - final LazyInitializer lazyInitializer = HibernateProxy.extractLazyInitializer( entity ); + final var lazyInitializer = extractLazyInitializer( entity ); if ( lazyInitializer != null ) { return lazyInitializer.getInternalIdentifier(); } 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 e8f5a3062546..e5cd89f47096 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 @@ -294,7 +294,6 @@ public Fetch generateFetch( fetchablePath, this, fetchTiming, - creationState, !sqlSelection.isVirtual() ); } @@ -306,7 +305,6 @@ public DomainResult createDomainResult( String resultVariable, DomainResultCreationState creationState) { final SqlSelection sqlSelection = resolveSqlSelection( tableGroup, creationState ); - return new BasicResult<>( sqlSelection.getValuesArrayPosition(), resultVariable, diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ExplicitColumnDiscriminatorMappingImpl.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ExplicitColumnDiscriminatorMappingImpl.java index 4cdafde04d55..af4857008864 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ExplicitColumnDiscriminatorMappingImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ExplicitColumnDiscriminatorMappingImpl.java @@ -124,16 +124,13 @@ public Expression resolveSqlExpression( JdbcMapping jdbcMappingToUse, TableGroup tableGroup, SqlAstCreationState creationState) { - final var expressionResolver = creationState.getSqlExpressionResolver(); final var tableReference = tableGroup.resolveTableReference( navigablePath, tableExpression ); - return expressionResolver.resolveSqlExpression( - createColumnReferenceKey( - tableGroup.getPrimaryTableReference(), - getSelectionExpression(), - jdbcMappingToUse - ), - processingState -> new ColumnReference( tableReference, this ) - ); + return creationState.getSqlExpressionResolver() + .resolveSqlExpression( + createColumnReferenceKey( tableGroup.getPrimaryTableReference(), + getSelectionExpression(), jdbcMappingToUse ), + processingState -> new ColumnReference( tableReference, this ) + ); } @Override 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 9b8473a974d8..472ff041945a 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 @@ -4,9 +4,6 @@ */ package org.hibernate.metamodel.mapping.internal; -import java.util.ArrayList; -import java.util.List; - import org.hibernate.Incubating; import org.hibernate.LockMode; import org.hibernate.LockOptions; @@ -23,13 +20,17 @@ import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.query.spi.QueryOptions; +import org.hibernate.sql.ast.spi.SqlAliasBaseManager; import org.hibernate.sql.ast.tree.select.SelectStatement; import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl; -import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.exec.spi.JdbcParametersList; +import org.hibernate.sql.exec.spi.JdbcSelect; import org.hibernate.sql.results.internal.RowTransformerArrayImpl; +import java.util.ArrayList; +import java.util.List; + import static org.hibernate.sql.results.spi.ListResultsConsumer.UniqueSemantic.FILTER; /** @@ -51,7 +52,7 @@ @Incubating public class GeneratedValuesProcessor { private final SelectStatement selectStatement; - private final JdbcOperationQuerySelect jdbcSelect; + private final JdbcSelect jdbcSelect; private final List generatedValuesToSelect; private final JdbcParametersList jdbcParameters; @@ -81,6 +82,7 @@ public GeneratedValuesProcessor( new LoadQueryInfluencers( sessionFactory ), new LockOptions( LockMode.READ ), builder::add, + new SqlAliasBaseManager(), sessionFactory ); jdbcSelect = @@ -182,7 +184,7 @@ private List executeSelect(Object id, SharedSessionContractImplementor private JdbcParameterBindings getJdbcParameterBindings(Object id, SharedSessionContractImplementor session) { final var jdbcParamBindings = new JdbcParameterBindingsImpl( jdbcParameters.size() ); - int offset = jdbcParamBindings.registerParametersForEachJdbcValue( + final int offset = jdbcParamBindings.registerParametersForEachJdbcValue( id, entityDescriptor.getIdentifierMapping(), jdbcParameters, @@ -217,7 +219,7 @@ public EntityMappingType getEntityDescriptor() { return entityDescriptor; } - public JdbcOperationQuerySelect getJdbcSelect() { + public JdbcSelect getJdbcSelect() { return jdbcSelect; } } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/IdClassEmbeddable.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/IdClassEmbeddable.java index 266b7e0d32a2..bfc8328ba1ed 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/IdClassEmbeddable.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/IdClassEmbeddable.java @@ -34,6 +34,8 @@ import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.spi.CompositeTypeImplementor; +import static org.hibernate.metamodel.mapping.internal.MappingModelCreationHelper.getTableIdentifierExpression; + /** * EmbeddableMappingType implementation describing an {@link jakarta.persistence.IdClass} */ @@ -128,7 +130,7 @@ public IdClassEmbeddable( this.virtualIdEmbeddable = (VirtualIdEmbeddable) valueMapping.getEmbeddableTypeDescriptor(); this.javaType = inverseMappingType.javaType; this.representationStrategy = new IdClassRepresentationStrategy( this, false, () -> { - final String[] attributeNames = new String[inverseMappingType.getNumberOfAttributeMappings()]; + final var attributeNames = new String[inverseMappingType.getNumberOfAttributeMappings()]; for ( int i = 0; i < attributeNames.length; i++ ) { attributeNames[i] = inverseMappingType.getAttributeMapping( i ).getAttributeName(); } @@ -137,7 +139,9 @@ public IdClassEmbeddable( this.embedded = valueMapping; this.selectableMappings = selectableMappings; creationProcess.registerInitializationCallback( - "IdClassEmbeddable(" + inverseMappingType.getNavigableRole().getFullPath() + ".{inverse})#finishInitialization", + "IdClassEmbeddable(" + + inverseMappingType.getNavigableRole().getFullPath() + + ".{inverse})#finishInitialization", () -> inverseInitializeCallback( declaringTableGroupProducer, selectableMappings, @@ -332,7 +336,7 @@ private boolean finishInitialization( throw new IllegalAttributeType( "An IdClass cannot define attributes : " + attributeName ); } }, - (column, jdbcEnvironment) -> MappingModelCreationHelper.getTableIdentifierExpression( column.getValue().getTable(), creationProcess ), + (column, jdbcEnvironment) -> getTableIdentifierExpression( column.getValue().getTable(), creationProcess ), this::addAttribute, () -> { // We need the attribute mapping types to finish initialization first before we can build the column mappings 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 6de2d203f13d..aa848c1f1bef 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 @@ -52,6 +52,7 @@ import static java.util.Objects.requireNonNullElse; import static org.hibernate.internal.util.StringHelper.isNotEmpty; import static org.hibernate.metamodel.mapping.internal.MappingModelCreationHelper.createInverseModelPart; +import static org.hibernate.metamodel.mapping.internal.MappingModelCreationHelper.getCollectionPropertyPath; import static org.hibernate.metamodel.mapping.internal.MappingModelCreationHelper.getPropertyOrder; /** @@ -266,14 +267,9 @@ public TableGroupJoin createTableGroupJoin( creationState ); - final var join = new TableGroupJoin( - navigablePath, - joinType, - lazyTableGroup, - null - ); + final var join = new TableGroupJoin( navigablePath, joinType, lazyTableGroup ); - lazyTableGroup.setTableGroupInitializerCallback( (partTableGroup) -> { + lazyTableGroup.setTableGroupInitializerCallback( partTableGroup -> { // `partTableGroup` is the association table group join.applyPredicate( foreignKey.generateJoinPredicate( @@ -322,7 +318,9 @@ public LazyTableGroup createRootTableGroupJoin( this, explicitSourceAlias, sqlAliasBase, - creationState.getCreationContext().getSessionFactory(), + creationState.getCreationContext() + // TODO: FIXME + .getSessionFactory(), lhs ); @@ -423,7 +421,12 @@ else if ( isNotEmpty( bootCollectionDescriptor.getMappedByProperty() ) ) { return false; } - foreignKey = createJoinTablePartForeignKey( collectionTableName, elementDescriptor, creationProcess ); + foreignKey = createJoinTablePartForeignKey( + collectionTableName, + elementDescriptor, + getCollectionPropertyPath( collectionDescriptor ), + creationProcess + ); creationProcess.registerForeignKey( this, foreignKey ); } else { @@ -450,6 +453,7 @@ else if ( isNotEmpty( bootCollectionDescriptor.getMappedByProperty() ) ) { element, (EntityType) collectionDescriptor.getElementType(), fkTargetModelPart, + getCollectionPropertyPath( collectionDescriptor ), creationProcess, collectionDescriptor.getFactory().getJdbcServices().getDialect() ); @@ -461,6 +465,7 @@ else if ( isNotEmpty( bootCollectionDescriptor.getMappedByProperty() ) ) { index, (EntityType) collectionDescriptor.getIndexType(), fkTargetModelPart, + null, // No @MapKeyFormula or @OrderFormula creationProcess, collectionDescriptor.getFactory().getJdbcServices().getDialect() ); @@ -472,6 +477,7 @@ else if ( isNotEmpty( bootCollectionDescriptor.getMappedByProperty() ) ) { private ForeignKeyDescriptor createJoinTablePartForeignKey( String collectionTableName, ManyToOne elementBootDescriptor, + @Nullable String propertyPath, MappingModelCreationProcess creationProcess) { final var associatedEntityMapping = getAssociatedEntityMappingType(); final var associatedIdMapping = associatedEntityMapping.getIdentifierMapping(); @@ -492,11 +498,14 @@ private ForeignKeyDescriptor createJoinTablePartForeignKey( final var keySelectableMapping = SelectableMappingImpl.from( collectionTableName, keyColumn, + propertyPath, + null, targetModelPart.getJdbcMapping(), creationProcess.getCreationContext().getTypeConfiguration(), true, false, false, + false, creationProcess.getCreationContext().getDialect(), creationProcess.getSqmFunctionRegistry(), creationProcess.getCreationContext() @@ -530,6 +539,8 @@ private ForeignKeyDescriptor createJoinTablePartForeignKey( collectionTableName, elementBootDescriptor, getPropertyOrder( elementBootDescriptor, creationProcess ), + propertyPath, + null, creationProcess.getCreationContext().getMetadata(), creationProcess.getCreationContext().getTypeConfiguration(), elementBootDescriptor.getColumnInsertability(), @@ -586,6 +597,7 @@ private ForeignKeyDescriptor createForeignKeyDescriptor( Value fkBootDescriptorSource, EntityType entityType, ModelPart fkTargetModelPart, + @Nullable String propertyPath, MappingModelCreationProcess creationProcess, Dialect dialect) { assert fkTargetModelPart != null; @@ -598,6 +610,7 @@ private ForeignKeyDescriptor createForeignKeyDescriptor( return determineForeignKey( toOneAttributeMapping.getForeignKeyDescriptor(), fkBootDescriptorSource, + propertyPath, creationProcess ); } @@ -610,6 +623,7 @@ private ForeignKeyDescriptor createForeignKeyDescriptor( return determineForeignKey( targetModelPart.getForeignKeyDescriptor(), fkBootDescriptorSource, + propertyPath, creationProcess ); } @@ -626,16 +640,19 @@ private ForeignKeyDescriptor createForeignKeyDescriptor( creationProcess, dialect, collectionTableName, - basicFkTarget + basicFkTarget, + propertyPath ); } if ( fkTargetModelPart instanceof EmbeddableValuedModelPart embeddableValuedModelPart ) { return MappingModelCreationHelper.buildEmbeddableForeignKeyDescriptor( + propertyPath, embeddableValuedModelPart, fkBootDescriptorSource, findContainingEntityMapping(), getCollectionDescriptor().getAttributeMapping(), + null, false, fkBootDescriptorSource.getColumnInsertability(), fkBootDescriptorSource.getColumnUpdateability(), @@ -652,6 +669,7 @@ private ForeignKeyDescriptor createForeignKeyDescriptor( private ForeignKeyDescriptor determineForeignKey( ForeignKeyDescriptor foreignKeyDescriptor, Value fkBootDescriptorSource, + @Nullable String propertyPath, MappingModelCreationProcess creationProcess) { final int selectableCount = foreignKeyDescriptor.getJdbcTypeCount(); final var keyPart = foreignKeyDescriptor.getKeyPart(); @@ -666,6 +684,8 @@ private ForeignKeyDescriptor determineForeignKey( keyPart.getContainingTableExpression(), fkBootDescriptorSource, getPropertyOrder( fkBootDescriptorSource, creationProcess ), + propertyPath, + null, creationProcess.getCreationContext().getMetadata(), creationProcess.getCreationContext().getTypeConfiguration(), fkBootDescriptorSource.getColumnInsertability(), @@ -692,7 +712,8 @@ private SimpleForeignKeyDescriptor createSimpleForeignKeyDescriptor( MappingModelCreationProcess creationProcess, Dialect dialect, String fkKeyTableName, - BasicValuedModelPart basicFkTargetPart) { + BasicValuedModelPart basicFkTargetPart, + @Nullable String propertyPath) { final boolean columnInsertable; final boolean columnUpdateable; if ( getNature() == Nature.ELEMENT && !fkBootDescriptorSource.getSelectables().get( 0 ).isFormula() ) { @@ -708,11 +729,14 @@ private SimpleForeignKeyDescriptor createSimpleForeignKeyDescriptor( final var keySelectableMapping = SelectableMappingImpl.from( fkKeyTableName, fkBootDescriptorSource.getSelectables().get( 0 ), + propertyPath, + null, basicFkTargetPart.getJdbcMapping(), creationProcess.getCreationContext().getTypeConfiguration(), columnInsertable, columnUpdateable, fkValue.isPartitionKey(), + false, dialect, creationProcess.getSqmFunctionRegistry(), creationProcess.getCreationContext() 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 212fe6538e8d..b436609627ca 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 @@ -43,6 +43,7 @@ import org.hibernate.mapping.PersistentClass; import org.hibernate.mapping.Property; import org.hibernate.mapping.Resolvable; +import org.hibernate.mapping.Selectable; import org.hibernate.mapping.SimpleValue; import org.hibernate.mapping.SortableValue; import org.hibernate.mapping.Table; @@ -575,12 +576,15 @@ public static PluralAttributeMapping buildPluralAttributeMapping( final var index = (BasicValue) ( (IndexedCollection) bootValueMapping ).getIndex(); final var selectableMapping = SelectableMappingImpl.from( tableExpression, - index.getSelectables().get(0), + index.getSelectables().get( 0 ), + null, // No support for @OrderFormula + null, creationContext.getTypeConfiguration().getBasicTypeForJavaType( Integer.class ), creationProcess.getCreationContext().getTypeConfiguration(), index.isColumnInsertable( 0 ), index.isColumnUpdateable( 0 ), false, + false, dialect, creationProcess.getSqmFunctionRegistry(), creationProcess.getCreationContext() @@ -627,12 +631,15 @@ public static PluralAttributeMapping buildPluralAttributeMapping( final var index = (BasicValue) ( (IndexedCollection) bootValueMapping ).getIndex(); final var selectableMapping = SelectableMappingImpl.from( tableExpression, - index.getSelectables().get(0), + index.getSelectables().get( 0 ), + null, // No support for @OrderFormula + null, creationContext.getTypeConfiguration().getBasicTypeForJavaType( Integer.class ), creationProcess.getCreationContext().getTypeConfiguration(), index.isColumnInsertable( 0 ), index.isColumnUpdateable( 0 ), false, + false, dialect, creationProcess.getSqmFunctionRegistry(), creationProcess.getCreationContext() @@ -835,12 +842,16 @@ private static void interpretPluralAttributeMappingKeyDescriptor( final String keyTableExpression = collectionTableName;//getTableIdentifierExpression( bootValueMappingKey.getTable(), creationProcess ); final var keySelectableMapping = SelectableMappingImpl.from( keyTableExpression, - bootValueMappingKey.getSelectables().get(0), + bootValueMappingKey.getSelectables().get( 0 ), + // In the annotation model it is not possible to have a @MapKeyFormula, but hbm.xml supports this + getPropertyPath( attributeMapping.getNavigableRole() ) + "." + CollectionPart.Nature.INDEX.getName(), + null, (JdbcMapping) keyType, creationProcess.getCreationContext().getTypeConfiguration(), bootValueMappingKey.isColumnInsertable( 0 ), bootValueMappingKey.isColumnUpdateable( 0 ), false, + false, dialect, creationProcess.getSqmFunctionRegistry(), creationProcess.getCreationContext() @@ -858,6 +869,7 @@ private static void interpretPluralAttributeMappingKeyDescriptor( } else if ( fkTargetPart instanceof EmbeddableValuedModelPart embeddableValuedModelPart ) { final var keyDescriptor = buildEmbeddableForeignKeyDescriptor( + getPropertyPath( attributeMapping.getNavigableRole() ), embeddableValuedModelPart, bootValueMapping, keyDeclaringType, @@ -880,6 +892,22 @@ else if ( fkTargetPart instanceof EmbeddableValuedModelPart embeddableValuedMode } } + static String getCollectionPropertyPath(CollectionPersister collectionPersister) { + return collectionPersister.getNavigableRole().getFullPath().substring( collectionPersister.getOwnerEntityPersister().getNavigableRole().getFullPath().length() + 1 ); + } + + static String getPropertyPath(NavigableRole navigableRole) { + final StringBuilder propertyPath = new StringBuilder(); + while (navigableRole.getParent() != null) { + if ( !propertyPath.isEmpty() ) { + propertyPath.insert( 0, "." ); + } + propertyPath.insert( 0, navigableRole.getLocalName() ); + navigableRole = navigableRole.getParent(); + } + return propertyPath.toString(); + } + /** * Tries to {@link ToOneAttributeMapping#setForeignKeyDescriptor} * to the given attribute {@code attributeMapping}. @@ -951,10 +979,12 @@ else if ( modelPart instanceof ToOneAttributeMapping toOneAttributeMapping ) { } else if ( modelPart instanceof EmbeddableValuedModelPart embeddableValuedModelPart ) { final var embeddedForeignKeyDescriptor = buildEmbeddableForeignKeyDescriptor( + getPropertyPath( attributeMapping.getNavigableRole() ), embeddableValuedModelPart, bootValueMapping, attributeMapping.getDeclaringType(), attributeMapping.findContainingEntityMapping(), + null, true, bootValueMapping.getColumnInsertability(), bootValueMapping.getColumnUpdateability(), @@ -1009,15 +1039,18 @@ else if ( modelPart instanceof EmbeddableValuedModelPart embeddableValuedModelPa int i = 0; final Value value = bootProperty.getValue(); if ( columnIterator.hasNext() ) { + final Selectable selectable = columnIterator.next(); keySelectableMapping = SelectableMappingImpl.from( tableExpression, - columnIterator.next(), + selectable, + selectable.isFormula() ? getPropertyPath( attributeMapping.getNavigableRole() ) : null, parentSelectablePath, simpleFkTarget.getJdbcMapping(), creationProcess.getCreationContext().getTypeConfiguration(), value.isColumnInsertable( i ), value.isColumnUpdateable( i ), value.isPartitionKey(), + false, dialect, creationProcess.getSqmFunctionRegistry(), creationProcess.getCreationContext() @@ -1029,12 +1062,14 @@ else if ( modelPart instanceof EmbeddableValuedModelPart embeddableValuedModelPa keySelectableMapping = SelectableMappingImpl.from( tableExpression, table.getPrimaryKey().getColumn( 0 ), + null, // Primary key selectable must always be a column parentSelectablePath, simpleFkTarget.getJdbcMapping(), creationProcess.getCreationContext().getTypeConfiguration(), value.isColumnInsertable( 0 ), value.isColumnUpdateable( 0 ), value.isPartitionKey(), + false, dialect, creationProcess.getSqmFunctionRegistry(), creationProcess.getCreationContext() @@ -1057,10 +1092,12 @@ else if ( modelPart instanceof EmbeddableValuedModelPart embeddableValuedModelPa else if ( fkTarget instanceof EmbeddableValuedModelPart embeddableValuedModelPart ) { final var value = bootProperty.getValue(); final var embeddedForeignKeyDescriptor = buildEmbeddableForeignKeyDescriptor( + getPropertyPath( attributeMapping.getNavigableRole() ), embeddableValuedModelPart, bootValueMapping, attributeMapping.getDeclaringType(), attributeMapping.findContainingEntityMapping(), + null, swapDirection, value.getColumnInsertability(), value.getColumnUpdateability(), @@ -1131,6 +1168,7 @@ else if ( modelPart instanceof EmbeddableValuedModelPart embeddableValuedModelPa return false; } + @Deprecated(forRemoval = true) public static EmbeddedForeignKeyDescriptor buildEmbeddableForeignKeyDescriptor( EmbeddableValuedModelPart embeddableValuedModelPart, Value bootValueMapping, @@ -1142,6 +1180,7 @@ public static EmbeddedForeignKeyDescriptor buildEmbeddableForeignKeyDescriptor( Dialect dialect, MappingModelCreationProcess creationProcess) { return buildEmbeddableForeignKeyDescriptor( + null, embeddableValuedModelPart, bootValueMapping, keyDeclaringType, @@ -1155,7 +1194,8 @@ public static EmbeddedForeignKeyDescriptor buildEmbeddableForeignKeyDescriptor( ); } - private static EmbeddedForeignKeyDescriptor buildEmbeddableForeignKeyDescriptor( + public static EmbeddedForeignKeyDescriptor buildEmbeddableForeignKeyDescriptor( + String propertyPath, EmbeddableValuedModelPart embeddableValuedModelPart, Value bootValueMapping, ManagedMappingType keyDeclaringType, @@ -1179,6 +1219,7 @@ private static EmbeddedForeignKeyDescriptor buildEmbeddableForeignKeyDescriptor( keyTableExpression, collectionBootValueMapping.getKey(), getPropertyOrder( bootValueMapping, creationProcess ), + propertyPath, parentSelectablePath, creationProcess.getCreationContext().getMetadata(), creationProcess.getCreationContext().getTypeConfiguration(), @@ -1204,6 +1245,7 @@ private static EmbeddedForeignKeyDescriptor buildEmbeddableForeignKeyDescriptor( keyTableExpression, bootValueMapping, getPropertyOrder( bootValueMapping, creationProcess ), + propertyPath, parentSelectablePath, creationProcess.getCreationContext().getMetadata(), creationProcess.getCreationContext().getTypeConfiguration(), @@ -1386,11 +1428,15 @@ private static CollectionPart interpretMapKey( final var selectableMapping = SelectableMappingImpl.from( tableExpression, basicValue.getSelectables().get( 0 ), + // In the annotation model it is not possible to have a @MapKeyFormula, but hbm.xml supports this + getCollectionPropertyPath( collectionDescriptor ) + "." + CollectionPart.Nature.INDEX.getName(), + null, basicValue.resolve().getJdbcMapping(), creationProcess.getCreationContext().getTypeConfiguration(), insertable, updatable, false, + false, dialect, creationProcess.getSqmFunctionRegistry(), creationProcess.getCreationContext() @@ -1478,7 +1524,11 @@ private static CollectionPart interpretElement( if ( element instanceof BasicValue basicElement ) { final var selectableMapping = SelectableMappingImpl.from( tableExpression, - basicElement.getSelectables().get(0), + basicElement.getSelectables().get( 0 ), + basicElement.getSelectables().get( 0 ).isFormula() + ? getCollectionPropertyPath( collectionDescriptor ) + : null, + null, basicElement.resolve().getJdbcMapping(), creationProcess.getCreationContext().getTypeConfiguration(), basicElement.isColumnInsertable( 0 ), 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 b81f892f4ff0..7dff3a5b2219 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 @@ -65,12 +65,12 @@ public NonAggregatedIdentifierMappingImpl( super( entityPersister, rootTableName, creationProcess ); entityDescriptor = entityPersister; - if ( bootEntityDescriptor.getIdentifierMapper() == null - || bootEntityDescriptor.getIdentifierMapper() == bootEntityDescriptor.getIdentifier() ) { + final var identifierMapper = bootEntityDescriptor.getIdentifierMapper(); + final var identifier = bootEntityDescriptor.getIdentifier(); + if ( identifierMapper == null || identifierMapper == identifier ) { // cid -> getIdentifier // idClass -> null - final Component virtualIdSource = (Component) bootEntityDescriptor.getIdentifier(); - + final var virtualIdSource = (Component) identifier; virtualIdEmbeddable = new VirtualIdEmbeddable( virtualIdSource, this, @@ -85,9 +85,8 @@ public NonAggregatedIdentifierMappingImpl( else { // cid = getIdentifierMapper // idClass = getIdentifier - final var virtualIdSource = bootEntityDescriptor.getIdentifierMapper(); - final var idClassSource = (Component) bootEntityDescriptor.getIdentifier(); - + final var virtualIdSource = identifierMapper; + final var idClassSource = (Component) identifier; virtualIdEmbeddable = new VirtualIdEmbeddable( virtualIdSource, this, 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 0630e9ff28c0..855ab62424e2 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 @@ -160,7 +160,8 @@ public TableGroupJoin createTableGroupJoin( if ( mapKeyPropertyName != null ) { final var elementPart = (EntityCollectionPart) - getCollectionDescriptor().getAttributeMapping().getElementDescriptor(); + getCollectionDescriptor().getAttributeMapping() + .getElementDescriptor(); if ( elementPart.getAssociatedEntityMappingType().findAttributeMapping( mapKeyPropertyName ) instanceof ToOneAttributeMapping toOne ) { final var mapKeyPropertyPath = navigablePath.append( mapKeyPropertyName ); @@ -180,7 +181,7 @@ public TableGroupJoin createTableGroupJoin( } } - return new TableGroupJoin( navigablePath, joinType, elementTableGroup, null ); + return new TableGroupJoin( navigablePath, joinType, elementTableGroup ); } @Override @@ -234,14 +235,16 @@ public boolean finishInitialization( if ( pluralAttribute == null ) { return false; } - - final var foreignKey = pluralAttribute.getKeyDescriptor(); - if ( foreignKey == null ) { - return false; + else { + final var foreignKey = pluralAttribute.getKeyDescriptor(); + if ( foreignKey == null ) { + return false; + } + else { + fetchAssociationKey = foreignKey.getAssociationKey(); + return true; + } } - - fetchAssociationKey = foreignKey.getAssociationKey(); - return true; } @Override 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 09bfb164a96f..e56ccf0a89bb 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 @@ -5,6 +5,7 @@ package org.hibernate.metamodel.mapping.internal; import org.checkerframework.checker.nullness.qual.Nullable; +import org.hibernate.MappingException; import org.hibernate.cache.MutableCacheKeyBuilder; import org.hibernate.engine.FetchStyle; import org.hibernate.engine.FetchTiming; @@ -18,10 +19,14 @@ import org.hibernate.mapping.List; import org.hibernate.mapping.Map; import org.hibernate.mapping.Property; +import org.hibernate.metamodel.RepresentationMode; +import org.hibernate.metamodel.mapping.AuditMapping; import org.hibernate.metamodel.mapping.AttributeMetadata; +import org.hibernate.metamodel.mapping.AuxiliaryMapping; import org.hibernate.metamodel.mapping.CollectionIdentifierDescriptor; import org.hibernate.metamodel.mapping.CollectionMappingType; import org.hibernate.metamodel.mapping.CollectionPart; +import org.hibernate.metamodel.mapping.EmbeddableMappingType; import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.ForeignKeyDescriptor; import org.hibernate.metamodel.mapping.JdbcMapping; @@ -32,21 +37,29 @@ import org.hibernate.metamodel.mapping.SelectableMapping; import org.hibernate.metamodel.mapping.SoftDeleteMapping; import org.hibernate.metamodel.mapping.TableDetails; +import org.hibernate.metamodel.mapping.TemporalMapping; +import org.hibernate.metamodel.mapping.ValuedModelPart; import org.hibernate.metamodel.mapping.ordering.OrderByFragment; import org.hibernate.metamodel.mapping.ordering.OrderByFragmentTranslator; import org.hibernate.metamodel.mapping.ordering.TranslationContext; import org.hibernate.metamodel.model.domain.NavigableRole; +import org.hibernate.metamodel.spi.ManagedTypeRepresentationStrategy; +import org.hibernate.models.spi.ClassDetails; +import org.hibernate.models.spi.FieldDetails; +import org.hibernate.models.spi.MemberDetails; +import org.hibernate.models.spi.MethodDetails; import org.hibernate.persister.collection.CollectionPersister; import org.hibernate.persister.collection.mutation.CollectionMutationTarget; import org.hibernate.property.access.spi.PropertyAccess; import org.hibernate.spi.NavigablePath; import org.hibernate.sql.ast.SqlAstJoinType; -import org.hibernate.sql.ast.internal.TableGroupJoinHelper; +import org.hibernate.sql.ast.spi.SqlAliasBaseGenerator; import org.hibernate.sql.ast.spi.SqlAliasBase; import org.hibernate.sql.ast.spi.SqlAliasStemHelper; import org.hibernate.sql.ast.spi.SqlAstCreationState; import org.hibernate.sql.ast.spi.SqlSelection; import org.hibernate.sql.ast.tree.from.CollectionTableGroup; +import org.hibernate.sql.ast.tree.from.AuxiliaryTableReference; import org.hibernate.sql.ast.tree.from.NamedTableReference; import org.hibernate.sql.ast.tree.from.OneToManyTableGroup; import org.hibernate.sql.ast.tree.from.TableGroup; @@ -64,12 +77,15 @@ import org.hibernate.sql.results.graph.collection.internal.EagerCollectionFetch; import org.hibernate.sql.results.graph.collection.internal.SelectEagerCollectionFetch; +import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Supplier; -import static org.hibernate.boot.model.internal.SoftDeleteHelper.resolveSoftDeleteMapping; +import static java.util.Locale.ROOT; import static org.hibernate.internal.util.StringHelper.subStringNullIfEmpty; +import static org.hibernate.sql.ast.internal.TableGroupJoinHelper.determineJoinForPredicateApply; /** * @author Steve Ebersole @@ -101,8 +117,7 @@ public interface Aware { private final CollectionIdentifierDescriptor identifierDescriptor; private final FetchTiming fetchTiming; private final FetchStyle fetchStyle; - private final SoftDeleteMapping softDeleteMapping; - private Boolean hasSoftDelete; + private final AuxiliaryMapping auxiliaryMapping; private final String bidirectionalAttributeName; @@ -145,16 +160,15 @@ public PluralAttributeMappingImpl( this.collectionDescriptor = collectionDescriptor; this.referencedPropertyName = bootDescriptor.getReferencedPropertyName(); - this.mapKeyPropertyName = bootDescriptor instanceof Map map ? map.getMapKeyPropertyName() : null; + mapKeyPropertyName = bootDescriptor instanceof Map map ? map.getMapKeyPropertyName() : null; - this.bidirectionalAttributeName = subStringNullIfEmpty( bootDescriptor.getMappedByProperty(), '.'); + bidirectionalAttributeName = subStringNullIfEmpty( bootDescriptor.getMappedByProperty(), '.'); - this.sqlAliasStem = SqlAliasStemHelper.INSTANCE.generateStemFromAttributeName( attributeName ); + sqlAliasStem = SqlAliasStemHelper.INSTANCE.generateStemFromAttributeName( attributeName ); separateCollectionTable = bootDescriptor.isOneToMany() ? null : collectionDescriptor.getTableName(); final int baseIndex = bootDescriptor instanceof List list ? list.getBaseIndex() : -1; - indexMetadata = new IndexMetadata() { @Override public CollectionPart getIndexDescriptor() { @@ -172,9 +186,150 @@ public String getIndexPropertyName() { } }; - softDeleteMapping = resolveSoftDeleteMapping( this, bootDescriptor, getSeparateCollectionTable(), creationProcess ); + auxiliaryMapping = + bootDescriptor.getStateManagement() + .createAuxiliaryMapping( this, bootDescriptor, creationProcess ); injectAttributeMapping( elementDescriptor, indexDescriptor, collectionDescriptor, this ); + + if ( elementDescriptor instanceof EntityCollectionPart elementMapping ) { + validateTargetEntity( elementMapping, declaringType, attributeName, propertyAccess, creationProcess ); + } + } + + /** + * @implNote This is check based on best effort. If we are not able to resolve + * something needed for the check we simply short-circuit in the "affirmative". + * In testing, this mainly manifested in cases with embeddable inheritance + * given how the EmbeddableMappingType is built and an inability to locate + * subtype members. + */ + private static void validateTargetEntity( + EntityCollectionPart elementPart, + ManagedMappingType declaringType, + String attributeName, + PropertyAccess propertyAccess, + MappingModelCreationProcess creationProcess) { + final var representationStrategy = typeRepresentationStrategy( declaringType ); + if ( representationStrategy != null + // nothing to check against with dynamic models + && representationStrategy.getMode() == RepresentationMode.POJO ) { + final var attributeMemberDetails = + getMemberDetails( attributeName, propertyAccess, + declaringClassDetails( declaringType, creationProcess ) ); + if ( attributeMemberDetails != null ) { + checkElementType( elementPart, declaringType, attributeName, attributeMemberDetails ); + } + // else usually indicates the case of embeddable + // inheritance mentioned in the @implNote + } + } + + private static ClassDetails declaringClassDetails( + ManagedMappingType declaringType, + MappingModelCreationProcess creationProcess) { + return creationProcess.getCreationContext().getBootstrapContext() + .getModelsContext().getClassDetailsRegistry() + .resolveClassDetails( declaringType.getJavaType().getTypeName() ); + } + + private static void checkElementType( + EntityCollectionPart elementPart, + ManagedMappingType declaringType, + String attributeName, + MemberDetails attributeMemberDetails) { + final var elementType = + attributeMemberDetails.getElementType() + .determineRawClass().toJavaClass(); + if ( !Object.class.equals( elementType ) ) { + final var targetType = elementPart.getJavaType().getJavaTypeClass(); + if ( !elementType.isAssignableFrom( targetType ) ) { + throw new MappingException( + String.format( + ROOT, + "Plural attribute [%s.%s] was mapped with targetEntity=`%s`," + + " but the attribute is declared as `%s`", + declaringType.getNavigableRole().getFullPath(), + attributeName, + targetType.getName(), + elementType.getName() + ) + ); + } + } + } + + private static @Nullable MemberDetails getMemberDetails( + String attributeName, PropertyAccess propertyAccess, ClassDetails declaringClassDetails) { + final var member = propertyAccess.getGetter().getMember(); + if ( member instanceof Field ) { + return locateField( declaringClassDetails, attributeName ); + } + else if ( member instanceof Method method ) { + return locateGetter( declaringClassDetails, method ); + } + else { + // we need access to the field or getter... + return null; + } + } + + private static @Nullable ManagedTypeRepresentationStrategy typeRepresentationStrategy(ManagedMappingType declaringType) { + if ( declaringType instanceof EntityMappingType declaringEntityType ) { + return declaringEntityType.getRepresentationStrategy(); + } + else if ( declaringType instanceof EmbeddableMappingType declaringEmbeddableType ) { + return declaringEmbeddableType.getRepresentationStrategy(); + } + else { + // should never happen, but be lenient + return null; + } + } + + /** + * Locate the corresponding field details. + * + * @return The field details, or {@code null} if we cannot locate it. + * + * @implNote See `implNote` on {@linkplain #validateTargetEntity} for details + * about why we return {@code null} instead of throwing an exception. + */ + private static FieldDetails locateField(ClassDetails declaringClassDetails, String attributeName) { + assert declaringClassDetails != null; + var classDetails = declaringClassDetails; + while ( classDetails != null && classDetails != ClassDetails.OBJECT_CLASS_DETAILS ) { + final var fieldDetails = classDetails.findFieldByName( attributeName ); + if ( fieldDetails != null ) { + return fieldDetails; + } + classDetails = classDetails.getSuperClass(); + } + return null; + } + + /** + * Locate the corresponding getter method details. + * + * @return The getter method details, or {@code null} if we cannot locate it. + * + * @implNote See `implNote` on {@linkplain #validateTargetEntity} for details + * about why we return {@code null} instead of throwing an exception. + */ + private static MethodDetails locateGetter(ClassDetails declaringClassDetails, Method method) { + assert declaringClassDetails != null; + var classDetails = declaringClassDetails; + while ( classDetails != null && classDetails != ClassDetails.OBJECT_CLASS_DETAILS ) { + for ( int i = 0; i < classDetails.getMethods().size(); i++ ) { + final var methodDetails = classDetails.getMethods().get(i); + if ( methodDetails.getName().equals( method.getName() ) + && methodDetails.getMethodKind() == MethodDetails.MethodKind.GETTER ) { + return methodDetails; + } + } + classDetails = classDetails.getSuperClass(); + } + return null; } @@ -189,8 +344,6 @@ protected PluralAttributeMappingImpl(PluralAttributeMappingImpl original) { this.identifierDescriptor = original.identifierDescriptor; this.fetchTiming = original.fetchTiming; this.fetchStyle = original.fetchStyle; - this.softDeleteMapping = original.softDeleteMapping; - this.hasSoftDelete = original.hasSoftDelete; this.collectionDescriptor = original.collectionDescriptor; this.referencedPropertyName = original.referencedPropertyName; this.mapKeyPropertyName = original.mapKeyPropertyName; @@ -201,6 +354,7 @@ protected PluralAttributeMappingImpl(PluralAttributeMappingImpl original) { this.fkDescriptor = original.fkDescriptor; this.orderByFragment = original.orderByFragment; this.manyToManyOrderByFragment = original.manyToManyOrderByFragment; + this.auxiliaryMapping = original.auxiliaryMapping; injectAttributeMapping( elementDescriptor, indexDescriptor, collectionDescriptor, this ); } @@ -225,11 +379,31 @@ private static void injectAttributeMapping( @Override public boolean isBidirectionalAttributeName(NavigablePath fetchablePath, ToOneAttributeMapping modelPart) { return bidirectionalAttributeName == null - // If the FK-target of the to-one mapping is the same as the FK-target of this plural mapping, - // then we say this is bidirectional, given that this is only invoked for model parts of the - // collection elements - ? fkDescriptor.getTargetPart() == modelPart.getForeignKeyDescriptor().getTargetPart() - : fetchablePath.getLocalName().endsWith( bidirectionalAttributeName ); + // If the FK-target of the to-one mapping is the same as the FK-target of this one-to-many mapping, + // and the FK-key refer to the same column then we say this is bidirectional, + // given that this is only invoked for model parts of the collection elements + ? modelPart.getSideNature() == ForeignKeyDescriptor.Nature.KEY + && collectionDescriptor.isOneToMany() + && fkDescriptor.getTargetPart() == modelPart.getForeignKeyDescriptor().getTargetPart() + && areEqual( fkDescriptor.getKeyPart(), modelPart.getForeignKeyDescriptor().getKeyPart() ) + : fetchablePath.getLocalName().equals( bidirectionalAttributeName ); + } + + private boolean areEqual(ValuedModelPart part1, ValuedModelPart part2) { + final int typeCount = part1.getJdbcTypeCount(); + if ( part2.getJdbcTypeCount() != typeCount ) { + return false; + } + for ( int i = 0; i < typeCount; i++ ) { + final var selectable1 = part1.getSelectable( i ); + final var selectable2 = part2.getSelectable( i ); + if ( selectable1.getJdbcMapping() != selectable2.getJdbcMapping() + || !selectable1.getContainingTableExpression().equals( selectable2.getContainingTableExpression() ) + || !selectable1.getSelectionExpression().equals( selectable2.getSelectionExpression() ) ) { + return false; + } + } + return true; } public void finishInitialization( @@ -304,7 +478,8 @@ public CollectionIdentifierDescriptor getIdentifierDescriptor() { @Override public SoftDeleteMapping getSoftDeleteMapping() { - return softDeleteMapping; + return auxiliaryMapping instanceof SoftDeleteMapping softDeleteMapping + ? softDeleteMapping : null; } @Override @@ -312,6 +487,23 @@ public TableDetails getSoftDeleteTableDetails() { return ( (CollectionMutationTarget) getCollectionDescriptor() ).getCollectionTableMapping(); } + @Override + public TemporalMapping getTemporalMapping() { + return auxiliaryMapping instanceof TemporalMapping temporalMapping + ? temporalMapping : null; + } + + @Override + public AuditMapping getAuditMapping() { + return auxiliaryMapping instanceof AuditMapping auditMapping + ? auditMapping : null; + } + + private AuxiliaryMapping getAuxiliaryMapping() { + return auxiliaryMapping; + } + + @Override public OrderByFragment getOrderByFragment() { return orderByFragment; @@ -364,34 +556,36 @@ public boolean hasPartitionedSelectionMapping() { } @Override - public void applySoftDeleteRestrictions(TableGroup tableGroup, PredicateConsumer predicateConsumer) { - if ( hasSoftDelete() ) { - final var descriptor = getCollectionDescriptor(); - if ( descriptor.isOneToMany() || descriptor.isManyToMany() ) { - // see if the associated entity has soft-delete defined - final var elementDescriptor = (EntityCollectionPart) getElementDescriptor(); - final var associatedEntityDescriptor = elementDescriptor.getAssociatedEntityMappingType(); - final var softDeleteMapping = associatedEntityDescriptor.getSoftDeleteMapping(); - if ( softDeleteMapping != null ) { - final String primaryTableName = - associatedEntityDescriptor.getSoftDeleteTableDetails().getTableName(); - final var primaryTableReference = - tableGroup.resolveTableReference( primaryTableName ); - final var softDeleteRestriction = - softDeleteMapping.createNonDeletedRestriction( primaryTableReference ); - predicateConsumer.applyPredicate( softDeleteRestriction ); - } + public void applyAuxiliaryRestrictions( + TableGroup tableGroup, + PredicateConsumer predicateConsumer, + LoadQueryInfluencers influencers, + SqlAliasBaseGenerator sqlAliasBaseGenerator) { + final var descriptor = getCollectionDescriptor(); + if ( descriptor.isOneToMany() || descriptor.isManyToMany() ) { + final var elementDescriptor = (EntityCollectionPart) getElementDescriptor(); + final var associatedEntityDescriptor = elementDescriptor.getAssociatedEntityMappingType(); + final var associatedAuxiliaryMapping = associatedEntityDescriptor.getAuxiliaryMapping(); + if ( associatedAuxiliaryMapping != null ) { + associatedAuxiliaryMapping.applyPredicate( + associatedEntityDescriptor, + predicateConsumer::applyPredicate, + tableGroup, + sqlAliasBaseGenerator, + influencers + ); } + } - // apply the collection's soft-delete mapping, if one - final var softDeleteMapping = getSoftDeleteMapping(); - if ( softDeleteMapping != null ) { - final var primaryTableReference = - tableGroup.resolveTableReference( getSoftDeleteTableDetails().getTableName() ); - final var softDeleteRestriction = - softDeleteMapping.createNonDeletedRestriction( primaryTableReference ); - predicateConsumer.applyPredicate( softDeleteRestriction ); - } + final var auxiliaryMapping = getAuxiliaryMapping(); + if ( auxiliaryMapping != null ) { + auxiliaryMapping.applyPredicate( + this, + predicateConsumer::applyPredicate, + tableGroup, + sqlAliasBaseGenerator, + influencers + ); } } @@ -524,19 +718,19 @@ public Fetch resolveCircularFetch( FetchParent fetchParent, FetchTiming fetchTiming, DomainResultCreationState creationState) { - if ( fetchTiming == FetchTiming.IMMEDIATE ) { - final boolean alreadyVisited = creationState.isAssociationKeyVisited( fkDescriptor.getAssociationKey() ); - if ( alreadyVisited ) { - return createSelectEagerCollectionFetch( - fetchParent, - fetchablePath, - creationState, - creationState.getSqlAstCreationState() - ); - } + if ( fetchTiming == FetchTiming.IMMEDIATE + // if it's already visited + && creationState.isAssociationKeyVisited( fkDescriptor.getAssociationKey() ) ) { + return createSelectEagerCollectionFetch( + fetchParent, + fetchablePath, + creationState, + creationState.getSqlAstCreationState() + ); + } + else { + return null; } - - return null; } private Fetch createSelectEagerCollectionFetch( @@ -544,20 +738,29 @@ private Fetch createSelectEagerCollectionFetch( NavigablePath fetchablePath, DomainResultCreationState creationState, SqlAstCreationState sqlAstCreationState) { - final DomainResult collectionKeyDomainResult; - if ( referencedPropertyName != null ) { - collectionKeyDomainResult = getKeyDescriptor().createTargetDomainResult( - fetchablePath, - sqlAstCreationState.getFromClauseAccess() - .getTableGroup( fetchParent.getNavigablePath() ), - fetchParent, - creationState - ); + return buildSelectEagerCollectionFetch( fetchablePath, this, + collectionKeyDomainResult( fetchParent, fetchablePath, creationState, sqlAstCreationState ), + fetchParent ); + } + + private @Nullable DomainResult collectionKeyDomainResult( + FetchParent fetchParent, + NavigablePath fetchablePath, + DomainResultCreationState creationState, + SqlAstCreationState sqlAstCreationState) { + if ( referencedPropertyName == null ) { + return null; } else { - collectionKeyDomainResult = null; + return getKeyDescriptor() + .createTargetDomainResult( + fetchablePath, + sqlAstCreationState.getFromClauseAccess() + .getTableGroup( fetchParent.getNavigablePath() ), + fetchParent, + creationState + ); } - return buildSelectEagerCollectionFetch( fetchablePath, this, collectionKeyDomainResult, fetchParent ); } private TableGroup resolveCollectionTableGroup( @@ -686,10 +889,11 @@ public TableGroupJoin createTableGroupJoin( creationState ); - applySoftDeleteRestriction( - predicateCollector::applyPredicate, + applyAuxiliaryRestrictions( tableGroup, - creationState + predicateCollector::applyPredicate, + creationState.getLoadQueryInfluencers(), + creationState.getSqlAliasBaseGenerator() ); if ( fetched ) { @@ -709,50 +913,18 @@ public TableGroupJoin createTableGroupJoin( collectionPredicateCollector.getPredicate() ); if ( predicateCollector != collectionPredicateCollector ) { - final var joinForPredicate = TableGroupJoinHelper.determineJoinForPredicateApply( tableGroupJoin ); - joinForPredicate.applyPredicate( predicateCollector.getPredicate() ); + determineJoinForPredicateApply( tableGroupJoin ) + .applyPredicate( predicateCollector.getPredicate() ); } return tableGroupJoin; } private boolean hasSoftDelete() { - // NOTE : this needs to be done lazily because the associated entity mapping (if one) + // NOTE: this needs to be done lazily because the associated entity mapping (if one) // does not know its SoftDeleteMapping yet when this is created - if ( hasSoftDelete == null ) { - hasSoftDelete = - softDeleteMapping != null - || getElementDescriptor() instanceof EntityCollectionPart collectionPart - && collectionPart.getAssociatedEntityMappingType().getSoftDeleteMapping() != null; - } - return hasSoftDelete; - } - - private void applySoftDeleteRestriction( - Consumer predicateConsumer, - TableGroup tableGroup, - SqlAstCreationState creationState) { - if ( hasSoftDelete() ) { - if ( getElementDescriptor() instanceof EntityCollectionPart entityCollectionPart ) { - final var entityMappingType = entityCollectionPart.getAssociatedEntityMappingType(); - final var softDeleteMapping = entityMappingType.getSoftDeleteMapping(); - if ( softDeleteMapping != null ) { - final var softDeleteTable = entityMappingType.getSoftDeleteTableDetails(); - predicateConsumer.accept( softDeleteMapping.createNonDeletedRestriction( - tableGroup.resolveTableReference( softDeleteTable.getTableName() ), - creationState.getSqlExpressionResolver() - ) ); - } - } - - final var softDeleteMapping = getSoftDeleteMapping(); - if ( softDeleteMapping != null ) { - final var softDeleteTable = getSoftDeleteTableDetails(); - predicateConsumer.accept( softDeleteMapping.createNonDeletedRestriction( - tableGroup.resolveTableReference( softDeleteTable.getTableName() ), - creationState.getSqlExpressionResolver() - ) ); - } - } + return auxiliaryMapping instanceof SoftDeleteMapping + || getElementDescriptor() instanceof EntityCollectionPart collectionPart + && collectionPart.getAssociatedEntityMappingType().getSoftDeleteMapping() != null; } public SqlAstJoinType determineSqlJoinType(TableGroup lhs, @Nullable SqlAstJoinType requestedJoinType, boolean fetched) { @@ -760,12 +932,9 @@ public SqlAstJoinType determineSqlJoinType(TableGroup lhs, @Nullable SqlAstJoinT return SqlAstJoinType.LEFT; } else if ( requestedJoinType == null ) { - if ( fetched ) { - return getDefaultSqlAstJoinType( lhs ); - } - else { - return SqlAstJoinType.INNER; - } + return fetched + ? getDefaultSqlAstJoinType( lhs ) + : SqlAstJoinType.INNER; } else { return requestedJoinType; @@ -805,43 +974,62 @@ private TableGroup createRootTableGroupJoin( boolean addsPredicate, Consumer predicateConsumer, SqlAstCreationState creationState) { - final CollectionPersister collectionDescriptor = getCollectionDescriptor(); - final SqlAstJoinType joinType = determineSqlJoinType( lhs, requestedJoinType, fetched ); - final SqlAliasBase sqlAliasBase = creationState.getSqlAliasBaseGenerator().createSqlAliasBase( getSqlAliasStem() ); - - final TableGroup tableGroup; - if ( collectionDescriptor.isOneToMany() ) { - tableGroup = createOneToManyTableGroup( - lhs.canUseInnerJoins() && joinType == SqlAstJoinType.INNER, - joinType, - navigablePath, - fetched, - addsPredicate, - explicitSourceAlias, - sqlAliasBase, - creationState - ); - } - else { - tableGroup = createCollectionTableGroup( - lhs.canUseInnerJoins() && joinType == SqlAstJoinType.INNER, - joinType, - navigablePath, - fetched, - addsPredicate, - explicitSourceAlias, - sqlAliasBase, - creationState - ); - } + + final var tableGroup = + rootTableGroup( + navigablePath, + lhs, + explicitSourceAlias, + fetched, + addsPredicate, + creationState, + determineSqlJoinType( lhs, requestedJoinType, fetched ), + creationState.getSqlAliasBaseGenerator() + .createSqlAliasBase( getSqlAliasStem() ) + ); if ( predicateConsumer != null ) { - predicateConsumer.accept( getKeyDescriptor().generateJoinPredicate( lhs, tableGroup, creationState ) ); + predicateConsumer.accept( getKeyDescriptor() + .generateJoinPredicate( lhs, tableGroup, creationState ) ); } return tableGroup; } + private TableGroup rootTableGroup( + NavigablePath navigablePath, + TableGroup lhs, + String explicitSourceAlias, + boolean fetched, + boolean addsPredicate, + SqlAstCreationState creationState, + SqlAstJoinType joinType, + SqlAliasBase sqlAliasBase) { + return getCollectionDescriptor().isOneToMany() + ? createOneToManyTableGroup( + lhs.canUseInnerJoins() + && joinType == SqlAstJoinType.INNER, + joinType, + navigablePath, + fetched, + addsPredicate, + explicitSourceAlias, + sqlAliasBase, + creationState + ) + : createCollectionTableGroup( + lhs.canUseInnerJoins() + && joinType == SqlAstJoinType.INNER, + joinType, + navigablePath, + fetched, + addsPredicate, + explicitSourceAlias, + sqlAliasBase, + creationState + ); + } + @Override public void setForeignKeyDescriptor(ForeignKeyDescriptor fkDescriptor) { @@ -857,33 +1045,27 @@ private TableGroup createOneToManyTableGroup( String sourceAlias, SqlAliasBase explicitSqlAliasBase, SqlAstCreationState creationState) { + final var oneToManyCollectionPart = (OneToManyCollectionPart) elementDescriptor; final var sqlAliasBase = SqlAliasBase.from( explicitSqlAliasBase, sourceAlias, this, creationState.getSqlAliasBaseGenerator() ); - final var oneToManyCollectionPart = (OneToManyCollectionPart) elementDescriptor; - final var elementTableGroup = oneToManyCollectionPart.createAssociatedTableGroup( - canUseInnerJoins, - navigablePath.append( CollectionPart.Nature.ELEMENT.getName() ), - fetched, - sourceAlias, - sqlAliasBase, - creationState - ); final var tableGroup = new OneToManyTableGroup( this, - elementTableGroup, - creationState.getCreationContext().getSessionFactory() + oneToManyCollectionPart.createAssociatedTableGroup( + canUseInnerJoins, + navigablePath.append( CollectionPart.Nature.ELEMENT.getName() ), + fetched, + sourceAlias, + sqlAliasBase, + creationState + ), + creationState.getCreationContext() + // TODO: FIX ME + .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 tableGroupJoinProducer ) { final var tableGroupJoin = tableGroupJoinProducer.createTableGroupJoin( navigablePath.append( CollectionPart.Nature.INDEX.getName() ), @@ -895,12 +1077,23 @@ private TableGroup createOneToManyTableGroup( false, creationState ); - tableGroup.registerIndexTableGroup( tableGroupJoin, nestedJoin ); + tableGroup.registerIndexTableGroup( tableGroupJoin, + isNestedJoin( joinType, addsPredicate, creationState ) ); } return tableGroup; } + private boolean isNestedJoin(SqlAstJoinType joinType, boolean addsPredicate, SqlAstCreationState creationState) { + // For inner joins we never need join nesting + return joinType != SqlAstJoinType.INNER + // For outer joins we need nesting if there might be an on-condition that refers to the element table + && ( addsPredicate + || collectionDescriptor.hasWhereRestrictions() + || isAffectedByEnabledFilters( creationState.getLoadQueryInfluencers(), + creationState.applyOnlyLoadByKeyFilters() ) ); + } + private TableGroup createCollectionTableGroup( boolean canUseInnerJoins, SqlAstJoinType joinType, @@ -917,12 +1110,12 @@ private TableGroup createCollectionTableGroup( this, creationState.getSqlAliasBaseGenerator() ); - final String collectionTableName = collectionDescriptor.getTableName(); - final var collectionTableReference = new NamedTableReference( - collectionTableName, - sqlAliasBase.generateNewAlias(), - true - ); + final String tableName = collectionDescriptor.getTableName(); + final String alias = sqlAliasBase.generateNewAlias(); + final var collectionTableReference = + collectionTableReference( creationState, tableName, alias ); + collectionTableReference.applyAuxiliaryTable( auxiliaryMapping, + creationState.getLoadQueryInfluencers() ); final var tableGroup = new CollectionTableGroup( canUseInnerJoins, @@ -935,14 +1128,12 @@ private TableGroup createCollectionTableGroup( sqlAliasBase, s -> false, null, - creationState.getCreationContext().getSessionFactory() + creationState.getCreationContext() + // TODO: FIX ME + .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() ); + + final boolean nestedJoin = isNestedJoin( joinType, addsPredicate, creationState ); if ( elementDescriptor instanceof TableGroupJoinProducer tableGroupJoinProducer ) { final var tableGroupJoin = tableGroupJoinProducer.createTableGroupJoin( @@ -975,12 +1166,19 @@ private TableGroup createCollectionTableGroup( return tableGroup; } + private NamedTableReference collectionTableReference(SqlAstCreationState creationState, String tableName, String alias) { + return auxiliaryMapping != null && auxiliaryMapping.useAuxiliaryTable( creationState.getLoadQueryInfluencers() ) + ? new AuxiliaryTableReference( auxiliaryMapping.getTableName(), tableName, alias, true ) + : new NamedTableReference( tableName, alias, true ); + } + @Override public TableGroup createRootTableGroup( boolean canUseInnerJoins, NavigablePath navigablePath, String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, Supplier> additionalPredicateCollectorAccess, + SqlAliasBase explicitSqlAliasBase, + Supplier> additionalPredicateCollectorAccess, SqlAstCreationState creationState) { if ( getCollectionDescriptor().isOneToMany() ) { return createOneToManyTableGroup( @@ -1018,6 +1216,23 @@ public boolean isAffectedByEnabledFilters(LoadQueryInfluencers influencers, bool return getCollectionDescriptor().isAffectedByEnabledFilters( influencers, onlyApplyForLoadByKeyFilters ); } + @Override + public boolean isAffectedByInfluencers(LoadQueryInfluencers influencers, boolean onlyApplyForLoadByKeyFilters) { + if ( PluralAttributeMapping.super.isAffectedByInfluencers( influencers, onlyApplyForLoadByKeyFilters ) + || auxiliaryMapping != null && auxiliaryMapping.isAffectedByInfluencers( influencers )) { + return true; + } + else { + final var descriptor = getCollectionDescriptor(); + if ( descriptor.isOneToMany() || descriptor.isManyToMany() ) { + final var elementDescriptor = (EntityCollectionPart) getElementDescriptor(); + return elementDescriptor.getAssociatedEntityMappingType() + .isAffectedByInfluencers( influencers, onlyApplyForLoadByKeyFilters ); + } + return false; + } + } + @Override public boolean isAffectedByEntityGraph(LoadQueryInfluencers influencers) { return getCollectionDescriptor().isAffectedByEntityGraph( influencers ); diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/SelectableMappingImpl.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/SelectableMappingImpl.java index d68524175980..9402bf31fcb1 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/SelectableMappingImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/SelectableMappingImpl.java @@ -109,6 +109,7 @@ public SelectableMappingImpl( this.isFormula = isFormula; } + @Deprecated(forRemoval = true) public static SelectableMapping from( final String containingTableExpression, final Selectable selectable, @@ -129,12 +130,14 @@ public static SelectableMapping from( insertable, updateable, partitioned, + false, dialect, sqmFunctionRegistry, creationContext ); } + @Deprecated(forRemoval = true) public static SelectableMapping from( final String containingTableExpression, final Selectable selectable, @@ -163,6 +166,7 @@ public static SelectableMapping from( ); } + @Deprecated(forRemoval = true) public static SelectableMapping from( final String containingTableExpression, final Selectable selectable, @@ -191,6 +195,7 @@ public static SelectableMapping from( ); } + @Deprecated(forRemoval = true) public static SelectableMapping from( final String containingTableExpression, final Selectable selectable, @@ -204,6 +209,39 @@ public static SelectableMapping from( final Dialect dialect, final SqmFunctionRegistry sqmFunctionRegistry, RuntimeModelCreationContext creationContext) { + return from( + containingTableExpression, + selectable, + selectable.isFormula() ? selectable.getText() : null, + parentPath, + jdbcMapping, + typeConfiguration, + insertable, + updateable, + partitioned, + forceNotNullable, + dialect, + sqmFunctionRegistry, + creationContext + ); + } + + public static SelectableMapping from( + final String containingTableExpression, + final Selectable selectable, + // The path to the property for this selectable mapping, which is used as selectable name + // if the selectable is a formula. If it's a formula, the value should be non-null + @Nullable final String propertyPath, + @Nullable final SelectablePath parentPath, + final JdbcMapping jdbcMapping, + final TypeConfiguration typeConfiguration, + boolean insertable, + boolean updateable, + boolean partitioned, + boolean forceNotNullable, + final Dialect dialect, + final SqmFunctionRegistry sqmFunctionRegistry, + RuntimeModelCreationContext creationContext) { final String columnExpression; final String columnDefinition; final Long length; @@ -212,6 +250,7 @@ public static SelectableMapping from( final Integer scale; final Integer temporalPrecision; final String selectableName; + final SelectablePath selectablePath; final boolean isLob; final boolean isNullable; if ( selectable.isFormula() ) { @@ -224,7 +263,9 @@ public static SelectableMapping from( temporalPrecision = null; isNullable = true; isLob = false; - selectableName = selectable.getText(); + selectableName = propertyPath; + assert propertyPath != null : "Property path must be non-null for formulas"; + selectablePath = new SelectablePath( propertyPath ); } else { var column = (Column) selectable; @@ -239,13 +280,14 @@ public static SelectableMapping from( isNullable = !forceNotNullable && column.isNullable(); isLob = column.isSqlTypeLob( creationContext.getMetadata() ); selectableName = column.getQuotedName( dialect ); + selectablePath = parentPath == null + ? null + : parentPath.append( selectableName ); } return new SelectableMappingImpl( containingTableExpression, columnExpression, - parentPath == null - ? null - : parentPath.append( selectableName ), + selectablePath, selectable.getCustomReadExpression(), selectable.getWriteExpr( jdbcMapping, dialect, creationContext.getBootModel() ), columnDefinition, diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/SelectableMappingsImpl.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/SelectableMappingsImpl.java index 7c0871166ad1..c8031de558b6 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/SelectableMappingsImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/SelectableMappingsImpl.java @@ -38,12 +38,12 @@ public SelectableMappingsImpl(SelectableMapping[] selectableMappings) { } private static void resolveJdbcMappings(List jdbcMappings, MappingContext mapping, Type valueType) { - final Type keyType = + final var keyType = valueType instanceof EntityType entityType ? entityType.getIdentifierOrUniqueKeyType( mapping ) : valueType; if ( keyType instanceof CompositeType compositeType ) { - for ( Type subtype : compositeType.getSubtypes() ) { + for ( var subtype : compositeType.getSubtypes() ) { resolveJdbcMappings( jdbcMappings, mapping, subtype ); } } @@ -52,6 +52,7 @@ private static void resolveJdbcMappings(List jdbcMappings, MappingC } } + @Deprecated(forRemoval = true) public static SelectableMappings from( String containingTableExpression, Value value, @@ -78,6 +79,7 @@ public static SelectableMappings from( ); } + @Deprecated(forRemoval = true) public static SelectableMappings from( String containingTableExpression, Value value, @@ -90,6 +92,35 @@ public static SelectableMappings from( Dialect dialect, SqmFunctionRegistry sqmFunctionRegistry, RuntimeModelCreationContext creationContext) { + return from( + containingTableExpression, + value, + propertyOrder, + null, + parentSelectablePath, + mappingContext, + typeConfiguration, + insertable, + updateable, + dialect, + sqmFunctionRegistry, + creationContext + ); + } + + public static SelectableMappings from( + String containingTableExpression, + Value value, + int[] propertyOrder, + @Nullable String propertyPath, + @Nullable SelectablePath parentSelectablePath, + MappingContext mappingContext, + TypeConfiguration typeConfiguration, + boolean[] insertable, + boolean[] updateable, + Dialect dialect, + SqmFunctionRegistry sqmFunctionRegistry, + RuntimeModelCreationContext creationContext) { final List jdbcMappings = new ArrayList<>(); resolveJdbcMappings( jdbcMappings, mappingContext, value.getType() ); @@ -99,12 +130,14 @@ public static SelectableMappings from( selectableMappings[propertyOrder[i]] = SelectableMappingImpl.from( containingTableExpression, selectables.get( i ), + selectables.get( i ).isFormula() ? (propertyPath + "_" + i) : null, parentSelectablePath, jdbcMappings.get( propertyOrder[i] ), typeConfiguration, i < insertable.length && insertable[i], i < updateable.length && updateable[i], false, + false, dialect, sqmFunctionRegistry, creationContext diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/SimpleForeignKeyDescriptor.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/SimpleForeignKeyDescriptor.java index aed1ace3cc83..04bef9d0f78e 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/SimpleForeignKeyDescriptor.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/SimpleForeignKeyDescriptor.java @@ -30,6 +30,7 @@ import org.hibernate.metamodel.mapping.PropertyBasedMapping; import org.hibernate.metamodel.mapping.SelectableConsumer; import org.hibernate.metamodel.mapping.SelectableMapping; +import org.hibernate.metamodel.mapping.SelectablePath; import org.hibernate.metamodel.mapping.ValuedModelPart; import org.hibernate.metamodel.model.domain.NavigableRole; import org.hibernate.property.access.spi.PropertyAccess; @@ -387,7 +388,8 @@ private DomainResult createDomainResult( selectableMapping.getJdbcMapping(), navigablePath, // if the expression type is different that the expected type coerce the value - selectionType != null && selectionType.getSingleJdbcMapping().getJdbcJavaType() != javaType, + selectionType != null + && selectionType.getSingleJdbcMapping().getJdbcJavaType() != javaType, !sqlSelection.isVirtual() ); } @@ -409,30 +411,21 @@ public Predicate generateJoinPredicate( TableGroup targetSideTableGroup, TableGroup keySideTableGroup, SqlAstCreationState creationState) { - final var lhsTableReference = targetSideTableGroup.resolveTableReference( - targetSideTableGroup.getNavigablePath(), - targetSide.getModelPart().getContainingTableExpression() - ); - final var rhsTableKeyReference = keySideTableGroup.resolveTableReference( - null, - keySide.getModelPart().getContainingTableExpression() - ); - + final var lhsTableReference = + targetSideTableGroup.resolveTableReference( targetSideTableGroup.getNavigablePath(), + targetSide.getModelPart().getContainingTableExpression() ); + final var rhsTableKeyReference = + keySideTableGroup.resolveTableReference( null, + keySide.getModelPart().getContainingTableExpression() ); return generateJoinPredicate( lhsTableReference, rhsTableKeyReference, creationState ); } @Override public boolean isSimpleJoinPredicate(Predicate predicate) { - if ( !(predicate instanceof ComparisonPredicate comparisonPredicate) ) { - return false; - } - if ( comparisonPredicate.getOperator() != ComparisonOperator.EQUAL ) { - return false; - } - final var lhsExpr = comparisonPredicate.getLeftHandExpression(); - final var rhsExpr = comparisonPredicate.getRightHandExpression(); - if ( lhsExpr instanceof ColumnReference lhsColumnRef - && rhsExpr instanceof ColumnReference rhsColumnRef ) { + if ( predicate instanceof ComparisonPredicate comparisonPredicate + && comparisonPredicate.getOperator() == ComparisonOperator.EQUAL + && comparisonPredicate.getLeftHandExpression() instanceof ColumnReference lhsColumnRef + && comparisonPredicate.getRightHandExpression() instanceof ColumnReference rhsColumnRef ) { final String lhs = lhsColumnRef.getColumnExpression(); final String rhs = rhsColumnRef.getColumnExpression(); final String keyExpression = keySide.getModelPart().getSelectionExpression(); @@ -440,9 +433,7 @@ public boolean isSimpleJoinPredicate(Predicate predicate) { return lhs.equals( keyExpression ) && rhs.equals( targetExpression ) || lhs.equals( targetExpression ) && rhs.equals( keyExpression ); } - else { - return false; - } + return false; } @Override @@ -500,7 +491,8 @@ public Object getAssociationKeyFromSide( } final var modelPart = side.getModelPart(); if ( modelPart.isEntityIdentifierMapping() ) { - return ( (EntityIdentifierMapping) modelPart ).getIdentifierIfNotUnsaved( targetObject, session ); + return ( (EntityIdentifierMapping) modelPart ) + .getIdentifierIfNotUnsaved( targetObject, session ); } if ( lazyInitializer == null && isPersistentAttributeInterceptable( targetObject ) ) { @@ -511,7 +503,8 @@ public Object getAssociationKeyFromSide( } } - return ( (PropertyBasedMapping) modelPart ).getPropertyAccess().getGetter().get( targetObject ); + return ( (PropertyBasedMapping) modelPart ) + .getPropertyAccess().getGetter().get( targetObject ); } @Override @@ -605,6 +598,16 @@ public String getSelectionExpression() { return keySide.getModelPart().getSelectionExpression(); } + @Override + public String getSelectableName() { + return keySide.getModelPart().getSelectableName(); + } + + @Override + public SelectablePath getSelectablePath() { + return keySide.getModelPart().getSelectablePath(); + } + @Override public SelectableMapping getSelectable(int columnIndex) { return keySide.getModelPart(); diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/SimpleNaturalIdMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/SimpleNaturalIdMapping.java index 555117bf9d97..b53d6688ea4f 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/SimpleNaturalIdMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/SimpleNaturalIdMapping.java @@ -10,6 +10,7 @@ import java.util.Map; import java.util.function.BiConsumer; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.HibernateException; import org.hibernate.cache.MutableCacheKeyBuilder; import org.hibernate.dialect.Dialect; @@ -34,7 +35,6 @@ import org.hibernate.sql.results.graph.DomainResult; import org.hibernate.sql.results.graph.DomainResultCreationState; import org.hibernate.type.descriptor.java.JavaType; -import org.hibernate.type.spi.TypeConfiguration; import static org.hibernate.loader.ast.internal.MultiKeyLoadHelper.supportsSqlArrayType; @@ -42,10 +42,9 @@ * Single-attribute NaturalIdMapping implementation */ public class SimpleNaturalIdMapping extends AbstractNaturalIdMapping - implements JavaType.CoercionContext, BasicValuedMapping { + implements BasicValuedMapping { private final SingularAttributeMapping attribute; private final SessionFactoryImplementor sessionFactory; - private final TypeConfiguration typeConfiguration; public SimpleNaturalIdMapping( SingularAttributeMapping attribute, @@ -54,7 +53,6 @@ public SimpleNaturalIdMapping( super( declaringType, attribute.getAttributeMetadata().isUpdatable() ); this.attribute = attribute; this.sessionFactory = creationProcess.getCreationContext().getSessionFactory(); - this.typeConfiguration = creationProcess.getCreationContext().getTypeConfiguration(); } public SingularAttributeMapping getAttribute() { @@ -106,19 +104,24 @@ public Object extractNaturalIdFromEntity(Object entity) { return attribute.getPropertyAccess().getGetter().get( entity ); } + @Override + public boolean isNormalized(Object incoming) { + return incoming == null || getJavaType().getJavaTypeClass().isInstance( incoming ); + } + @Override public void validateInternalForm(Object naturalIdValue) { if ( naturalIdValue != null ) { final var naturalIdValueClass = naturalIdValue.getClass(); + // be flexible - allow a single-valued array if ( naturalIdValueClass.isArray() && !naturalIdValueClass.getComponentType().isPrimitive() ) { - // be flexible final var values = (Object[]) naturalIdValue; if ( values.length == 1 ) { naturalIdValue = values[0]; } } - if ( !getJavaType().getJavaTypeClass().isInstance( naturalIdValue ) ) { + if ( !getJavaType().isInstance( naturalIdValue ) ) { throw new IllegalArgumentException( String.format( Locale.ROOT, @@ -143,10 +146,12 @@ public Object normalizeInput(Object incoming) { final Object normalizedValue = normalizedValue( incoming ); return isLoadByIdComplianceEnabled() ? normalizedValue - : getJavaType().coerce( normalizedValue, this ); + : getJavaType().coerce( normalizedValue ); } private Object normalizedValue(Object incoming) { + sessionFactory.getStatistics().normalizeNaturalId( getDeclaringType().getEntityName() ); + if ( incoming instanceof Map valueMap ) { assert valueMap.size() == 1; assert valueMap.containsKey( getAttribute().getAttributeName() ); @@ -170,6 +175,12 @@ public List getNaturalIdAttributes() { return Collections.singletonList( attribute ); } + @Override + @Nullable + public Class getNaturalIdClass() { + return null; + } + @Override public MappingType getPartMappingType() { return attribute.getPartMappingType(); @@ -295,11 +306,6 @@ private Dialect getDialect() { return sessionFactory.getJdbcServices().getDialect(); } - @Override - public TypeConfiguration getTypeConfiguration() { - return typeConfiguration; - } - @Override public AttributeMapping asAttributeMapping() { return getAttribute(); diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/SoftDeleteMappingImpl.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/SoftDeleteMappingImpl.java index 0c78976e3a6d..047f31c48bc3 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/SoftDeleteMappingImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/SoftDeleteMappingImpl.java @@ -7,6 +7,7 @@ import org.hibernate.annotations.SoftDeleteType; import org.hibernate.cache.MutableCacheKeyBuilder; import org.hibernate.dialect.function.CurrentFunction; +import org.hibernate.engine.spi.LoadQueryInfluencers; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.internal.util.IndexedConsumer; import org.hibernate.mapping.BasicValue; @@ -14,16 +15,25 @@ import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.JdbcMapping; import org.hibernate.metamodel.mapping.MappingType; +import org.hibernate.metamodel.mapping.PluralAttributeMapping; import org.hibernate.metamodel.mapping.SoftDeletableModelPart; import org.hibernate.metamodel.mapping.SoftDeleteMapping; import org.hibernate.metamodel.model.domain.NavigableRole; +import org.hibernate.persister.entity.EntityNameUse; +import org.hibernate.persister.entity.EntityPersister; import org.hibernate.query.sqm.function.SelfRenderingFunctionSqlAstExpression; import org.hibernate.spi.NavigablePath; +import org.hibernate.sql.ast.spi.SqlAliasBaseGenerator; +import org.hibernate.sql.ast.spi.SqlAstCreationState; import org.hibernate.sql.ast.spi.SqlExpressionResolver; import org.hibernate.sql.ast.spi.SqlSelection; import org.hibernate.sql.ast.tree.expression.ColumnReference; import org.hibernate.sql.ast.tree.expression.JdbcLiteral; +import org.hibernate.sql.ast.tree.from.LazyTableGroup; +import org.hibernate.sql.ast.tree.from.NamedTableReference; +import org.hibernate.sql.ast.tree.from.StandardTableGroup; import org.hibernate.sql.ast.tree.from.TableGroup; +import org.hibernate.sql.ast.tree.from.TableGroupJoin; import org.hibernate.sql.ast.tree.from.TableReference; import org.hibernate.sql.ast.tree.predicate.ComparisonPredicate; import org.hibernate.sql.ast.tree.predicate.NullnessPredicate; @@ -31,6 +41,8 @@ import org.hibernate.sql.ast.tree.update.Assignment; import org.hibernate.sql.model.ast.ColumnValueBinding; import org.hibernate.sql.model.ast.ColumnWriteFragment; +import org.hibernate.sql.model.ast.builder.MutationGroupBuilder; +import org.hibernate.sql.model.ast.builder.TableInsertBuilder; import org.hibernate.sql.results.graph.DomainResult; import org.hibernate.sql.results.graph.DomainResultCreationState; import org.hibernate.sql.results.graph.basic.BasicResult; @@ -40,6 +52,8 @@ import java.time.Instant; import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Supplier; import static java.util.Collections.emptyList; import static org.hibernate.query.sqm.ComparisonOperator.EQUAL; @@ -351,6 +365,90 @@ public EntityMappingType findContainingEntityMapping() { return softDeletable.findContainingEntityMapping(); } + @Override + public void addToInsertGroup(MutationGroupBuilder insertGroupBuilder, EntityPersister persister) { + final TableInsertBuilder insertBuilder = + insertGroupBuilder.getTableDetailsBuilder( persister.getIdentifierTableName() ); + insertBuilder.addValueColumn( createNonDeletedValueBinding( + new ColumnReference( insertBuilder.getMutatingTable(), this ) ) ); + } + + @Override + public void applyPredicate( + EntityMappingType associatedEntityMappingType, + Consumer predicateConsumer, + LazyTableGroup lazyTableGroup, + NavigablePath navigablePath, + SqlAstCreationState creationState) { + // add the restriction + final var tableReference = + lazyTableGroup.resolveTableReference( navigablePath, + associatedEntityMappingType.getSoftDeleteTableDetails().getTableName() ); + predicateConsumer.accept( createNonDeletedRestriction( tableReference, + creationState.getSqlExpressionResolver() ) ); + } + + @Override + public void applyPredicate( + EntityMappingType associatedEntityDescriptor, + Consumer predicateConsumer, + TableGroup tableGroup, + SqlAliasBaseGenerator sqlAliasBaseGenerator, + LoadQueryInfluencers influencers) { + final String primaryTableName = + associatedEntityDescriptor.getSoftDeleteTableDetails().getTableName(); + predicateConsumer.accept( createNonDeletedRestriction( + tableGroup.resolveTableReference( primaryTableName ) ) ); + } + + @Override + public void applyPredicate( + PluralAttributeMapping associatedEntityDescriptor, + Consumer predicateConsumer, + TableGroup tableGroup, + SqlAliasBaseGenerator sqlAliasBaseGenerator, + LoadQueryInfluencers influencers) { + predicateConsumer.accept( createNonDeletedRestriction( + tableGroup.resolveTableReference( getTableName() ) ) ); + } + + @Override + public void applyPredicate(TableGroupJoin tableGroupJoin, LoadQueryInfluencers loadQueryInfluencers) { + tableGroupJoin.applyPredicate( createNonDeletedRestriction( + tableGroupJoin.getJoinedGroup().resolveTableReference( getTableName() ) + ) ); + } + + @Override + public void applyPredicate( + Supplier> predicateCollector, + SqlAstCreationState creationState, + StandardTableGroup tableGroup, + NamedTableReference rootTableReference, + EntityMappingType entityMappingType) { + final var tableReference = + tableGroup.resolveTableReference( getTableName() ); + final var softDeletePredicate = + createNonDeletedRestriction( tableReference, + creationState.getSqlExpressionResolver() ); + predicateCollector.get().accept( softDeletePredicate ); + if ( tableReference != rootTableReference && creationState.supportsEntityNameUsage() ) { + // Register entity name usage for the hierarchy root table to avoid pruning + creationState.registerEntityNameUsage( tableGroup, EntityNameUse.EXPRESSION, + entityMappingType.getRootEntityDescriptor().getEntityName() ); + } + } + + @Override + public boolean useAuxiliaryTable(LoadQueryInfluencers influencers) { + return false; + } + + @Override + public boolean isAffectedByInfluencers(LoadQueryInfluencers influencers) { + return false; + } + @Override public String toString() { return "SoftDeleteMapping(" + tableName + "." + columnName + ")"; diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/TemporalMappingImpl.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/TemporalMappingImpl.java new file mode 100644 index 000000000000..13bd9cf0d6dc --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/TemporalMappingImpl.java @@ -0,0 +1,361 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.metamodel.mapping.internal; + +import org.hibernate.temporal.TemporalTableStrategy; +import org.hibernate.engine.spi.LoadQueryInfluencers; +import org.hibernate.mapping.BasicValue; +import org.hibernate.mapping.Stateful; +import org.hibernate.metamodel.mapping.EntityMappingType; +import org.hibernate.metamodel.mapping.JdbcMapping; +import org.hibernate.metamodel.mapping.PluralAttributeMapping; +import org.hibernate.metamodel.mapping.SelectableMapping; +import org.hibernate.metamodel.mapping.TemporalMapping; +import org.hibernate.persister.entity.EntityNameUse; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.spi.NavigablePath; +import org.hibernate.sql.ast.spi.SqlAliasBaseGenerator; +import org.hibernate.sql.ast.spi.SqlAstCreationState; +import org.hibernate.sql.ast.spi.SqlExpressionResolver; +import org.hibernate.sql.ast.tree.expression.ColumnReference; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.SelfRenderingSqlFragmentExpression; +import org.hibernate.sql.ast.tree.from.LazyTableGroup; +import org.hibernate.sql.ast.tree.from.NamedTableReference; +import org.hibernate.sql.ast.tree.from.StandardTableGroup; +import org.hibernate.sql.ast.tree.from.TableGroup; +import org.hibernate.sql.ast.tree.from.TableGroupJoin; +import org.hibernate.sql.ast.tree.from.TableReference; +import org.hibernate.sql.ast.tree.predicate.ComparisonPredicate; +import org.hibernate.sql.ast.tree.predicate.Junction; +import org.hibernate.sql.ast.tree.predicate.NullnessPredicate; +import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.sql.model.ast.ColumnValueBinding; +import org.hibernate.sql.model.ast.ColumnValueParameter; +import org.hibernate.sql.model.ast.ColumnWriteFragment; +import org.hibernate.sql.exec.internal.TemporalJdbcParameter; +import org.hibernate.sql.model.ast.builder.MutationGroupBuilder; +import org.hibernate.sql.model.ast.builder.TableInsertBuilder; + +import java.util.function.Consumer; +import java.util.function.Supplier; + +import static java.util.Collections.emptyList; +import static org.hibernate.boot.model.internal.TemporalHelper.ROW_END; +import static org.hibernate.boot.model.internal.TemporalHelper.ROW_START; +import static org.hibernate.query.sqm.ComparisonOperator.GREATER_THAN; +import static org.hibernate.query.sqm.ComparisonOperator.LESS_THAN_OR_EQUAL; + +/** + * Temporal mapping implementation. + * + * @author Gavin King + * + * @since 7.4 + */ +public class TemporalMappingImpl implements TemporalMapping { + private final String tableName; + private final SelectableMapping startingColumnMapping; + private final SelectableMapping endingColumnMapping; + private final JdbcMapping jdbcMapping; + private final String currentTimestampFunctionName; + private final Expression currentTimestampExpression; + private final TemporalTableStrategy temporalTableStrategy; + + public TemporalMappingImpl( + Stateful bootMapping, + String tableName, + MappingModelCreationProcess creationProcess) { + this.tableName = tableName; + + final var creationContext = creationProcess.getCreationContext(); + temporalTableStrategy = + creationContext.getSessionFactoryOptions() + .getTemporalTableStrategy(); + + final var startingColumn = bootMapping.getAuxiliaryColumn( ROW_START ); + final var endingColumn = bootMapping.getAuxiliaryColumn( ROW_END ); + final var startingValue = (BasicValue) startingColumn.getValue(); + final var endingValue = (BasicValue) endingColumn.getValue(); + + final var startingResolution = startingValue.resolve(); + final var endingResolution = endingValue.resolve(); + + jdbcMapping = startingResolution.getJdbcMapping(); + if ( jdbcMapping != endingResolution.getJdbcMapping() ) { + throw new IllegalStateException( "Temporal starting and ending columns must use the same JDBC mapping" ); + } + + final var typeConfiguration = creationContext.getTypeConfiguration(); + final var dialect = creationContext.getDialect(); + final var sessionFactory = creationContext.getSessionFactory(); + final var sqmFunctionRegistry = sessionFactory.getQueryEngine().getSqmFunctionRegistry(); + + startingColumnMapping = SelectableMappingImpl.from( + tableName, + startingColumn, + null, + null, + jdbcMapping, + typeConfiguration, + true, + false, + false, + false, + dialect, + sqmFunctionRegistry, + creationContext + ); + endingColumnMapping = SelectableMappingImpl.from( + tableName, + endingColumn, + null, + null, + jdbcMapping, + typeConfiguration, + true, + true, + false, + false, + dialect, + sqmFunctionRegistry, + creationContext + ); + + if ( sessionFactory.getTransactionIdentifierService().useServerTimestamp( dialect ) ) { + currentTimestampFunctionName = dialect.currentTimestamp(); + currentTimestampExpression = + new SelfRenderingSqlFragmentExpression( currentTimestampFunctionName, jdbcMapping ); + } + else { + currentTimestampFunctionName = null; + currentTimestampExpression = null; + } + } + + @Override + public String getTableName() { + return tableName; + } + + @Override + public SelectableMapping getStartingColumnMapping() { + return startingColumnMapping; + } + + @Override + public SelectableMapping getEndingColumnMapping() { + return endingColumnMapping; + } + + @Override + public JdbcMapping getJdbcMapping() { + return jdbcMapping; + } + + private Predicate createCurrentRestriction(TableReference tableReference) { + return createCurrentRestriction( tableReference, null ); + } + + private Predicate createCurrentRestriction(TableReference tableReference, SqlExpressionResolver expressionResolver) { + final var endingColumn = resolveColumn( tableReference, expressionResolver, endingColumnMapping ); + return new NullnessPredicate( endingColumn, false, jdbcMapping ); + } + + private Predicate createRestriction(TableReference tableReference, Object temporalValue) { + return createRestriction( tableReference, null, temporalValue ); + } + + private Predicate createRestriction( + TableReference tableReference, + SqlExpressionResolver expressionResolver, + Object temporalValue) { + final var startingColumn = resolveColumn( tableReference, expressionResolver, startingColumnMapping ); + final var endingColumn = resolveColumn( tableReference, expressionResolver, endingColumnMapping ); + + final Expression startingTemporalValue; + final Expression endingTemporalValue; + if ( currentTimestampExpression == null || temporalValue != null ) { + startingTemporalValue = new TemporalJdbcParameter( startingColumnMapping ); + endingTemporalValue = new TemporalJdbcParameter( endingColumnMapping ); + } + else { + startingTemporalValue = currentTimestampExpression; + endingTemporalValue = currentTimestampExpression; + } + + final var startingPredicate = new ComparisonPredicate( startingColumn, LESS_THAN_OR_EQUAL, startingTemporalValue ); + final var endingNullPredicate = new NullnessPredicate( endingColumn, false, jdbcMapping ); + final var endingAfterPredicate = new ComparisonPredicate( endingColumn, GREATER_THAN, endingTemporalValue ); + + final var endingPredicate = new Junction( Junction.Nature.DISJUNCTION ); + endingPredicate.add( endingNullPredicate ); + endingPredicate.add( endingAfterPredicate ); + + final var predicate = new Junction( Junction.Nature.CONJUNCTION ); + predicate.add( startingPredicate ); + predicate.add( endingPredicate ); + + return predicate; + } + + @Override + public ColumnValueBinding createStartingValueBinding(ColumnReference startingColumnReference) { + return createTemporalValueBinding( startingColumnReference, startingColumnMapping ); + } + + @Override + public ColumnValueBinding createEndingValueBinding(ColumnReference endingColumnReference) { + return createTemporalValueBinding( endingColumnReference, endingColumnMapping ); + } + + private ColumnValueBinding createTemporalValueBinding( + ColumnReference endingColumnReference, SelectableMapping columnMapping) { + return new ColumnValueBinding( endingColumnReference, + currentTimestampFunctionName != null + ? new ColumnWriteFragment( currentTimestampFunctionName, emptyList(), columnMapping ) + : new ColumnWriteFragment( "?", + new ColumnValueParameter( endingColumnReference ), + columnMapping ) ); + } + + @Override + public ColumnValueBinding createNullEndingValueBinding(ColumnReference endingColumnReference) { + return new ColumnValueBinding( endingColumnReference, + new ColumnWriteFragment( null, emptyList(), endingColumnMapping ) ); + } + + private Expression resolveColumn( + TableReference tableReference, + SqlExpressionResolver expressionResolver, + SelectableMapping selectableMapping) { + return expressionResolver != null + ? expressionResolver.resolveSqlExpression( tableReference, selectableMapping ) + : new ColumnReference( tableReference, selectableMapping ); + } + + @Override + public void addToInsertGroup(MutationGroupBuilder insertGroupBuilder, EntityPersister persister) { + if ( temporalTableStrategy == TemporalTableStrategy.SINGLE_TABLE ) { + final TableInsertBuilder insertBuilder = + insertGroupBuilder.getTableDetailsBuilder( persister.getIdentifierTableName() ); + final var mutatingTable = insertBuilder.getMutatingTable(); + + final var startingColumn = new ColumnReference( mutatingTable, getStartingColumnMapping() ); + insertBuilder.addValueColumn( createStartingValueBinding( startingColumn ) ); + + final var endingColumn = new ColumnReference( mutatingTable, getEndingColumnMapping() ); + insertBuilder.addValueColumn( createNullEndingValueBinding( endingColumn ) ); + } + } + + @Override + public void applyPredicate( + EntityMappingType associatedEntityMappingType, + Consumer predicateConsumer, + LazyTableGroup lazyTableGroup, + NavigablePath navigablePath, + SqlAstCreationState creationState) { + if ( useTemporalRestriction( creationState ) ) { + final var tableReference = lazyTableGroup.resolveTableReference( navigablePath, getTableName() ); + final var temporalInstant = creationState.getLoadQueryInfluencers().getTemporalIdentifier(); + final var resolver = creationState.getSqlExpressionResolver(); + final var temporalPredicate = + temporalInstant == null + ? createCurrentRestriction( tableReference, resolver ) + : createRestriction( tableReference, resolver, temporalInstant ); + predicateConsumer.accept( temporalPredicate ); + } + } + + @Override + public void applyPredicate( + EntityMappingType associatedEntityDescriptor, + Consumer predicateConsumer, + TableGroup tableGroup, + SqlAliasBaseGenerator sqlAliasBaseGenerator, + LoadQueryInfluencers influencers) { + if ( useTemporalRestriction( influencers ) ) { + final var instant = influencers.getTemporalIdentifier(); + final var primaryTableReference = + tableGroup.resolveTableReference( getTableName() ); + predicateConsumer.accept( instant == null + ? createCurrentRestriction( primaryTableReference ) + : createRestriction( primaryTableReference, instant ) ); + } + } + + @Override + public void applyPredicate( + PluralAttributeMapping associatedEntityDescriptor, + Consumer predicateConsumer, + TableGroup tableGroup, + SqlAliasBaseGenerator sqlAliasBaseGenerator, + LoadQueryInfluencers influencers) { + if ( useTemporalRestriction( influencers ) ) { + final var instant = influencers.getTemporalIdentifier(); + final var tableReference = tableGroup.resolveTableReference( getTableName() ); + predicateConsumer.accept( instant == null + ? createCurrentRestriction( tableReference ) + : createRestriction( tableReference, instant ) ); + } + } + + @Override + public void applyPredicate(TableGroupJoin tableGroupJoin, LoadQueryInfluencers loadQueryInfluencers) { + if ( useTemporalRestriction( loadQueryInfluencers ) ) { + final var temporalInstant = loadQueryInfluencers.getTemporalIdentifier(); + final var tableReference = tableGroupJoin.getJoinedGroup().resolveTableReference( getTableName() ); + tableGroupJoin.applyPredicate( temporalInstant == null + ? createCurrentRestriction( tableReference ) + : createRestriction( tableReference, temporalInstant ) ); + } + } + + @Override + public void applyPredicate( + Supplier> predicateCollector, + SqlAstCreationState creationState, + StandardTableGroup tableGroup, + NamedTableReference rootTableReference, + EntityMappingType entityMappingType) { + if ( useTemporalRestriction( creationState ) ) { + final var tableReference = + tableGroup.resolveTableReference( getTableName() ); + final var temporalInstant = + creationState.getLoadQueryInfluencers().getTemporalIdentifier(); + final var resolver = creationState.getSqlExpressionResolver(); + predicateCollector.get() + .accept( temporalInstant == null + ? createCurrentRestriction( tableReference, resolver ) + : createRestriction( tableReference, resolver, temporalInstant ) ); + if ( tableReference != rootTableReference && creationState.supportsEntityNameUsage() ) { + creationState.registerEntityNameUsage( tableGroup, EntityNameUse.EXPRESSION, + entityMappingType.getRootEntityDescriptor().getEntityName() ); + } + } + } + + private static boolean useTemporalRestriction(LoadQueryInfluencers influencers) { + return influencers.getSessionFactory().getJdbcServices().getDialect().getTemporalTableSupport() + .useTemporalRestriction( influencers ); + } + + private boolean useTemporalRestriction(SqlAstCreationState creationState) { + return creationState.getCreationContext().getDialect().getTemporalTableSupport() + .useTemporalRestriction( creationState.getLoadQueryInfluencers() ); + } + + @Override + public boolean useAuxiliaryTable(LoadQueryInfluencers influencers) { + return temporalTableStrategy == TemporalTableStrategy.HISTORY_TABLE + && influencers.getTemporalIdentifier() != null; + } + + @Override + public boolean isAffectedByInfluencers(LoadQueryInfluencers influencers) { + return influencers.getTemporalIdentifier() != null; + } +} 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 b2dd2b44219a..40a87e91ce82 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 @@ -6,8 +6,10 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.AssertionFailure; +import org.hibernate.MappingException; import org.hibernate.annotations.NotFoundAction; import org.hibernate.cache.MutableCacheKeyBuilder; +import org.hibernate.temporal.TemporalTableStrategy; import org.hibernate.engine.FetchStyle; import org.hibernate.engine.FetchTiming; import org.hibernate.engine.spi.LoadQueryInfluencers; @@ -18,12 +20,12 @@ import org.hibernate.internal.util.collections.ArrayHelper; import org.hibernate.mapping.Collection; import org.hibernate.mapping.Component; -import org.hibernate.mapping.Join; import org.hibernate.mapping.ManyToOne; import org.hibernate.mapping.OneToOne; import org.hibernate.mapping.Property; import org.hibernate.mapping.ToOne; import org.hibernate.mapping.Value; +import org.hibernate.metamodel.RepresentationMode; import org.hibernate.metamodel.UnsupportedMappingException; import org.hibernate.metamodel.mapping.AssociationKey; import org.hibernate.metamodel.mapping.AttributeMetadata; @@ -107,6 +109,12 @@ import java.util.function.Consumer; import java.util.function.Supplier; +import static org.hibernate.metamodel.mapping.internal.MappingModelCreationHelper.createInverseModelPart; +import static org.hibernate.metamodel.mapping.internal.MappingModelCreationHelper.getTableIdentifierExpression; +import static org.hibernate.metamodel.mapping.internal.ToOneAttributeMapping.Cardinality.LOGICAL_ONE_TO_ONE; +import static org.hibernate.metamodel.mapping.internal.ToOneAttributeMapping.Cardinality.MANY_TO_ONE; +import static org.hibernate.metamodel.mapping.internal.ToOneAttributeMapping.Cardinality.ONE_TO_ONE; + /** * @author Steve Ebersole */ @@ -168,6 +176,7 @@ public class Entity1 { private boolean canUseParentTableGroup; private @Nullable EmbeddableValuedModelPart circularFetchModelPart; + private final TemporalTableStrategy temporalTableStrategy; /** * For Hibernate Reactive */ @@ -194,7 +203,7 @@ protected ToOneAttributeMapping(ToOneAttributeMapping original) { sideNature = original.sideNature; identifyingColumnsTableExpression = original.identifyingColumnsTableExpression; canUseParentTableGroup = original.canUseParentTableGroup; - + temporalTableStrategy = original.temporalTableStrategy; } public ToOneAttributeMapping( @@ -248,6 +257,32 @@ public ToOneAttributeMapping( declaringType, propertyAccess ); + + + temporalTableStrategy = + declaringEntityPersister.getFactory() + .getSessionFactoryOptions().getTemporalTableStrategy(); + + if ( entityMappingType.getRepresentationStrategy().getMode() == RepresentationMode.POJO ) { + // validate the type. + var declaredType = propertyAccess.getGetter().getReturnTypeClass(); + if ( !Object.class.equals( declaredType ) ) { + final var targetType = entityMappingType.getMappedJavaType().getJavaTypeClass(); + if ( !declaredType.isAssignableFrom( targetType ) ) { + throw new MappingException( + String.format( + Locale.ROOT, + "To-one mapping [%s.%s] was mapped with targetEntity=`%s`, but the attribute is declared as `%s`", + declaringType.getNavigableRole().getFullPath(), + name, + targetType.getName(), + declaredType.getName() + ) + ); + } + } + } + sqlAliasStem = SqlAliasStemHelper.INSTANCE.generateStemFromAttributeName( name ); isNullable = bootValue.isNullable(); isLazy = navigableRole.getParent().getParent() == null @@ -265,21 +300,21 @@ public ToOneAttributeMapping( ); if ( bootValue instanceof ManyToOne manyToOne ) { notFoundAction = manyToOne.getNotFoundAction(); - cardinality = manyToOne.isLogicalOneToOne() - ? Cardinality.LOGICAL_ONE_TO_ONE - : Cardinality.MANY_TO_ONE; + cardinality = + manyToOne.isLogicalOneToOne() + ? LOGICAL_ONE_TO_ONE + : MANY_TO_ONE; final var entityBinding = - manyToOne.getMetadata().getEntityBinding( manyToOne.getReferencedEntityName() ); + manyToOne.getMetadata() + .getEntityBinding( manyToOne.getReferencedEntityName() ); if ( referencedPropertyName == null ) { SelectablePath bidirectionalAttributeName = null; - final String propertyPath = - bootValue.getPropertyName() == null - ? name - : bootValue.getPropertyName(); - if ( cardinality == Cardinality.LOGICAL_ONE_TO_ONE ) { + final String propertyName = bootValue.getPropertyName(); + final String propertyPath = propertyName == null ? name : propertyName; + if ( cardinality == LOGICAL_ONE_TO_ONE ) { boolean hasJoinTable = false; // Handle join table cases - for ( Join join : entityBinding.getJoinClosure() ) { + for ( var join : entityBinding.getJoinClosure() ) { if ( join.getPersistentClass().getEntityName().equals( entityBinding.getEntityName() ) && join.getPropertySpan() == 1 && join.getTable() == manyToOne.getTable() @@ -304,12 +339,13 @@ && equal( join.getKey(), manyToOne ) ) { } else { this.hasJoinTable = false; - bidirectionalAttributeName = findBidirectionalOneToManyAttributeName( - propertyPath, - declaringType, - null, - entityBinding.getPropertyClosure() - ); + bidirectionalAttributeName = + findBidirectionalOneToManyAttributeName( + propertyPath, + declaringType, + null, + entityBinding.getPropertyClosure() + ); } bidirectionalAttributePath = bidirectionalAttributeName; } @@ -317,7 +353,7 @@ && equal( join.getKey(), manyToOne ) ) { // Only set the bidirectional attribute name if the referenced property can actually be circular i.e. an entity type final var property = entityBinding.getProperty( referencedPropertyName ); hasJoinTable = - cardinality == Cardinality.LOGICAL_ONE_TO_ONE + cardinality == LOGICAL_ONE_TO_ONE && property != null && property.getValue() instanceof ManyToOne manyToOneValue && manyToOneValue.isLogicalOneToOne(); @@ -331,7 +367,7 @@ && equal( join.getKey(), manyToOne ) ) { } else { final String targetTableName = - MappingModelCreationHelper.getTableIdentifierExpression( manyToOne.getTable(), + getTableIdentifierExpression( manyToOne.getTable(), declaringEntityPersister.getFactory() ); if ( CollectionPart.Nature.fromNameExact( navigableRole.getParent().getLocalName() ) != null ) { // * the to-one's parent is directly a collection element or index @@ -342,7 +378,9 @@ && equal( join.getKey(), manyToOne ) ) { final var pluralAttribute = (PluralAttributeMapping) declaringEntityPersister.findByPath( path ); assert pluralAttribute != null; - final var persister = (AbstractCollectionPersister) pluralAttribute.getCollectionDescriptor(); + final var persister = + (AbstractCollectionPersister) + pluralAttribute.getCollectionDescriptor(); isKeyTableNullable = !persister.getTableName().equals( targetTableName ); } else { @@ -358,7 +396,7 @@ && equal( join.getKey(), manyToOne ) ) { } else { assert bootValue instanceof OneToOne; - cardinality = Cardinality.ONE_TO_ONE; + cardinality = ONE_TO_ONE; hasJoinTable = false; /* @@ -416,16 +454,15 @@ the navigable path is NavigablePath(Card.fields.{element}.{id}.card) and it does isInternalLoadNullable = isNullable(); } - if ( entityMappingType.getSoftDeleteMapping() != null ) { - // cannot be lazy - if ( getTiming() == FetchTiming.DELAYED ) { - throw new UnsupportedMappingException( String.format( - Locale.ROOT, - "To-one attribute (%s.%s) cannot be mapped as LAZY as its associated entity is defined with @SoftDelete", - declaringType.getPartName(), - getAttributeName() - ) ); - } + if ( entityMappingType.getSoftDeleteMapping() != null + // cannot be lazy + && getTiming() == FetchTiming.DELAYED ) { + throw new UnsupportedMappingException( String.format( + Locale.ROOT, + "To-one attribute (%s.%s) cannot be mapped as LAZY as its associated entity is defined with @SoftDelete", + declaringType.getPartName(), + getAttributeName() + ) ); } if ( referencedPropertyName == null ) { @@ -435,12 +472,13 @@ the navigable path is NavigablePath(Card.fields.{element}.{id}.card) and it does bootValue.getBuildingContext().getMetadataCollector() .getEntityBinding( entityMappingType.getEntityName() ); final var identifierMapper = entityBinding.getIdentifierMapper(); - final Type propertyType = + final var propertyType = identifierMapper == null ? entityBinding.getIdentifier().getType() : identifierMapper.getType(); if ( entityBinding.getIdentifierProperty() == null ) { - if ( propertyType instanceof ComponentType compositeType && compositeType.isEmbedded() + if ( propertyType instanceof ComponentType compositeType + && compositeType.isEmbedded() && compositeType.getPropertyNames().length == 1 ) { targetKeyPropertyName = compositeType.getPropertyNames()[0]; addPrefixedPropertyPaths( @@ -481,7 +519,9 @@ the navigable path is NavigablePath(Card.fields.{element}.{id}.card) and it does final var entityBinding = bootValue.getBuildingContext().getMetadataCollector() .getEntityBinding( entityMappingType.getEntityName() ); - final Type propertyType = entityBinding.getRecursiveProperty( referencedPropertyName ).getType(); + final var propertyType = + entityBinding.getRecursiveProperty( referencedPropertyName ) + .getType(); if ( bootValue.isReferenceToPrimaryKey() ) { this.targetKeyPropertyName = referencedPropertyName; final Set targetKeyPropertyNames = new HashSet<>( 3 ); @@ -500,7 +540,8 @@ the navigable path is NavigablePath(Card.fields.{element}.{id}.card) and it does this.targetKeyPropertyNames = targetKeyPropertyNames; } else { - if ( propertyType instanceof ComponentType compositeType && compositeType.isEmbedded() + if ( propertyType instanceof ComponentType compositeType + && compositeType.isEmbedded() && compositeType.getPropertyNames().length == 1 ) { final Set targetKeyPropertyNames = new HashSet<>( 2 ); this.targetKeyPropertyName = compositeType.getPropertyNames()[0]; @@ -521,13 +562,13 @@ the navigable path is NavigablePath(Card.fields.{element}.{id}.card) and it does else { final Set targetKeyPropertyNames = new HashSet<>( 2 ); this.targetKeyPropertyName = referencedPropertyName; - final String mapsIdAttributeName; - // If there is a "virtual property" for a non-PK join mapping, we try to see if the columns match the - // primary key columns and if so, we add the primary key property name as target key property - if ( ( mapsIdAttributeName = findMapsIdPropertyName( - entityMappingType, - referencedPropertyName - ) ) != null ) { + final String mapsIdAttributeName = + findMapsIdPropertyName( entityMappingType, referencedPropertyName ); + // If there is a "virtual property" for a non-PK join mapping, + // we try to see if the columns match the primary key columns + // and if so, we add the primary key property name as target + // key property + if ( mapsIdAttributeName != null ) { addPrefixedPropertyPaths( targetKeyPropertyNames, mapsIdAttributeName, @@ -558,17 +599,18 @@ private static SelectablePath findBidirectionalOneToManyAttributeName( ManagedMappingType declaringType, SelectablePath parentSelectablePath, java.util.Collection properties) { - for ( Property property : properties ) { - final Value value = property.getValue(); + for ( var property : properties ) { + final var value = property.getValue(); if ( value instanceof Component component ) { - final SelectablePath bidirectionalAttributeName = findBidirectionalOneToManyAttributeName( - propertyPath, - declaringType, - parentSelectablePath == null - ? SelectablePath.parse( property.getName() ) - : parentSelectablePath.append( property.getName() ), - component.getProperties() - ); + final var bidirectionalAttributeName = + findBidirectionalOneToManyAttributeName( + propertyPath, + declaringType, + parentSelectablePath == null + ? SelectablePath.parse( property.getName() ) + : parentSelectablePath.append( property.getName() ), + component.getProperties() + ); if ( bidirectionalAttributeName != null ) { return bidirectionalAttributeName; } @@ -591,8 +633,8 @@ private SelectablePath findBidirectionalOneToOneAttributeName( ManagedMappingType declaringType, SelectablePath parentSelectablePath, java.util.Collection properties) { - for ( Property property : properties ) { - final Value value = property.getValue(); + for ( var property : properties ) { + final var value = property.getValue(); if ( value instanceof Component component ) { final var bidirectionalAttributeName = findBidirectionalOneToOneAttributeName( @@ -611,7 +653,8 @@ else if ( value instanceof OneToOne oneToOne ) { if ( declaringTableGroupProducer.getNavigableRole().getLocalName() .equals( oneToOne.getReferencedEntityName() ) && propertyPath.equals( oneToOne.getMappedByProperty() ) - && oneToOne.getReferencedEntityName().equals( declaringType.getJavaType().getTypeName() ) ) { + && oneToOne.getReferencedEntityName() + .equals( declaringType.getJavaType().getTypeName() ) ) { return parentSelectablePath == null ? SelectablePath.parse( property.getName() ) : parentSelectablePath.append( property.getName() ); @@ -624,13 +667,16 @@ else if ( value instanceof OneToOne oneToOne ) { private static FetchTiming adjustFetchTiming( FetchTiming mappedFetchTiming, ToOne bootValue) { - return bootValue instanceof ManyToOne manyToOne && manyToOne.getNotFoundAction() != null + return bootValue instanceof ManyToOne manyToOne + && manyToOne.getNotFoundAction() != null ? FetchTiming.IMMEDIATE : mappedFetchTiming; } - private static TableGroupProducer resolveDeclaringTableGroupProducer(EntityPersister declaringEntityPersister, NavigableRole navigableRole) { - // Also handle cases where a collection contains an embeddable, that contains an association + private static TableGroupProducer resolveDeclaringTableGroupProducer( + EntityPersister declaringEntityPersister, NavigableRole navigableRole) { + // Also handle cases where a collection contains an embeddable, + // that contains an association NavigableRole parentRole = navigableRole.getParent(); String collectionRole = null; do { @@ -682,6 +728,7 @@ private ToOneAttributeMapping( this.bidirectionalAttributePath = original.bidirectionalAttributePath; this.declaringTableGroupProducer = declaringTableGroupProducer; this.isInternalLoadNullable = original.isInternalLoadNullable; + this.temporalTableStrategy = original.temporalTableStrategy; } private static boolean equal(Value lhsValue, Value rhsValue) { @@ -704,11 +751,10 @@ private static boolean equal(Value lhsValue, Value rhsValue) { static String findMapsIdPropertyName(EntityMappingType entityMappingType, String referencedPropertyName) { final var persister = entityMappingType.getEntityPersister(); - if ( Arrays.equals( persister.getIdentifierColumnNames(), - persister.getPropertyColumnNames( referencedPropertyName ) ) ) { - return persister.getIdentifierPropertyName(); - } - return null; + return Arrays.equals( persister.getIdentifierColumnNames(), + persister.getPropertyColumnNames( referencedPropertyName ) ) + ? persister.getIdentifierPropertyName() + : null; } public static void addPrefixedPropertyPaths( @@ -745,26 +791,17 @@ public static void addPrefixedPropertyNames( targetKeyPropertyNames.add( prefix ); } if ( type instanceof ComponentType componentType ) { - final String[] propertyNames = componentType.getPropertyNames(); - final Type[] componentTypeSubtypes = componentType.getSubtypes(); + final var propertyNames = componentType.getPropertyNames(); + final var componentTypeSubtypes = componentType.getSubtypes(); for ( int i = 0, propertyNamesLength = propertyNames.length; i < propertyNamesLength; i++ ) { final String newPrefix = prefix == null ? propertyNames[i] : prefix + "." + propertyNames[i]; addPrefixedPropertyNames( targetKeyPropertyNames, newPrefix, componentTypeSubtypes[i], factory ); } } else if ( type instanceof EntityType entityType ) { - final Type identifierOrUniqueKeyType = + final var identifierOrUniqueKeyType = entityType.getIdentifierOrUniqueKeyType( factory.getRuntimeMetamodels() ); - final String propertyName; - if ( entityType.isReferenceToPrimaryKey() ) { - propertyName = entityType.getAssociatedEntityPersister( factory ).getIdentifierPropertyName(); - } - else if ( identifierOrUniqueKeyType instanceof EmbeddedComponentType ) { - propertyName = null; - } - else { - propertyName = entityType.getRHSUniqueKeyPropertyName(); - } + final String propertyName = propertyName( factory, entityType, identifierOrUniqueKeyType ); final String newPrefix; final String newPkPrefix; final String newFkPrefix; @@ -803,6 +840,18 @@ else if ( propertyName == null ) { } } + private static @Nullable String propertyName(SessionFactoryImplementor factory, EntityType entityType, Type identifierOrUniqueKeyType) { + if ( entityType.isReferenceToPrimaryKey() ) { + return entityType.getAssociatedEntityPersister( factory ).getIdentifierPropertyName(); + } + else if ( identifierOrUniqueKeyType instanceof EmbeddedComponentType ) { + return null; + } + else { + return entityType.getRHSUniqueKeyPropertyName(); + } + } + public ToOneAttributeMapping copy(ManagedMappingType declaringType, TableGroupProducer declaringTableGroupProducer) { return new ToOneAttributeMapping( this, declaringType, declaringTableGroupProducer ); } @@ -811,7 +860,7 @@ public ToOneAttributeMapping copy(ManagedMappingType declaringType, TableGroupPr public void setForeignKeyDescriptor(ForeignKeyDescriptor foreignKeyDescriptor) { assert identifyingColumnsTableExpression != null; this.foreignKeyDescriptor = foreignKeyDescriptor; - if ( cardinality == Cardinality.ONE_TO_ONE && bidirectionalAttributePath != null ) { + if ( cardinality == ONE_TO_ONE && bidirectionalAttributePath != null ) { sideNature = ForeignKeyDescriptor.Nature.TARGET; } else { @@ -827,7 +876,7 @@ public void setForeignKeyDescriptor(ForeignKeyDescriptor foreignKeyDescriptor) { // Otherwise we need to join to the associated entity table(s) final boolean forceJoin = hasNotFoundAction() || entityMappingType.getSoftDeleteMapping() != null - || ( cardinality == Cardinality.ONE_TO_ONE && isNullable() ); + || cardinality == ONE_TO_ONE && isNullable(); canUseParentTableGroup = ! forceJoin && sideNature == ForeignKeyDescriptor.Nature.KEY && declaringTableGroupProducer.containsTableReference( identifyingColumnsTableExpression ); @@ -842,7 +891,7 @@ && getAssociatedEntityMappingType().getIdentifierMapping() // This is needed if the association entity nests the "inverse" toOne association in the embedded id, // because then, the key part of the foreign key is just a simple value instead of the expected embedded id // when doing delayed creation/querying of target entities. See HHH-19687 for details - circularFetchModelPart = MappingModelCreationHelper.createInverseModelPart( + circularFetchModelPart = createInverseModelPart( identifierMapping, getDeclaringType(), this, @@ -938,12 +987,13 @@ public ModelPart findSubPart(String name, EntityMappingType targetType) { sideNature == ForeignKeyDescriptor.Nature.KEY ? foreignKeyDescriptor.getKeyPart() : foreignKeyDescriptor.getTargetPart(); - if ( fkPart instanceof EmbeddableValuedModelPart && fkPart instanceof VirtualModelPart + if ( fkPart instanceof EmbeddableValuedModelPart modelPart + && fkPart instanceof VirtualModelPart && !EntityIdentifierMapping.ID_ROLE_NAME.equals( name ) && !ForeignKeyDescriptor.PART_NAME.equals( name ) && !ForeignKeyDescriptor.TARGET_PART_NAME.equals( name ) && !fkPart.getPartName().equals( name ) ) { - return ( (ModelPartContainer) fkPart ).findSubPart( name, targetType ); + return modelPart.findSubPart( name, targetType ); } return fkPart; } @@ -964,8 +1014,18 @@ public Fetch resolveCircularFetch( // then there can't be a circular fetch return null; } + final var fetchParentNavigablePath = fetchParent.getNavigablePath(); var parentNavigablePath = fetchablePath.getParent(); - assert parentNavigablePath.equals( fetchParent.getNavigablePath() ); + assert parentNavigablePath.equals( fetchParentNavigablePath ) + || fetchParentNavigablePath instanceof TreatedNavigablePath + && parentNavigablePath.equals( fetchParentNavigablePath.getRealParent() ); + if ( fetchParentNavigablePath instanceof TreatedNavigablePath + && parentNavigablePath.equals( fetchParentNavigablePath.getRealParent() ) ) { + // Children of treated paths report the untreated path as their parent. + // For circular-fetch resolution we need the actual treated fetch-parent path, + // otherwise bidirectional checks compare against the wrong navigable path. + parentNavigablePath = fetchParentNavigablePath; + } // The parent navigable path is {fk} if we are creating the domain result for the foreign key for a circular fetch // In the following example, we create a circular fetch for the composite `Card.field.{id}.card.field` // While creating the domain result for the foreign key of `Card#field`, we run into this condition @@ -992,8 +1052,9 @@ public class PrimaryKey { private Key key; } */ - if ( parentNavigablePath.getLocalName().equals( ForeignKeyDescriptor.PART_NAME ) - || parentNavigablePath.getLocalName().equals( ForeignKeyDescriptor.TARGET_PART_NAME ) ) { + final String localName = parentNavigablePath.getLocalName(); + if ( localName.equals( ForeignKeyDescriptor.PART_NAME ) + || localName.equals( ForeignKeyDescriptor.TARGET_PART_NAME ) ) { // todo (6.0): maybe it's better to have a flag in creation state that marks if we are building a circular fetch domain result already to skip this? return null; } @@ -1061,7 +1122,8 @@ private DomainResult determineCircularKeyResult( if ( circularFetchModelPart != null ) { return circularFetchModelPart.createDomainResult( fetchablePath, - createTableGroupForDelayedFetch( fetchablePath, parentTableGroup, null, creationState ), + createTableGroupForDelayedFetch( fetchablePath, + parentTableGroup, null, creationState ), null, creationState ); @@ -1069,7 +1131,8 @@ private DomainResult determineCircularKeyResult( else if ( sideNature == ForeignKeyDescriptor.Nature.KEY ) { return foreignKeyDescriptor.createKeyDomainResult( fetchablePath, - createTableGroupForDelayedFetch( fetchablePath, parentTableGroup, null, creationState ), + createTableGroupForDelayedFetch( fetchablePath, + parentTableGroup, null, creationState ), fetchParent, creationState ); @@ -1166,7 +1229,7 @@ class EmbeddableB { */ return false; } - else if ( cardinality == Cardinality.MANY_TO_ONE ) { + else if ( cardinality == MANY_TO_ONE ) { /* class Child { @OneToOne(mappedBy = "biologicalChild") @@ -1189,19 +1252,19 @@ class Mother { final var parentPath = grandparentNavigablePath.getParent(); // This can be null for a collection loader if ( parentPath == null ) { - return grandparentNavigablePath.getFullPath().equals( - entityMappingType.findByPath( bidirectionalAttributePath ).getNavigableRole().getFullPath() - ); + return grandparentNavigablePath.getFullPath() + .equals( entityMappingType.findByPath( bidirectionalAttributePath ) + .getNavigableRole().getFullPath() ); } else { // If the parent is null, this is a simple collection fetch of a root, in which case the types must match if ( parentPath.getParent() == null ) { final String entityName = entityMappingType.getPartName(); - return parentPath.getFullPath().startsWith( entityName ) && ( - parentPath.getFullPath().length() == entityName.length() - // Ignore a possible alias - || parentPath.getFullPath().charAt( entityName.length() ) == '(' - ); + final String fullPath = parentPath.getFullPath(); + return fullPath.startsWith( entityName ) + && ( fullPath.length() == entityName.length() + // Ignore a possible alias + || fullPath.charAt( entityName.length() ) == '('); } // If we have a parent, we ensure that the parent is the same as the attribute name else { @@ -1227,7 +1290,7 @@ class Mother { private boolean isParentEmbeddedCollectionPart(DomainResultCreationState creationState, NavigablePath parentNavigablePath) { while ( parentNavigablePath != null ) { - final ModelPart parentModelPart = creationState.resolveModelPart( parentNavigablePath ); + final var parentModelPart = creationState.resolveModelPart( parentNavigablePath ); if ( parentModelPart instanceof EmbeddedCollectionPart ) { return true; } @@ -1361,7 +1424,8 @@ else if ( CollectionPart.Nature.fromNameExact( parentNavigablePath.getLocalName( ); } - if ( entityMappingType.isConcreteProxy() && sideNature == ForeignKeyDescriptor.Nature.TARGET ) { + if ( entityMappingType.isConcreteProxy() + && sideNature == ForeignKeyDescriptor.Nature.TARGET ) { createTableGroupForDelayedFetch( fetchablePath, parentTableGroup, null, creationState ); } @@ -1444,7 +1508,9 @@ private NavigablePath getReferencedNavigablePath( DomainResultCreationState creationState, NavigablePath parentNavigablePath) { NavigablePath referencedNavigablePath = parentNavigablePath.getParent(); - MappingType partMappingType = creationState.resolveModelPart( referencedNavigablePath ).getPartMappingType(); + MappingType partMappingType = + creationState.resolveModelPart( referencedNavigablePath ) + .getPartMappingType(); /* class LineItem { @@ -1500,13 +1566,17 @@ class Level3 { */ while ( !( partMappingType instanceof EntityMappingType entityMapping ) || ( partMappingType != entityMappingType - && !entityMappingType.getEntityPersister().isSubclassEntityName( partMappingType.getMappedJavaType().getTypeName() ) - && !entityMapping.getEntityPersister().isSubclassEntityName( entityMappingType.getEntityName() ) ) ) { + && !entityMappingType.getEntityPersister() + .isSubclassEntityName( partMappingType.getMappedJavaType().getTypeName() ) + && !entityMapping.getEntityPersister() + .isSubclassEntityName( entityMappingType.getEntityName() ) ) ) { referencedNavigablePath = referencedNavigablePath.getParent(); if ( referencedNavigablePath == null ) { return null; } - partMappingType = creationState.resolveModelPart( referencedNavigablePath ).getPartMappingType(); + partMappingType = + creationState.resolveModelPart( referencedNavigablePath ) + .getPartMappingType(); } return referencedNavigablePath; } @@ -1523,12 +1593,13 @@ public EntityFetch generateFetch( final var sqlAstCreationState = creationState.getSqlAstCreationState(); final var fromClauseAccess = sqlAstCreationState.getFromClauseAccess(); - final var parentTableGroup = fromClauseAccess.getTableGroup( fetchParent.getNavigablePath() ); + final var fetchParentNavigablePath = fetchParent.getNavigablePath(); + final var parentTableGroup = fromClauseAccess.getTableGroup( fetchParentNavigablePath ); final var parentNavigablePath = fetchablePath.getParent(); - assert parentNavigablePath.equals( fetchParent.getNavigablePath() ) - || fetchParent.getNavigablePath() instanceof TreatedNavigablePath - && parentNavigablePath.equals( fetchParent.getNavigablePath().getRealParent() ); + assert parentNavigablePath.equals( fetchParentNavigablePath ) + || fetchParentNavigablePath instanceof TreatedNavigablePath + && parentNavigablePath.equals( fetchParentNavigablePath.getRealParent() ); /* In case of selected we are going to add a fetch for the `fetchablePath` only if there is not already a `TableGroupJoin`. @@ -1555,19 +1626,10 @@ 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) */ - final var 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 var side = getSide( creationState ); if ( fetchTiming == FetchTiming.IMMEDIATE && selected - || !creationState.getSqlAstCreationState().isProcedureOrNativeQuery() && needsJoinFetch( side ) ) { + || !creationState.getSqlAstCreationState().isProcedureOrNativeQuery() + && needsJoinFetch( side ) ) { final var tableGroup = determineTableGroupForFetch( fetchablePath, fetchParent, @@ -1645,31 +1707,8 @@ else if ( hasNotFoundAction() */ - final DomainResult keyResult; - if ( side == ForeignKeyDescriptor.Nature.KEY ) { - final var tableGroup = - sideNature == ForeignKeyDescriptor.Nature.KEY - ? createTableGroupForDelayedFetch( fetchablePath, parentTableGroup, null, creationState ) - : parentTableGroup; - keyResult = foreignKeyDescriptor.createKeyDomainResult( - fetchablePath, - tableGroup, - fetchParent, - creationState - ); - } - else { - final var tableGroup = - sideNature == ForeignKeyDescriptor.Nature.TARGET - ? parentTableGroup - : createTableGroupForDelayedFetch( fetchablePath, parentTableGroup, null, creationState ); - keyResult = foreignKeyDescriptor.createTargetDomainResult( - fetchablePath, - tableGroup, - fetchParent, - creationState - ); - } + final var keyResult = + getKeyResult( fetchParent, fetchablePath, creationState, side, parentTableGroup ); final boolean selectByUniqueKey = isSelectByUniqueKey( side ); if ( needsImmediateFetch( fetchTiming ) ) { @@ -1698,19 +1737,68 @@ else if ( hasNotFoundAction() ); } + private DomainResult getKeyResult( + FetchParent fetchParent, + NavigablePath fetchablePath, + DomainResultCreationState creationState, + ForeignKeyDescriptor.Nature side, + TableGroup parentTableGroup) { + if ( side == ForeignKeyDescriptor.Nature.KEY ) { + final var tableGroup = + sideNature == ForeignKeyDescriptor.Nature.KEY + ? createTableGroupForDelayedFetch( fetchablePath, parentTableGroup, null, creationState ) + : parentTableGroup; + return foreignKeyDescriptor.createKeyDomainResult( + fetchablePath, + tableGroup, + fetchParent, + creationState + ); + } + else { + final var tableGroup = + sideNature == ForeignKeyDescriptor.Nature.TARGET + ? parentTableGroup + : createTableGroupForDelayedFetch( fetchablePath, parentTableGroup, null, creationState ); + return foreignKeyDescriptor.createTargetDomainResult( + fetchablePath, + tableGroup, + fetchParent, + creationState + ); + } + } + + private ForeignKeyDescriptor.Nature getSide(DomainResultCreationState creationState) { + final var resolvingKeySideOfForeignKey = creationState.getCurrentlyResolvingForeignKeyPart(); + if ( resolvingKeySideOfForeignKey == ForeignKeyDescriptor.Nature.KEY + && 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 + return ForeignKeyDescriptor.Nature.KEY; + } + else { + return sideNature; + } + } + private boolean needsJoinFetch(ForeignKeyDescriptor.Nature side) { if ( side == ForeignKeyDescriptor.Nature.TARGET ) { - // With composite identifier if the target model part doesn't correspond to the identifier of the target entity mapping - // we must eagerly fetch with a join (subselect would still cause problems). + // With composite identifier if the target model part doesn't correspond + // to the identifier of the target entity mapping, we must eagerly fetch + // with a join (subselect would still cause problems). final var identifier = entityMappingType.getIdentifierMapping(); if ( identifier instanceof BasicEntityIdentifierMappingImpl ) { return false; } - final var 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(); + else { + final var 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(); + } } } @@ -1735,7 +1823,7 @@ else if ( !entityMappingType.isConcreteProxy() ) { return hasNotFoundAction() || entityMappingType.getSoftDeleteMapping() != null || ( !entityMappingType.getEntityPersister().isInstrumented() - && cardinality == Cardinality.ONE_TO_ONE && isOptional ); + && cardinality == ONE_TO_ONE && isOptional ); } else { return false; @@ -1842,17 +1930,19 @@ private boolean isSelectByUniqueKey(ForeignKeyDescriptor.Nature side) { if ( referencedPropertyName == null ) { return false; } + final var identifierMapping = entityMappingType.getIdentifierMapping(); if ( side == ForeignKeyDescriptor.Nature.KEY ) { // case 1.2 return !foreignKeyDescriptor.getNavigableRole() - .equals( entityMappingType.getIdentifierMapping().getNavigableRole() ); + .equals( identifierMapping.getNavigableRole() ); } else { // case 1.1 - // Make sure the entity identifier is not a target key property i.e. this really is a unique key mapping + // Make sure the entity identifier is not a target key property, + // i.e. this really is a unique key mapping return bidirectionalAttributePath != null - && ( !( entityMappingType.getIdentifierMapping() instanceof SingleAttributeIdentifierMapping ) - || !targetKeyPropertyNames.contains( entityMappingType.getIdentifierMapping().getAttributeName() ) ); + && !( identifierMapping instanceof SingleAttributeIdentifierMapping + && targetKeyPropertyNames.contains( identifierMapping.getAttributeName() ) ); } } @@ -1872,16 +1962,20 @@ public DomainResult createSnapshotDomainResult( ); } else { - return new NullDomainResult( foreignKeyDescriptor.getKeyPart().getJavaType() ); + final var javaType = foreignKeyDescriptor.getKeyPart().getJavaType(); + @SuppressWarnings("unchecked") + // Not safe, but that's the fault of the method signature + final var castJavaType = (JavaType) javaType; + return new NullDomainResult<>( castJavaType ); } } - public static class NullDomainResult implements DomainResult { - private final DomainResultAssembler resultAssembler; - private final JavaType resultJavaType; + public static class NullDomainResult implements DomainResult { + private final DomainResultAssembler resultAssembler; + private final JavaType resultJavaType; - public NullDomainResult(JavaType javaType) { - resultAssembler = new NullValueAssembler( javaType ); + public NullDomainResult(JavaType javaType) { + resultAssembler = new NullValueAssembler<>( javaType ); this.resultJavaType = javaType; } @@ -1891,8 +1985,8 @@ public String getResultVariable() { } @Override - public DomainResultAssembler createResultAssembler( - InitializerParent parent, + public DomainResultAssembler createResultAssembler( + InitializerParent parent, AssemblerCreationState creationState) { return resultAssembler; } @@ -1911,14 +2005,16 @@ public void collectValueIndexesToCache(BitSet valueIndexes) { private EntityFetch withRegisteredAssociationKeys( Supplier fetchCreator, DomainResultCreationState creationState) { - final boolean added = creationState.registerVisitedAssociationKey( foreignKeyDescriptor.getAssociationKey() ); + final boolean added = + creationState.registerVisitedAssociationKey( + foreignKeyDescriptor.getAssociationKey() ); AssociationKey additionalAssociationKey = null; - if ( cardinality == Cardinality.LOGICAL_ONE_TO_ONE && bidirectionalAttributePath != null ) { - final var bidirectionalModelPart = entityMappingType.findByPath( bidirectionalAttributePath ); + if ( cardinality == LOGICAL_ONE_TO_ONE && bidirectionalAttributePath != null ) { // Add the inverse association key side as well to be able to resolve to a CircularFetch - if ( bidirectionalModelPart instanceof ToOneAttributeMapping bidirectionalAttribute ) { - assert bidirectionalModelPart.getPartMappingType() == declaringTableGroupProducer; - final AssociationKey secondKey = bidirectionalAttribute.getForeignKeyDescriptor().getAssociationKey(); + if ( entityMappingType.findByPath( bidirectionalAttributePath ) + instanceof ToOneAttributeMapping bidirectionalAttribute ) { + assert bidirectionalAttribute.getPartMappingType() == declaringTableGroupProducer; + final var secondKey = bidirectionalAttribute.getForeignKeyDescriptor().getAssociationKey(); if ( creationState.registerVisitedAssociationKey( secondKey ) ) { additionalAssociationKey = secondKey; } @@ -1930,7 +2026,8 @@ private EntityFetch withRegisteredAssociationKeys( } finally { if ( added ) { - creationState.removeVisitedAssociationKey( foreignKeyDescriptor.getAssociationKey() ); + creationState.removeVisitedAssociationKey( + foreignKeyDescriptor.getAssociationKey() ); } if ( additionalAssociationKey != null ) { creationState.removeVisitedAssociationKey( additionalAssociationKey ); @@ -1964,7 +2061,8 @@ else if ( parentTableGroup.getModelPart() instanceof CollectionPart ) { public boolean isSimpleJoinPredicate(Predicate predicate) { // Since the table group is lazy, the initial predicate is null, // but if we get null here, we can safely assume this will be a simple join predicate - return predicate == null || foreignKeyDescriptor.isSimpleJoinPredicate( predicate ); + return predicate == null + || foreignKeyDescriptor.isSimpleJoinPredicate( predicate ); } @Override @@ -2001,7 +2099,8 @@ public TableGroupJoin createTableGroupJoin( // If a parent is a collection part, there is no custom predicate and the join is INNER or LEFT // we check if this attribute is the map key property to reuse the existing index table group - if ( !addsPredicate && ( joinType == SqlAstJoinType.INNER || joinType == SqlAstJoinType.LEFT ) ) { + if ( !addsPredicate + && ( joinType == SqlAstJoinType.INNER || joinType == SqlAstJoinType.LEFT ) ) { TableGroup parentTableGroup = lhs; ModelPartContainer parentContainer = lhs.getModelPart(); StringBuilder embeddablePathSb = null; @@ -2012,11 +2111,11 @@ public TableGroupJoin createTableGroupJoin( embeddablePathSb = new StringBuilder(); } embeddablePathSb.insert( 0, parentContainer.getPartName() + "." ); - final NavigablePath parentNavigablePath = parentTableGroup.getNavigablePath(); - final TableGroup tableGroup = fromClauseAccess.findTableGroup( parentNavigablePath.getParent() ); + final var parentNavigablePath = parentTableGroup.getNavigablePath(); + final var tableGroup = fromClauseAccess.findTableGroup( parentNavigablePath.getParent() ); if ( tableGroup == null ) { assert parentNavigablePath.getLocalName().equals( ForeignKeyDescriptor.PART_NAME ) - || parentNavigablePath.getLocalName().equals( ForeignKeyDescriptor.TARGET_PART_NAME ); + || parentNavigablePath.getLocalName().equals( ForeignKeyDescriptor.TARGET_PART_NAME ); // Might happen that we don't register a table group for the collection role if this is a // foreign key part and the collection is delayed. We can just break out in this case though, // since these checks here are only for reusing a map key property, which we won't have @@ -2031,8 +2130,11 @@ public TableGroupJoin createTableGroupJoin( } if ( CollectionPart.Nature.ELEMENT.getName().equals( parentTableGroup.getNavigablePath().getLocalName() ) ) { - final var parentParentPath = parentTableGroup.getNavigablePath().getParent(); - final var pluralTableGroup = (PluralTableGroup) fromClauseAccess.findTableGroup( parentParentPath ); + final var parentParentPath = + parentTableGroup.getNavigablePath().getParent(); + final var pluralTableGroup = + (PluralTableGroup) + fromClauseAccess.findTableGroup( parentParentPath ); if ( pluralTableGroup != null ) { final String indexPropertyName = pluralTableGroup.getModelPart().getIndexMetadata() @@ -2055,8 +2157,7 @@ public TableGroupJoin createTableGroupJoin( fetched, pluralTableGroup, this - ), - null + ) ); } } @@ -2076,13 +2177,14 @@ public TableGroupJoin createTableGroupJoin( final var join = new TableGroupJoin( navigablePath, - // 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 + // 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 + lazyTableGroup ); final var lhsTableReference = lhs.resolveTableReference( navigablePath, @@ -2134,17 +2236,16 @@ public TableGroupJoin createTableGroupJoin( associatedEntityMappingType.applyDiscriminator( null, null, tableGroup, creationState ); } - final var softDeleteMapping = associatedEntityMappingType.getSoftDeleteMapping(); - if ( softDeleteMapping != null ) { - // add the restriction - final TableReference tableReference = lazyTableGroup.resolveTableReference( + final var auxiliaryMapping = + associatedEntityMappingType.getAuxiliaryMapping(); + if ( auxiliaryMapping != null ) { + auxiliaryMapping.applyPredicate( + associatedEntityMappingType, + join::applyPredicate, + lazyTableGroup, navigablePath, - associatedEntityMappingType.getSoftDeleteTableDetails().getTableName() + creationState ); - join.applyPredicate( softDeleteMapping.createNonDeletedRestriction( - tableReference, - creationState.getSqlExpressionResolver() - ) ); } } ); @@ -2175,14 +2276,13 @@ public LazyTableGroup createRootTableGroupJoin( boolean fetched, @Nullable Consumer predicateConsumer, SqlAstCreationState creationState) { - final SqlAliasBase sqlAliasBase = SqlAliasBase.from( + final var sqlAliasBase = SqlAliasBase.from( explicitSqlAliasBase, explicitSourceAlias, this, creationState.getSqlAliasBaseGenerator() ); - final var softDeleteMapping = getAssociatedEntityMappingType().getSoftDeleteMapping(); final boolean canUseInnerJoin; final var currentlyProcessingJoinType = creationState instanceof SqmToSqlAstConverter sqmToSqlAstConverter @@ -2241,7 +2341,8 @@ public LazyTableGroup createRootTableGroupJoin( tableGroupProducer, explicitSourceAlias, sqlAliasBase, - creationState.getCreationContext().getSessionFactory(), + creationState.getCreationContext() + .getSessionFactory(), lhs ); @@ -2258,13 +2359,18 @@ public LazyTableGroup createRootTableGroupJoin( ) ); - if ( fetched && softDeleteMapping != null ) { - // add the restriction - final var tableReference = - lazyTableGroup.resolveTableReference( navigablePath, - getAssociatedEntityMappingType().getSoftDeleteTableDetails().getTableName() ); - predicateConsumer.accept( softDeleteMapping.createNonDeletedRestriction( tableReference, - creationState.getSqlExpressionResolver() ) ); + if ( fetched ) { + final var auxiliaryMapping = + getAssociatedEntityMappingType().getAuxiliaryMapping(); + if ( auxiliaryMapping != null ) { + auxiliaryMapping.applyPredicate( + getAssociatedEntityMappingType(), + predicateConsumer, + lazyTableGroup, + navigablePath, + creationState + ); + } } } @@ -2290,7 +2396,8 @@ public boolean canUseParentTableGroup( } private void initializeIfNeeded(TableGroup lhs, SqlAstJoinType sqlAstJoinType, TableGroup tableGroup) { - if ( sqlAstJoinType == SqlAstJoinType.INNER && ( isNullable || !lhs.canUseInnerJoins() ) ) { + if ( sqlAstJoinType == SqlAstJoinType.INNER + && ( isNullable || !lhs.canUseInnerJoins() ) ) { if ( hasJoinTable ) { // Set the join type of the table reference join to INNER to retain cardinality expectation final var lhsTableReference = @@ -2423,7 +2530,7 @@ public int breakDownJdbcValues( Y y, JdbcValueBiConsumer valueConsumer, SharedSessionContractImplementor session) { - if ( cardinality == Cardinality.ONE_TO_ONE && sideNature == ForeignKeyDescriptor.Nature.TARGET ) { + if ( cardinality == ONE_TO_ONE && sideNature == ForeignKeyDescriptor.Nature.TARGET ) { return 0; } @@ -2462,7 +2569,7 @@ protected static Object extractAttributePathValue(Object domainValue, EntityMapp Object value = domainValue; ManagedMappingType managedType = entityType; - final String[] pathParts = StringHelper.split( ".", attributePath ); + final var pathParts = StringHelper.split( ".", attributePath ); for ( int i = 0; i < pathParts.length; i++ ) { assert managedType != null; @@ -2480,12 +2587,9 @@ protected static Object extractAttributePathValue(Object domainValue, EntityMapp @Override public int forEachSelectable(int offset, SelectableConsumer consumer) { - if ( sideNature == ForeignKeyDescriptor.Nature.KEY ) { - return foreignKeyDescriptor.visitKeySelectables( offset, consumer ); - } - else { - return 0; - } + return sideNature == ForeignKeyDescriptor.Nature.KEY + ? foreignKeyDescriptor.visitKeySelectables( offset, consumer ) + : 0; } @Override @@ -2516,20 +2620,16 @@ public void applySqlSelections( @Override public String getContainingTableExpression() { - if ( sideNature == ForeignKeyDescriptor.Nature.KEY ) { - return foreignKeyDescriptor.getKeyTable(); - } - else { - return foreignKeyDescriptor.getTargetTable(); - } + return sideNature == ForeignKeyDescriptor.Nature.KEY + ? foreignKeyDescriptor.getKeyTable() + : foreignKeyDescriptor.getTargetTable(); } @Override public int getJdbcTypeCount() { - if ( sideNature == ForeignKeyDescriptor.Nature.KEY ) { - return foreignKeyDescriptor.getJdbcTypeCount(); - } - return 0; + return sideNature == ForeignKeyDescriptor.Nature.KEY + ? foreignKeyDescriptor.getJdbcTypeCount() + : 0; } @Override @@ -2539,10 +2639,9 @@ public JdbcMapping getJdbcMapping(final int index) { @Override public SelectableMapping getSelectable(int columnIndex) { - if ( sideNature == ForeignKeyDescriptor.Nature.KEY ) { - return foreignKeyDescriptor.getSelectable( columnIndex ); - } - return null; + return sideNature == ForeignKeyDescriptor.Nature.KEY + ? foreignKeyDescriptor.getSelectable( columnIndex ) + : null; } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/UnifiedAnyDiscriminatorConverter.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/UnifiedAnyDiscriminatorConverter.java index 5d022bc820e8..08661756f12c 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/UnifiedAnyDiscriminatorConverter.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/UnifiedAnyDiscriminatorConverter.java @@ -7,6 +7,7 @@ import org.hibernate.HibernateException; import org.hibernate.metamodel.internal.FullNameImplicitDiscriminatorStrategy; import org.hibernate.metamodel.mapping.DiscriminatorConverter; +import org.hibernate.metamodel.mapping.DiscriminatorValue; import org.hibernate.metamodel.mapping.DiscriminatorValueDetails; import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.model.domain.NavigableRole; @@ -22,16 +23,16 @@ import static org.hibernate.Hibernate.unproxy; import static org.hibernate.internal.util.collections.CollectionHelper.concurrentMap; -import static org.hibernate.persister.entity.DiscriminatorHelper.NOT_NULL_DISCRIMINATOR; -import static org.hibernate.persister.entity.DiscriminatorHelper.NULL_DISCRIMINATOR; +import static org.hibernate.metamodel.mapping.DiscriminatorValue.Special.NOT_NULL; +import static org.hibernate.metamodel.mapping.DiscriminatorValue.Special.NULL; /** * @author Steve Ebersole */ public class UnifiedAnyDiscriminatorConverter extends DiscriminatorConverter { private final NavigableRole discriminatorRole; - private final Map detailsByValue; - private final Map detailsByEntityName; + private final Map detailsByValue; + private final Map detailsByEntityName; private final ImplicitDiscriminatorStrategy implicitValueStrategy; private final MappingMetamodelImplementor mappingMetamodel; @@ -39,7 +40,7 @@ public UnifiedAnyDiscriminatorConverter( NavigableRole discriminatorRole, JavaType domainJavaType, JavaType relationalJavaType, - Map explicitValueMappings, + Map explicitValueMappings, ImplicitDiscriminatorStrategy implicitValueStrategy, MappingMetamodelImplementor mappingMetamodel) { super( discriminatorRole.getFullPath(), domainJavaType, relationalJavaType ); @@ -57,14 +58,20 @@ public UnifiedAnyDiscriminatorConverter( } ); } - private ImplicitDiscriminatorStrategy resolveImplicitValueStrategy(ImplicitDiscriminatorStrategy implicitValueStrategy, Map explicitValueMappings) { + public boolean hasImplicitValueStrategy() { + return implicitValueStrategy != null; + } + + private ImplicitDiscriminatorStrategy resolveImplicitValueStrategy( + ImplicitDiscriminatorStrategy implicitValueStrategy, + Map explicitValueMappings) { if ( explicitValueMappings.isEmpty() ) { if ( implicitValueStrategy == null ) { return FullNameImplicitDiscriminatorStrategy.FULL_NAME_STRATEGY; } } else { - if ( explicitValueMappings.containsKey( NOT_NULL_DISCRIMINATOR ) ) { + if ( explicitValueMappings.containsKey( NOT_NULL ) ) { if ( implicitValueStrategy != null ) { // we will ultimately not know how to handle "implicit" values which are non-null throw new HibernateException( "Illegal use of ImplicitDiscriminatorStrategy with explicit non-null discriminator mapping: " + discriminatorRole.getFullPath() ); @@ -74,54 +81,50 @@ private ImplicitDiscriminatorStrategy resolveImplicitValueStrategy(ImplicitDiscr return implicitValueStrategy; } - private DiscriminatorValueDetails register(Object value, EntityMappingType entityMapping) { + private DiscriminatorValueDetails register(DiscriminatorValue value, EntityMappingType entityMapping) { final var details = new DiscriminatorValueDetailsImpl( value, entityMapping ); detailsByValue.put( value, details ); detailsByEntityName.put( entityMapping.getEntityName(), details ); return details; } - public Map getDetailsByValue() { - return detailsByValue; - } - - public Map getDetailsByEntityName() { - return detailsByEntityName; - } - @Override public DiscriminatorValueDetails getDetailsForDiscriminatorValue(Object relationalValue) { if ( relationalValue == null ) { - return detailsByValue.get( NULL_DISCRIMINATOR ); + // return immediately to avoid falling through to NOT_NULL case + return detailsByValue.get( NULL ); } + else { + final var existing = detailsByValue.get( DiscriminatorValue.of( relationalValue ) ); + if ( existing != null ) { + return existing; + } - final var existing = detailsByValue.get( relationalValue ); - if ( existing != null ) { - return existing; - } + if ( relationalValue.getClass().isEnum() ) { + final Object enumValue = enumValue( (Enum) relationalValue ); + final var enumMatch = detailsByValue.get( DiscriminatorValue.of( enumValue ) ); + if ( enumMatch != null ) { + return enumMatch; + } + } - if ( relationalValue.getClass().isEnum() ) { - final Object enumValue = enumValue( (Enum) relationalValue ); - final var enumMatch = detailsByValue.get( enumValue ); - if ( enumMatch != null ) { - return enumMatch; + if ( implicitValueStrategy != null ) { + final var entityMapping = + implicitValueStrategy.toEntityMapping( relationalValue, + discriminatorRole, mappingMetamodel ); + if ( entityMapping != null ) { + return register( DiscriminatorValue.of( relationalValue ), entityMapping ); + } } - } - if ( implicitValueStrategy != null ) { - final var entityMapping = - implicitValueStrategy.toEntityMapping( relationalValue, discriminatorRole, mappingMetamodel ); - if ( entityMapping != null ) { - return register( relationalValue, entityMapping ); + final var nonNullMatch = detailsByValue.get( NOT_NULL ); + if ( nonNullMatch != null ) { + return nonNullMatch; } - } - final var nonNullMatch = detailsByValue.get( NOT_NULL_DISCRIMINATOR ); - if ( nonNullMatch != null ) { - return nonNullMatch; + throw new HibernateException( + "Unknown discriminator value for " + discriminatorRole.getFullPath() + ": " + relationalValue ); } - - throw new HibernateException( "Unknown discriminator value (" + discriminatorRole.getFullPath() + ") : " + relationalValue ); } private Object enumValue(Enum relationalEnum) { @@ -140,7 +143,7 @@ else if ( relationalJavaType instanceof CharacterJavaType ) { @Override public DiscriminatorValueDetails getDetailsForEntityName(String entityName) { final var existing = detailsByEntityName.get( entityName ); - if ( existing != null) { + if ( existing != null ) { return existing; } @@ -152,7 +155,7 @@ public DiscriminatorValueDetails getDetailsForEntityName(String entityName) { discriminatorRole, mappingMetamodel ); - return register( discriminatorValue, entityMapping ); + return register( DiscriminatorValue.of( discriminatorValue ), entityMapping ); } throw new HibernateException( "Cannot determine discriminator value from entity-name (" + discriminatorRole.getFullPath() + ") : " + entityName ); @@ -163,6 +166,13 @@ public void forEachValueDetail(Consumer consumer) { detailsByEntityName.values().forEach( consumer ); } + @Override + @SuppressWarnings("unchecked") + public R toRelationalValue(O domainForm) { + final String entityName = getEntityName( domainForm ); + return entityName == null ? null : (R) getDetailsForEntityName( entityName ).getValue(); + } + @Override public X fromValueDetails(Function handler) { for ( var valueDetails : detailsByEntityName.values() ) { diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/VirtualIdEmbeddable.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/VirtualIdEmbeddable.java index f1f69e3f4a3c..cc577960c98a 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/VirtualIdEmbeddable.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/VirtualIdEmbeddable.java @@ -54,7 +54,8 @@ public VirtualIdEmbeddable( ); final var compositeType = virtualIdSource.getType(); - ( (CompositeTypeImplementor) compositeType ).injectMappingModelPart( idMapping, creationProcess ); + ( (CompositeTypeImplementor) compositeType ) + .injectMappingModelPart( idMapping, creationProcess ); creationProcess.registerInitializationCallback( "VirtualIdEmbeddable(" + navigableRole.getFullPath() + ")#finishInitialization", diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/JpaMetamodel.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/JpaMetamodel.java index ca496516c904..cedc919759e5 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/JpaMetamodel.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/JpaMetamodel.java @@ -34,7 +34,7 @@ public interface JpaMetamodel extends Metamodel { /** * Access to a managed type through its name */ - ManagedDomainType managedType(String typeName); + ManagedDomainType managedType(String typeName); /** * Access to an entity supporting Hibernate's entity-name feature @@ -50,13 +50,13 @@ public interface JpaMetamodel extends Metamodel { * Specialized handling for resolving entity-name references in * an HQL query */ - EntityDomainType getHqlEntityReference(String entityName); + EntityDomainType getHqlEntityReference(String entityName); /** * Specialized handling for resolving entity-name references in * an HQL query */ - EntityDomainType resolveHqlEntityReference(String entityName); + EntityDomainType resolveHqlEntityReference(String entityName); /** * Same as {@link #managedType(Class)} except {@code null} is returned rather @@ -80,7 +80,7 @@ public interface JpaMetamodel extends Metamodel { * Same as {@link #managedType(String)} except {@code null} is returned rather * than throwing an exception */ - @Nullable ManagedDomainType findManagedType(@Nullable String typeName); + @Nullable ManagedDomainType findManagedType(@Nullable String typeName); /** * Same as {@link #entity(String)} except {@code null} is returned rather @@ -107,7 +107,7 @@ public interface JpaMetamodel extends Metamodel { JavaType getJavaConstantType(String className, String fieldName); - T getJavaConstant(String className, String fieldName); + @Nullable E getJavaConstant(String className, String fieldName, Class javaTypeClass); // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Covariant returns diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/NavigableRole.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/NavigableRole.java index 51b1f0848042..29ec99b46375 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/NavigableRole.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/NavigableRole.java @@ -16,7 +16,7 @@ /** * A compound path which represents a {@link org.hibernate.metamodel.mapping.ModelPart} * and uniquely identifies it with the runtime metamodel. - *

        + *

        * The {@linkplain #isRoot root} will name either an * {@linkplain org.hibernate.metamodel.MappingMetamodel#getEntityDescriptor entity} or * {@linkplain org.hibernate.metamodel.MappingMetamodel#getCollectionDescriptor collection}. diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AbstractAttribute.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AbstractAttribute.java index 22b999de5b4c..93661361b225 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AbstractAttribute.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AbstractAttribute.java @@ -11,11 +11,10 @@ import jakarta.persistence.metamodel.Attribute; import org.hibernate.metamodel.AttributeClassification; -import org.hibernate.metamodel.mapping.CollectionPart; import org.hibernate.metamodel.model.domain.DomainType; import org.hibernate.metamodel.model.domain.EntityDomainType; import org.hibernate.metamodel.model.domain.ManagedDomainType; -import org.hibernate.metamodel.model.domain.PluralPersistentAttribute; +import org.hibernate.query.sqm.spi.SqmCreationHelper; import org.hibernate.query.sqm.tree.domain.SqmDomainType; import org.hibernate.query.sqm.tree.domain.SqmPath; import org.hibernate.query.sqm.tree.domain.SqmPersistentAttribute; @@ -63,8 +62,11 @@ public String getName() { @Override public Class getJavaType() { - return valueType instanceof BasicTypeImpl basicType - ? basicType.getJavaType() + // TODO: create a new method to abstract this logic + return valueType instanceof BasicTypeImpl basicType + // handles primitives in basic types + ? (Class) basicType.getJavaType() + // good for everything else : attributeJtd.getJavaTypeClass(); } @@ -105,11 +107,7 @@ public DomainType getValueGraphType() { NavigablePath getParentNavigablePath(SqmPath parent) { final var parentPathSource = parent.getResolvedModel(); final var parentType = parentPathSource.getPathType(); - final NavigablePath parentNavigablePath = - parentPathSource instanceof PluralPersistentAttribute - // for collections, implicitly navigate to the element - ? parent.getNavigablePath().append( CollectionPart.Nature.ELEMENT.getName() ) - : parent.getNavigablePath(); + final NavigablePath parentNavigablePath = SqmCreationHelper.buildParentNavigablePath( parent, "" ); if ( parentType != declaringType && parentType instanceof EntityDomainType entityDomainType && entityDomainType.findAttribute( name ) == null ) { diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AbstractIdentifiableType.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AbstractIdentifiableType.java index e3edd41e03c2..658ea8fd557e 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AbstractIdentifiableType.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AbstractIdentifiableType.java @@ -34,8 +34,6 @@ import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.descriptor.java.spi.PrimitiveJavaType; -import org.jboss.logging.Logger; - import static java.util.Collections.emptyList; /** @@ -114,14 +112,15 @@ public boolean hasSingleIdAttribute() { } @Override - @SuppressWarnings("unchecked") public SqmSingularPersistentAttribute getId(Class javaType) { ensureNoIdClass(); final var id = findIdAttribute(); if ( id != null ) { checkType( id, javaType ); } - return (SqmSingularPersistentAttribute) id; + @SuppressWarnings("unchecked") // safe, we just checked + final var castId = (SqmSingularPersistentAttribute) id; + return castId; } private void ensureNoIdClass() { @@ -148,8 +147,7 @@ else if ( getSuperType() != null ) { private void checkType(SingularPersistentAttribute attribute, Class javaType) { if ( !javaType.isAssignableFrom( attribute.getType().getJavaType() ) ) { - final JavaType attributeJavaType = attribute.getAttributeJavaType(); - if ( !( attributeJavaType instanceof PrimitiveJavaType primitiveJavaType ) + if ( !( attribute.getAttributeJavaType() instanceof PrimitiveJavaType primitiveJavaType ) || primitiveJavaType.getPrimitiveClass() != javaType ) { throw new IllegalArgumentException( String.format( @@ -165,14 +163,15 @@ private void checkType(SingularPersistentAttribute attribute, Class jav } @Override - @SuppressWarnings("unchecked") public SqmSingularPersistentAttribute getDeclaredId(Class javaType) { ensureNoIdClass(); if ( id == null ) { throw new IllegalArgumentException( "The id attribute is not declared on this type [" + getTypeName() + "]" ); } checkType( id, javaType ); - return (SqmSingularPersistentAttribute) id; + @SuppressWarnings("unchecked") // safe, we just checked + final var castId = (SqmSingularPersistentAttribute) id; + return castId; } @Override @@ -226,14 +225,16 @@ else if ( idClassType instanceof SimpleDomainType simpleDomainType ) { } @Override - @SuppressWarnings("unchecked") public void visitIdClassAttributes(Consumer> attributeConsumer) { if ( nonAggregatedIdAttributes != null ) { nonAggregatedIdAttributes.forEach( attributeConsumer ); } - else if ( getSuperType() != null ) { - //noinspection rawtypes - getSuperType().visitIdClassAttributes( (Consumer) attributeConsumer ); + else { + final var superType = getSuperType(); + if ( superType != null ) { + //noinspection rawtypes, unchecked + superType.visitIdClassAttributes( (Consumer) attributeConsumer ); + } } } @@ -247,14 +248,15 @@ public boolean hasDeclaredVersionAttribute() { } @Override - @SuppressWarnings("unchecked") public SingularPersistentAttribute getVersion(Class javaType) { if ( hasVersionAttribute() ) { final var version = findVersionAttribute(); if ( version != null ) { checkType( version, javaType ); } - return (SingularPersistentAttribute) version; + @SuppressWarnings("unchecked") // safe, we just checked + final var castVersion = (SingularPersistentAttribute) version; + return castVersion; } else { return null; @@ -288,21 +290,27 @@ else if ( getSuperType() != null ) { } @Override - @SuppressWarnings("unchecked") public SingularPersistentAttribute getDeclaredVersion(Class javaType) { checkDeclaredVersion(); checkType( versionAttribute, javaType ); - return (SingularPersistentAttribute) versionAttribute; + @SuppressWarnings("unchecked") // safe, we just checked + final var castVersion = (SingularPersistentAttribute) versionAttribute; + return castVersion; } private void checkDeclaredVersion() { - if ( versionAttribute == null || ( getSuperType() != null && getSuperType().hasVersionAttribute() )) { + if ( versionAttribute == null || supertypeDeclaresVersion() ) { throw new IllegalArgumentException( "The version attribute is not declared by this type [" + getJavaType() + "]" ); } } + private boolean supertypeDeclaresVersion() { + final var superType = getSuperType(); + return superType != null && superType.hasVersionAttribute(); + } + // @Override // public void visitJdbcTypes(Consumer action, TypeConfiguration typeConfiguration) { // id.visitJdbcTypes( action, typeConfiguration ); @@ -341,7 +349,7 @@ public void applyIdAttribute(SingularPersistentAttribute idAttribute) { @Override public void applyNonAggregatedIdAttributes( - Set> idAttributes, + Set> idAttributes, EmbeddableDomainType idClassType) { if ( id != null ) { throw new IllegalArgumentException( "`AbstractIdentifiableType#id` already set on call to `#applyNonAggregatedIdAttribute`" ); @@ -360,9 +368,8 @@ public void applyNonAggregatedIdAttributes( nonAggregatedIdAttributes.add( (SqmSingularPersistentAttribute) idAttribute ); if ( AbstractIdentifiableType.this == idAttribute.getDeclaringType() ) { @SuppressWarnings("unchecked") - // Safe, because we know it's declared by this type - final PersistentAttribute declaredAttribute = - (PersistentAttribute) idAttribute; + // Safe, because we know it's declared by this type + final var declaredAttribute = (PersistentAttribute) idAttribute; addAttribute( declaredAttribute ); } } @@ -402,11 +409,7 @@ public void finishUp() { } } - private static final Logger LOG = Logger.getLogger( AbstractIdentifiableType.class ); - private SqmPathSource interpretIdDescriptor() { - LOG.tracef( "Interpreting domain-model identifier descriptor" ); - final var superType = getSuperType(); if ( superType != null ) { final var idDescriptor = superType.getIdentifierDescriptor(); @@ -425,11 +428,10 @@ else if ( nonAggregatedIdAttributes != null && !nonAggregatedIdAttributes.isEmpt else { if ( isIdMappingRequired() ) { throw new UnsupportedMappingException( - "Could not build SqmPathSource for entity identifier : " + getTypeName() ); + "Could not build SqmPathSource for entity identifier: " + getTypeName() ); } return null; } - } private AbstractSqmPathSource compositePathSource() { diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AbstractManagedType.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AbstractManagedType.java index f9c319934dd6..c47ce142886d 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AbstractManagedType.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AbstractManagedType.java @@ -126,8 +126,9 @@ public RepresentationMode getRepresentationMode() { @Override public void visitAttributes(Consumer> action) { visitDeclaredAttributes( action ); - if ( getSuperType() != null ) { - getSuperType().visitAttributes( action ); + final var superType = getSuperType(); + if ( superType != null ) { + superType.visitAttributes( action ); } } @@ -317,9 +318,9 @@ private SqmSingularPersistentAttribute checkTypeForSingleAttribute( Class javaType) { if ( attribute == null || !hasMatchingReturnType( attribute, javaType ) ) { throw new IllegalArgumentException( - "SingularAttribute named " + name - + ( javaType != null ? " and of type " + javaType.getName() : "" ) - + " is not present" + "No singular attribute named '" + name + + ( javaType != null ? "' and of type '" + javaType.getName() : "" ) + + "' in type '" + hibernateTypeName + "'" ); } else { @@ -417,9 +418,9 @@ private void checkTypeForPluralAttributes( || elementType != null && !attribute.getBindableJavaType().equals( elementType ) || attribute.getCollectionType() != collectionType ) { throw new IllegalArgumentException( - attributeType + " named " + name - + ( elementType != null ? " and of element type " + elementType : "" ) - + " is not present" + "No plural attribute named '" + name + + ( elementType != null ? "' and of element type '" + elementType.getName() : "" ) + + "' in type '" + hibernateTypeName + "'" ); } } 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 8fcf2003a79a..4c085a9b4546 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 @@ -125,8 +125,9 @@ public SimpleDomainType getKeyGraphType() { @Override public boolean isAssociation() { - return getPersistentAttributeType() == PersistentAttributeType.ONE_TO_MANY - || getPersistentAttributeType() == PersistentAttributeType.MANY_TO_MANY; + final var persistentAttributeType = getPersistentAttributeType(); + return persistentAttributeType == PersistentAttributeType.ONE_TO_MANY + || persistentAttributeType == PersistentAttributeType.MANY_TO_MANY; } @Override @@ -147,8 +148,8 @@ public Class getBindableJavaType() { @SuppressWarnings("unchecked") @Override public SqmPath createSqmPath(SqmPath lhs, @Nullable SqmPathSource intermediatePathSource) { - // We need an unchecked cast here : PluralPersistentAttribute implements path source with its element type - // but resolving paths from it must produce collection-typed expressions. + // We need an unchecked cast here: PluralPersistentAttribute implements PathSource with + // its element type, but resolving paths from it must produce collection-typed expressions. return (SqmPath) new SqmPluralValuedSimplePath<>( PathHelper.append( lhs, this, intermediatePathSource ), this, diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AnyDiscriminatorSqmPath.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AnyDiscriminatorSqmPath.java index 3fbe0ec4040e..03b74253ba57 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AnyDiscriminatorSqmPath.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AnyDiscriminatorSqmPath.java @@ -29,7 +29,7 @@ protected AnyDiscriminatorSqmPath( @Override public AnyDiscriminatorSqmPath copy(SqmCopyContext context) { - final AnyDiscriminatorSqmPath existing = context.getCopy( this ); + final var existing = context.getCopy( this ); if ( existing != null ) { return existing; } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AnyDiscriminatorSqmPathSource.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AnyDiscriminatorSqmPathSource.java index a49b01080e81..64ea52e4f7d9 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AnyDiscriminatorSqmPathSource.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AnyDiscriminatorSqmPathSource.java @@ -12,7 +12,6 @@ import org.hibernate.query.sqm.SqmPathSource; import org.hibernate.query.sqm.tree.domain.SqmDomainType; import org.hibernate.query.sqm.tree.domain.SqmPath; -import org.hibernate.spi.NavigablePath; import org.hibernate.type.BasicType; import org.hibernate.type.descriptor.java.JavaType; @@ -38,10 +37,11 @@ public AnyDiscriminatorSqmPathSource( @Override public SqmPath createSqmPath(SqmPath lhs, @Nullable SqmPathSource intermediatePathSource) { - final NavigablePath navigablePath = + final var path = lhs.getNavigablePath(); + final var navigablePath = intermediatePathSource == null - ? lhs.getNavigablePath() - : lhs.getNavigablePath().append( intermediatePathSource.getPathName() ); + ? path + : path.append( intermediatePathSource.getPathName() ); return new AnyDiscriminatorSqmPath<>( navigablePath, pathModel, lhs, lhs.nodeBuilder() ); } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AnyMappingDomainTypeImpl.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AnyMappingDomainTypeImpl.java index 81e7b9977877..1145ed937e8a 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AnyMappingDomainTypeImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AnyMappingDomainTypeImpl.java @@ -6,7 +6,6 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.mapping.Any; -import org.hibernate.mapping.Column; import org.hibernate.metamodel.model.domain.AnyMappingDomainType; import org.hibernate.metamodel.model.domain.NavigableRole; import org.hibernate.metamodel.model.domain.SimpleDomainType; @@ -18,8 +17,6 @@ import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.internal.ConvertedBasicTypeImpl; -import java.util.List; - import static jakarta.persistence.metamodel.Type.PersistenceType.ENTITY; import static org.hibernate.metamodel.mapping.internal.AnyDiscriminatorPart.determineDiscriminatorConverter; @@ -40,10 +37,9 @@ public AnyMappingDomainTypeImpl( this.anyType = anyType; this.baseJtd = baseJtd; - final MetaType discriminatorType = (MetaType) anyType.getDiscriminatorType(); - final BasicType discriminatorBaseType = (BasicType) discriminatorType.getBaseType(); - final NavigableRole navigableRole = resolveNavigableRole( bootAnyMapping ); - + final var discriminatorType = (MetaType) anyType.getDiscriminatorType(); + final var discriminatorBaseType = (BasicType) discriminatorType.getBaseType(); + final var navigableRole = resolveNavigableRole( bootAnyMapping ); anyDiscriminatorType = new ConvertedBasicTypeImpl( navigableRole.getFullPath(), discriminatorBaseType.getJdbcType(), @@ -73,13 +69,14 @@ public String getTypeName() { } private NavigableRole resolveNavigableRole(Any bootAnyMapping) { - final StringBuilder buffer = new StringBuilder(); - if ( bootAnyMapping.getTable() != null ) { - buffer.append( bootAnyMapping.getTable().getName() ); + final var buffer = new StringBuilder(); + final var table = bootAnyMapping.getTable(); + if ( table != null ) { + buffer.append( table.getName() ); } buffer.append( "(" ); - final List columns = bootAnyMapping.getColumns(); + final var columns = bootAnyMapping.getColumns(); for ( int i = 0; i < columns.size(); i++ ) { buffer.append( columns.get( i ).getName() ); if ( i+1 < columns.size() ) { diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/ArrayTupleType.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/ArrayTupleType.java index f25f1eb7e394..c06524882631 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/ArrayTupleType.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/ArrayTupleType.java @@ -53,7 +53,7 @@ public String getTypeName() { } private static JavaType[] getTypeDescriptors(SqmExpressible[] components) { - final JavaType[] typeDescriptors = new JavaType[components.length]; + final var typeDescriptors = new JavaType[components.length]; for ( int i = 0; i < components.length; i++ ) { typeDescriptors[i] = components[i].getExpressibleJavaType(); } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AttributeContainer.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AttributeContainer.java index 741b4fc25a25..c972fc58dc20 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AttributeContainer.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AttributeContainer.java @@ -35,7 +35,7 @@ default void applyIdAttribute(SingularPersistentAttribute idAttribute) { } default void applyNonAggregatedIdAttributes( - Set> idAttributes, + Set> idAttributes, EmbeddableDomainType idClassType) { throw new UnsupportedMappingException( "AttributeContainer [" + getClass().getName() + "] does not support identifiers" diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/BasicSqmPathSource.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/BasicSqmPathSource.java index cd699971ba16..6b909090f748 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/BasicSqmPathSource.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/BasicSqmPathSource.java @@ -48,8 +48,8 @@ public String getTypeName() { @Override public @Nullable SqmPathSource findSubPathSource(String name) { - String path = pathModel.getPathName(); - String pathDesc = path == null || path.startsWith( "{" ) ? " " : " '" + pathModel.getPathName() + "' "; + final String path = pathModel.getPathName(); + final String pathDesc = path == null || path.startsWith( "{" ) ? " " : " '" + pathModel.getPathName() + "' "; throw new TerminalPathException( "Terminal path" + pathDesc + "has no attribute '" + name + "'" ); } @@ -85,8 +85,8 @@ public boolean isGeneric() { @Override public String toString() { - return "BasicSqmPathSource(" + - getPathName() + " : " + getJavaType().getSimpleName() + - ")"; + return "BasicSqmPathSource(" + + getPathName() + " : " + getJavaType().getSimpleName() + + ")"; } } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/DomainModelHelper.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/DomainModelHelper.java index d01d2ef09ac3..f4cfb2433533 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/DomainModelHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/DomainModelHelper.java @@ -5,7 +5,6 @@ package org.hibernate.metamodel.model.domain.internal; import org.hibernate.metamodel.MappingMetamodel; -import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.ModelPart; import org.hibernate.metamodel.model.domain.EntityDomainType; import org.hibernate.metamodel.model.domain.ManagedDomainType; @@ -28,9 +27,9 @@ static boolean isCompatible( return true; } else { - final ModelPart modelPart1 = + final var modelPart1 = getEntityAttributeModelPart( attribute1, attribute1.getDeclaringType(), mappingMetamodel ); - final ModelPart modelPart2 = + final var modelPart2 = getEntityAttributeModelPart( attribute2, attribute2.getDeclaringType(), mappingMetamodel ); return modelPart1 != null && modelPart2 != null @@ -43,13 +42,13 @@ static ModelPart getEntityAttributeModelPart( ManagedDomainType domainType, MappingMetamodel mappingMetamodel) { if ( domainType instanceof EntityDomainType ) { - final EntityMappingType entity = mappingMetamodel.getEntityDescriptor( domainType.getTypeName() ); - return entity.findSubPart( attribute.getName() ); + return mappingMetamodel.getEntityDescriptor( domainType.getTypeName() ) + .findSubPart( attribute.getName() ); } else { ModelPart candidate = null; - for ( ManagedDomainType subType : domainType.getSubTypes() ) { - final ModelPart modelPart = getEntityAttributeModelPart( attribute, subType, mappingMetamodel ); + for ( var subType : domainType.getSubTypes() ) { + final var modelPart = getEntityAttributeModelPart( attribute, subType, mappingMetamodel ); if ( modelPart != null ) { if ( candidate != null && !isCompatibleModelPart( candidate, modelPart ) ) { return null; diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/EmbeddableTypeImpl.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/EmbeddableTypeImpl.java index 267c7cba8051..d308d62873c0 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/EmbeddableTypeImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/EmbeddableTypeImpl.java @@ -5,9 +5,12 @@ package org.hibernate.metamodel.model.domain.internal; import java.io.Serializable; +import java.util.ArrayList; import java.util.Collection; +import java.util.List; import org.checkerframework.checker.nullness.qual.Nullable; +import org.hibernate.AssertionFailure; import org.hibernate.metamodel.UnsupportedMappingException; import org.hibernate.metamodel.mapping.EntityDiscriminatorMapping; import org.hibernate.metamodel.model.domain.DomainType; @@ -31,8 +34,10 @@ public class EmbeddableTypeImpl extends AbstractManagedType implements SqmEmbeddableDomainType, Serializable { + @SuppressWarnings("FieldCanBeLocal") private final boolean isDynamic; private final EmbeddedDiscriminatorSqmPathSource discriminatorPathSource; + private final List> subtypes = new ArrayList<>(); public EmbeddableTypeImpl( JavaType javaType, @@ -61,15 +66,27 @@ public PersistenceType getPersistenceType() { public int getTupleLength() { int count = 0; for ( var attribute : getSingularAttributes() ) { - count += ( (SqmDomainType) attribute.getType() ).getTupleLength(); + if ( attribute.getType() instanceof SqmDomainType domainType ) { + count += domainType.getTupleLength(); + } + else { + throw new AssertionFailure( "Should have been a domain type" ); + } } return count; } @Override public Collection> getSubTypes() { - //noinspection unchecked - return (Collection>) super.getSubTypes(); + return subtypes; + } + + @Override + public void addSubType(ManagedDomainType subType) { + super.addSubType( subType ); + if ( subType instanceof SqmEmbeddableDomainType entityDomainType ) { + subtypes.add( entityDomainType ); + } } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/EmbeddedDiscriminatorSqmPath.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/EmbeddedDiscriminatorSqmPath.java index 65a3b05667d5..650f0c406415 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/EmbeddedDiscriminatorSqmPath.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/EmbeddedDiscriminatorSqmPath.java @@ -52,7 +52,7 @@ public EmbeddableDomainType getEmbeddableDomainType() { @Override public EmbeddedDiscriminatorSqmPath copy(SqmCopyContext context) { - final EmbeddedDiscriminatorSqmPath existing = context.getCopy( this ); + final var existing = context.getCopy( this ); if ( existing != null ) { return existing; } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/EntityDiscriminatorSqmPath.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/EntityDiscriminatorSqmPath.java index 10fe6fa1b404..093b7d59238e 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/EntityDiscriminatorSqmPath.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/EntityDiscriminatorSqmPath.java @@ -64,7 +64,7 @@ public EntityMappingType getEntityDescriptor() { @Override public EntityDiscriminatorSqmPath copy(SqmCopyContext context) { - final EntityDiscriminatorSqmPath existing = context.getCopy( this ); + final var existing = context.getCopy( this ); if ( existing != null ) { return existing; } @@ -80,6 +80,7 @@ public EntityDiscriminatorSqmPath copy(SqmCopyContext context) { public X accept(SemanticQueryWalker walker) { return entityDescriptor.hasSubclasses() ? walker.visitDiscriminatorPath( this ) - : walker.visitEntityTypeLiteralExpression( new SqmLiteralEntityType( entityDomainType, nodeBuilder() ) ); + : walker.visitEntityTypeLiteralExpression( + new SqmLiteralEntityType( entityDomainType, nodeBuilder() ) ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/EntityPersisterConcurrentMap.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/EntityPersisterConcurrentMap.java index db13d7b2e468..85526604475c 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/EntityPersisterConcurrentMap.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/EntityPersisterConcurrentMap.java @@ -7,10 +7,11 @@ import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; import org.hibernate.persister.entity.EntityPersister; +import static java.util.stream.Collectors.toUnmodifiableMap; + /** * Concurrent Map implementation of mappings entity name -> EntityPersister. * Concurrency is optimised for read operations; write operations will @@ -26,10 +27,7 @@ public final class EntityPersisterConcurrentMap { public EntityPersister get(final String name) { final var entityPersisterHolder = map.get( name ); - if ( entityPersisterHolder != null ) { - return entityPersisterHolder.entityPersister; - } - return null; + return entityPersisterHolder == null ? null : entityPersisterHolder.entityPersister; } public EntityPersister[] values() { @@ -55,10 +53,10 @@ public String[] keys() { } private void recomputeValues() { - //Assumption: the write lock is being held (synchronize on this) + //Assumption: the write lock is held (synchronize on this) final int size = map.size(); - final EntityPersister[] newValues = new EntityPersister[size]; - final String[] newKeys = new String[size]; + final var newValues = new EntityPersister[size]; + final var newKeys = new String[size]; int i = 0; for ( var entry : map.entrySet() ) { newValues[i] = entry.getValue().entityPersister; @@ -75,7 +73,7 @@ private void recomputeValues() { */ @Deprecated(forRemoval = true) public Map convertToMap() { - return map.entrySet().stream().collect( Collectors.toUnmodifiableMap( + return map.entrySet().stream().collect( toUnmodifiableMap( Map.Entry::getKey, e -> e.getValue().entityPersister ) ); diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/EntityTypeImpl.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/EntityTypeImpl.java index b9f2da1a067a..6a7a91821778 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/EntityTypeImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/EntityTypeImpl.java @@ -7,7 +7,9 @@ import java.io.ObjectStreamException; import java.io.Serial; import java.io.Serializable; +import java.util.ArrayList; import java.util.Collection; +import java.util.List; import java.util.Locale; import jakarta.persistence.metamodel.EntityType; @@ -15,19 +17,16 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.mapping.PersistentClass; import org.hibernate.metamodel.UnsupportedMappingException; -import org.hibernate.metamodel.mapping.DiscriminatorType; import org.hibernate.metamodel.mapping.EntityDiscriminatorMapping; import org.hibernate.metamodel.mapping.EntityIdentifierMapping; import org.hibernate.metamodel.mapping.EntityVersionMapping; import org.hibernate.metamodel.model.domain.IdentifiableDomainType; import org.hibernate.metamodel.model.domain.JpaMetamodel; -import org.hibernate.metamodel.model.domain.PersistentAttribute; +import org.hibernate.metamodel.model.domain.ManagedDomainType; import org.hibernate.metamodel.model.domain.spi.JpaMetamodelImplementor; -import org.hibernate.persister.entity.EntityPersister; import org.hibernate.query.PathException; import org.hibernate.query.sqm.SqmPathSource; import org.hibernate.query.sqm.tree.domain.SqmDomainType; -import org.hibernate.query.sqm.tree.domain.SqmManagedDomainType; import org.hibernate.query.sqm.tree.domain.SqmPath; import org.hibernate.query.sqm.tree.domain.SqmPersistentAttribute; import org.hibernate.query.sqm.tree.domain.SqmEntityDomainType; @@ -35,6 +34,7 @@ import static jakarta.persistence.metamodel.Bindable.BindableType.ENTITY_TYPE; import static jakarta.persistence.metamodel.Type.PersistenceType.ENTITY; +import static jakarta.persistence.metamodel.Type.PersistenceType.MAPPED_SUPERCLASS; import static org.hibernate.metamodel.model.domain.internal.DomainModelHelper.isCompatible; /** @@ -50,6 +50,7 @@ public class EntityTypeImpl private final String jpaEntityName; private final JpaMetamodelImplementor metamodel; private final SqmPathSource discriminatorPathSource; + private final List> subtypes = new ArrayList<>(); public EntityTypeImpl( String entityName, @@ -77,10 +78,10 @@ public EntityTypeImpl( } private EntityDiscriminatorSqmPathSource entityDiscriminatorPathSource(JpaMetamodelImplementor metamodel) { - final EntityPersister entityDescriptor = + final var entityDescriptor = metamodel.getMappingMetamodel() .getEntityDescriptor( getHibernateEntityName() ); - final DiscriminatorType discriminatorType = entityDescriptor.getDiscriminatorDomainType(); + final var discriminatorType = entityDescriptor.getDiscriminatorDomainType(); return discriminatorType == null ? null : new EntityDiscriminatorSqmPathSource<>( discriminatorType, this, entityDescriptor ); } @@ -151,7 +152,7 @@ public SqmEntityDomainType getPathType() { @Override public @Nullable SqmPathSource findSubPathSource(String name) { - final PersistentAttribute attribute = super.findAttribute( name ); + final var attribute = super.findAttribute( name ); if ( attribute != null ) { return (SqmPathSource) attribute; } @@ -176,13 +177,19 @@ else if ( EntityDiscriminatorMapping.matchesRoleName( name ) ) { @Override public @Nullable SqmPathSource findSubPathSource(String name, boolean includeSubtypes) { - final PersistentAttribute attribute = super.findAttribute( name ); + final var attribute = super.findAttribute( name ); if ( attribute != null ) { + if ( attribute.getDeclaringType().getPersistenceType() == MAPPED_SUPERCLASS ) { + final var concreteGeneric = findConcreteGenericAttribute( name ); + if ( concreteGeneric != null ) { + return (SqmPathSource) concreteGeneric; + } + } return (SqmPathSource) attribute; } else { if ( includeSubtypes ) { - final PersistentAttribute subtypeAttribute = findSubtypeAttribute( name ); + final var subtypeAttribute = findSubtypeAttribute( name ); if ( subtypeAttribute != null ) { return (SqmPathSource) subtypeAttribute; } @@ -201,8 +208,8 @@ else if ( EntityDiscriminatorMapping.matchesRoleName( name ) ) { private SqmPersistentAttribute findSubtypeAttribute(String name) { SqmPersistentAttribute subtypeAttribute = null; - for ( SqmManagedDomainType subtype : getSubTypes() ) { - final SqmPersistentAttribute candidate = subtype.findSubTypesAttribute( name ); + for ( var subtype : super.getSubTypes() ) { + final var candidate = subtype.findSubTypesAttribute( name ); if ( candidate != null ) { if ( subtypeAttribute != null && !isCompatible( subtypeAttribute, candidate, metamodel.getMappingMetamodel() ) ) { @@ -249,8 +256,15 @@ public PersistenceType getPersistenceType() { @Override public Collection> getSubTypes() { - //noinspection unchecked - return (Collection>) super.getSubTypes(); + return subtypes; + } + + @Override + public void addSubType(ManagedDomainType subType) { + super.addSubType( subType ); + if ( subType instanceof SqmEntityDomainType entityDomainType ) { + subtypes.add( entityDomainType ); + } } @Override 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 e412aa39bb02..e08792583ec9 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 @@ -14,6 +14,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.TreeMap; import java.util.concurrent.ConcurrentHashMap; @@ -21,6 +22,7 @@ import org.checkerframework.checker.nullness.qual.Nullable; +import org.hibernate.AssertionFailure; import org.hibernate.boot.model.NamedEntityGraphDefinition; import org.hibernate.boot.query.NamedQueryDefinition; import org.hibernate.boot.registry.classloading.spi.ClassLoaderService; @@ -58,6 +60,7 @@ import static java.util.Collections.emptySet; import static org.hibernate.internal.CoreMessageLogger.CORE_LOGGER; +import static org.hibernate.internal.util.type.PrimitiveWrappers.canonicalize; import static org.hibernate.metamodel.internal.InjectionHelper.injectEntityGraph; import static org.hibernate.metamodel.internal.InjectionHelper.injectTypedQueryReference; @@ -66,11 +69,11 @@ */ public class JpaMetamodelImpl implements JpaMetamodelImplementor, Serializable { - private static class ImportInfo { + private static class ImportInfo { private final String importedName; - private Class loadedClass; // could be null for boot metamodel import; not final to allow for populating later + private Class loadedClass; // could be null for boot metamodel import; not final to allow for populating later - private ImportInfo(String importedName, Class loadedClass) { + private ImportInfo(String importedName, Class loadedClass) { this.importedName = importedName; this.loadedClass = loadedClass; } @@ -93,7 +96,7 @@ private ImportInfo(String importedName, Class loadedClass) { private final Map, String> entityProxyInterfaceMap = new HashMap<>(); - private final Map> nameToImportMap = new ConcurrentHashMap<>(); + private final Map nameToImportMap = new ConcurrentHashMap<>(); private final Map knownInvalidnameToImportMap = new ConcurrentHashMap<>(); @@ -116,14 +119,13 @@ public ServiceRegistry getServiceRegistry() { } @Override - public @Nullable ManagedDomainType findManagedType(@Nullable String typeName) { - //noinspection unchecked - return typeName == null ? null : (ManagedDomainType) managedTypeByName.get( typeName ); + public @Nullable ManagedDomainType findManagedType(@Nullable String typeName) { + return typeName == null ? null : managedTypeByName.get( typeName ); } @Override - public ManagedDomainType managedType(String typeName) { - final ManagedDomainType managedType = findManagedType( typeName ); + public ManagedDomainType managedType(String typeName) { + final var managedType = findManagedType( typeName ); if ( managedType == null ) { throw new IllegalArgumentException( "Not a managed type: " + typeName ); } @@ -137,18 +139,18 @@ public EntityDomainType findEntityType(@Nullable String entityName) { return null; } - final ManagedDomainType managedType = managedTypeByName.get( entityName ); - if ( managedType instanceof EntityDomainType entityDomainType ){ + if ( managedTypeByName.get( entityName ) + instanceof EntityDomainType entityDomainType ){ return entityDomainType; } // NOTE: `managedTypeByName` is keyed by Hibernate entity name. // If there is a direct match based on key, we want that one - see above. - // However, the JPA contract for `#entity` is to match `@Entity(name)`; if there - // was no direct match, we need to iterate over all of the and look based on - // JPA entity-name. + // However, the JPA contract for `#entity` is to match `@Entity(name)`; + // if there was no direct match, we need to iterate over all of them and + // look based on JPA entity name. - for ( Map.Entry> entry : managedTypeByName.entrySet() ) { + for ( var entry : managedTypeByName.entrySet() ) { if ( entry.getValue() instanceof EntityDomainType possibility ) { if ( entityName.equals( possibility.getName() ) ) { return possibility; @@ -176,7 +178,7 @@ public EmbeddableDomainType findEmbeddableType(@Nullable String embeddableNam if ( embeddableName == null ) { return null; } - final ManagedDomainType managedType = managedTypeByName.get( embeddableName ); + final var managedType = managedTypeByName.get( embeddableName ); if ( !( managedType instanceof EmbeddableDomainType embeddableDomainType ) ) { return null; } @@ -185,7 +187,7 @@ public EmbeddableDomainType findEmbeddableType(@Nullable String embeddableNam @Override public EmbeddableDomainType embeddable(String embeddableName) { - final EmbeddableDomainType embeddableType = findEmbeddableType( embeddableName ); + final var embeddableType = findEmbeddableType( embeddableName ); if ( embeddableType == null ) { throw new IllegalArgumentException( "Not an embeddable: " + embeddableName ); } @@ -193,52 +195,60 @@ public EmbeddableDomainType embeddable(String embeddableName) { } @Override - public EntityDomainType getHqlEntityReference(String entityName) { - Class loadedClass = null; - final ImportInfo importInfo = resolveImport( entityName ); + public EntityDomainType getHqlEntityReference(String entityName) { + Class loadedClass = null; + final var importInfo = resolveImport( entityName ); if ( importInfo != null ) { loadedClass = importInfo.loadedClass; entityName = importInfo.importedName; } - final EntityDomainType entityDescriptor = findEntityType( entityName ); + final var entityDescriptor = findEntityType( entityName ); if ( entityDescriptor != null ) { - //noinspection unchecked - return (EntityDomainType) entityDescriptor; + return entityDescriptor; } if ( loadedClass == null ) { loadedClass = resolveRequestedClass( entityName ); - // populate class cache for boot metamodel imports + // populate the class cache for boot metamodel imports if ( importInfo != null && loadedClass != null ) { importInfo.loadedClass = loadedClass; } } - if ( loadedClass != null ) { - return resolveEntityReference( loadedClass ); - } - return null; + + return loadedClass == null ? null : resolveEntityReference( loadedClass ); } @Override - public EntityDomainType resolveHqlEntityReference(String entityName) { - final EntityDomainType hqlEntityReference = getHqlEntityReference( entityName ); + public EntityDomainType resolveHqlEntityReference(String entityName) { + final var hqlEntityReference = getHqlEntityReference( entityName ); if ( hqlEntityReference == null ) { throw new EntityTypeException( "Could not resolve entity name '" + entityName + "'", entityName ); } return hqlEntityReference; } + private static ManagedDomainType checkDomainType(Class cls, ManagedDomainType domainType) { + if ( domainType != null && !Objects.equals( domainType.getJavaType(), cls ) ) { + throw new IllegalStateException( "Managed type " + domainType + + " has a different Java type than requested" ); + } + else { + @SuppressWarnings("unchecked") // Safe, we checked it + final var type = (ManagedDomainType) domainType; + return type; + } + } + @Override @Nullable public ManagedDomainType findManagedType(Class cls) { - //noinspection unchecked - return (ManagedDomainType) managedTypeByClass.get( cls ); + return checkDomainType( cls, managedTypeByClass.get( cls ) ); } @Override public ManagedDomainType managedType(Class cls) { - final ManagedDomainType type = findManagedType( cls ); + final var type = findManagedType( cls ); if ( type == null ) { // per JPA throw new IllegalArgumentException( "Not a managed type: " + cls ); @@ -249,17 +259,15 @@ public ManagedDomainType managedType(Class cls) { @Override @Nullable public EntityDomainType findEntityType(Class cls) { - final ManagedType type = managedTypeByClass.get( cls ); - if ( !( type instanceof EntityDomainType ) ) { - return null; - } - //noinspection unchecked - return (EntityDomainType) type; + return checkDomainType( cls, managedTypeByClass.get( cls ) ) + instanceof EntityDomainType entityDomainType + ? entityDomainType + : null; } @Override public EntityDomainType entity(Class cls) { - final EntityDomainType entityType = findEntityType( cls ); + final var entityType = findEntityType( cls ); if ( entityType == null ) { throw new IllegalArgumentException( "Not an entity: " + cls.getName() ); } @@ -268,17 +276,15 @@ public EntityDomainType entity(Class cls) { @Override public @Nullable EmbeddableDomainType findEmbeddableType(Class cls) { - final ManagedType type = managedTypeByClass.get( cls ); - if ( !( type instanceof EmbeddableDomainType ) ) { - return null; - } - //noinspection unchecked - return (EmbeddableDomainType) type; + return checkDomainType( cls, managedTypeByClass.get( cls ) ) + instanceof EmbeddableDomainType embeddableDomainType + ? embeddableDomainType + : null; } @Override public EmbeddableDomainType embeddable(Class cls) { - final EmbeddableDomainType embeddableType = findEmbeddableType( cls ); + final var embeddableType = findEmbeddableType( cls ); if ( embeddableType == null ) { throw new IllegalArgumentException( "Not an embeddable: " + cls.getName() ); } @@ -322,7 +328,7 @@ public Set> getEmbeddables() { @Override public EnumJavaType getEnumType(String className) { - final EnumJavaType enumJavaType = enumJavaTypes.get( className ); + final var enumJavaType = enumJavaTypes.get( className ); if ( enumJavaType != null ) { return enumJavaType; } @@ -332,8 +338,10 @@ public EnumJavaType getEnumType(String className) { if ( clazz == null || !clazz.isEnum() ) { return null; } - //noinspection rawtypes,unchecked - return new EnumJavaType( clazz ); + else { + //noinspection rawtypes,unchecked + return new EnumJavaType( clazz ); + } } catch (ClassLoadingException e) { throw new RuntimeException( e ); @@ -349,7 +357,7 @@ public > E enumValue(EnumJavaType enumType, String enumValu @Override public JavaType getJavaConstantType(String className, String fieldName) { try { - final Field referencedField = getJavaField( className, fieldName ); + final var referencedField = getJavaField( className, fieldName ); if ( referencedField != null ) { return getTypeConfiguration().getJavaTypeRegistry() .resolveDescriptor( referencedField.getType() ); @@ -362,11 +370,19 @@ public JavaType getJavaConstantType(String className, String fieldName) { } @Override - public T getJavaConstant(String className, String fieldName) { + public E getJavaConstant(String className, String fieldName, Class javaTypeClass) { try { - final Field referencedField = getJavaField( className, fieldName ); - //noinspection unchecked - return (T) referencedField.get( null ); + final var referencedField = getJavaField( className, fieldName ); + if ( referencedField == null ) { + throw new IllegalArgumentException( "Referenced field '" + fieldName + + "' of class '" + className + "' does not exist" ); + } + if ( !javaTypeClass.isAssignableFrom( canonicalize( referencedField.getType() ) ) ) { + throw new IllegalArgumentException( "Referenced field '" + fieldName + + "' of class '" + className + + "' is not assignable to '" + javaTypeClass.getTypeName() + "'" ); + } + return javaTypeClass.cast( referencedField.get( null) ); } catch (NoSuchFieldException | IllegalAccessException e) { throw new RuntimeException( e ); @@ -375,15 +391,12 @@ public T getJavaConstant(String className, String fieldName) { private Field getJavaField(String className, String fieldName) throws NoSuchFieldException { final Class namedClass = classLoaderService.classForName( className ); - if ( namedClass != null ) { - return namedClass.getDeclaredField( fieldName ); - } - return null; + return namedClass == null ? null : namedClass.getDeclaredField( fieldName ); } @Override public void addNamedEntityGraph(String graphName, RootGraphImplementor rootGraph) { - final EntityGraph old = entityGraphMap.put( graphName, rootGraph.makeImmutableCopy( graphName ) ); + final var old = entityGraphMap.put( graphName, rootGraph.makeImmutableCopy( graphName ) ); if ( old != null ) { CORE_LOGGER.tracef( "EntityGraph named '%s' was replaced", graphName ); } @@ -396,7 +409,7 @@ public RootGraphImplementor findEntityGraphByName(String name) { @Override public List> findEntityGraphsByJavaType(Class entityClass) { - final EntityDomainType entityType = entity( entityClass ); + final var entityType = entity( entityClass ); if ( entityType == null ) { throw new IllegalArgumentException( "Given class is not an entity: " + entityClass.getName() ); } @@ -415,7 +428,7 @@ public List> findEntityGraphsByJavaType(Class enti @Override public Map> getNamedEntityGraphs(Class entityClass) { - final EntityDomainType entityType = entity( entityClass ); + final var entityType = entity( entityClass ); if ( entityType == null ) { throw new IllegalArgumentException( "Given class is not an entity: " + entityClass.getName() ); } @@ -434,27 +447,26 @@ public Map> getNamedEntityGraphs(Class e @Override public String qualifyImportableName(String queryName) { - final ImportInfo importInfo = resolveImport( queryName ); + final var importInfo = resolveImport( queryName ); return importInfo == null ? null : importInfo.importedName; } - private ImportInfo resolveImport(final String name) { - final ImportInfo importInfo = nameToImportMap.get( name ); + private ImportInfo resolveImport(final String name) { + final var importInfo = nameToImportMap.get( name ); //optimal path first if ( importInfo != null ) { - //noinspection unchecked - return (ImportInfo) importInfo; + return importInfo; } else { - //then check the negative cache, to avoid bothering the classloader unnecessarily + //then check the negative cache to avoid bothering the classloader unnecessarily if ( knownInvalidnameToImportMap.containsKey( name ) ) { return null; } else { - // see if the name is a fully-qualified class name - final Class loadedClass = resolveRequestedClass( name ); + // see if the name is a fully qualified class name + final var loadedClass = resolveRequestedClass( name ); if ( loadedClass == null ) { - // it is NOT a fully-qualified class name - add a marker entry so we do not keep trying later + // it is NOT a fully qualified class name - add a marker entry, so we do not keep trying later // note that ConcurrentHashMap does not support null value so a marker entry is needed // [HHH-14948] But only add it if the cache size isn't getting too large, as in some use cases // the queries are dynamically generated and this cache could lead to memory leaks when left unbounded. @@ -470,9 +482,9 @@ private ImportInfo resolveImport(final String name) { return null; } else { - // it is a fully-qualified class name - add it to the cache + // it is a fully qualified class name - add it to the cache // so to not needing to load from the classloader again - final ImportInfo info = new ImportInfo<>( name, loadedClass ); + final var info = new ImportInfo( name, loadedClass ); nameToImportMap.put( name, info ); return info; } @@ -481,35 +493,34 @@ private ImportInfo resolveImport(final String name) { } private void applyNamedEntityGraphs(Collection namedEntityGraphs) { - for ( NamedEntityGraphDefinition definition : namedEntityGraphs ) { + for ( var definition : namedEntityGraphs ) { CORE_LOGGER.tracef( "Applying named entity graph [name=%s, source=%s]", definition.name(), definition.source() ); - final RootGraphImplementor graph = definition.graphCreator().createEntityGraph( - (entityClass) -> { - final ManagedDomainType managedDomainType = managedTypeByClass.get( entityClass ); - if ( managedDomainType instanceof EntityDomainType match ) { + final var graph = definition.graphCreator().createEntityGraph( + entityClass -> { + if ( managedTypeByClass.get( entityClass ) instanceof EntityDomainType match ) { return match; } throw new IllegalArgumentException( "Cannot resolve entity class : " + entityClass.getName() ); }, - (jpaEntityName) -> { - for ( Map.Entry> entry : managedTypeByName.entrySet() ) { - if ( entry.getValue() instanceof EntityDomainType possibility ) { - if ( jpaEntityName.equals( possibility.getName() ) ) { - return possibility; - } + jpaEntityName -> { + for ( var entry : managedTypeByName.entrySet() ) { + if ( entry.getValue() instanceof EntityDomainType possibility + && jpaEntityName.equals( possibility.getName() ) ) { + return possibility; } } throw new IllegalArgumentException( "Cannot resolve entity name : " + jpaEntityName ); - } + }, + serviceRegistry ); entityGraphMap.put( definition.name(), graph ); } } - private Class resolveRequestedClass(String entityName) { + private Class resolveRequestedClass(String entityName) { try { return classLoaderService.classForName( entityName ); } @@ -518,68 +529,29 @@ private Class resolveRequestedClass(String entityName) { } } - @SuppressWarnings("unchecked") - public EntityDomainType resolveEntityReference(Class javaType) { + private EntityDomainType resolveEntityReference(Class javaType) { // try the incoming Java type as a "strict" entity reference - { - final ManagedDomainType managedType = managedTypeByClass.get( javaType ); - if ( managedType instanceof EntityDomainType ) { - return (EntityDomainType) managedType; - } + final var managedType = managedTypeByClass.get( javaType ); + if ( managedType instanceof EntityDomainType entityDomainType ) { + return entityDomainType; } // Next, try it as a proxy interface reference - { - final String proxyEntityName = entityProxyInterfaceMap.get( javaType ); - if ( proxyEntityName != null ) { - return (EntityDomainType) entity( proxyEntityName ); - } + final String proxyEntityName = entityProxyInterfaceMap.get( javaType ); + if ( proxyEntityName != null ) { + return entity( proxyEntityName ); } // otherwise, try to handle it as a polymorphic reference - { - final EntityDomainType polymorphicDomainType = - (EntityDomainType) polymorphicEntityReferenceMap.get( javaType ); - if ( polymorphicDomainType != null ) { - return polymorphicDomainType; - } - - // create a set of descriptors that should be used to build the polymorphic EntityDomainType - final Set> matchingDescriptors = new HashSet<>(); - for ( ManagedDomainType managedType : managedTypeByName.values() ) { - if ( managedType.getPersistenceType() != Type.PersistenceType.ENTITY ) { - continue; - } - // see if we should add `entityDomainType` as one of the matching-descriptors. - if ( javaType.isAssignableFrom( managedType.getJavaType() ) ) { - // the queried type is assignable from the type of the current entity-type - // we should add it to the collecting set of matching descriptors. it should - // be added aside from a few cases... - - // if the managed-type has a super type and the java type is assignable from the super type, - // do not add the managed-type as the super itself will get added and the initializers for - // entity mappings already handle loading subtypes - adding it would be redundant and lead to - // incorrect results - final ManagedDomainType superType = managedType.getSuperType(); - if ( superType != null - && superType.getPersistenceType() == Type.PersistenceType.ENTITY - && javaType.isAssignableFrom( superType.getJavaType() ) ) { - continue; - } - - // otherwise, add it - matchingDescriptors.add( (EntityDomainType) managedType ); - } - } - - // if we found any matching, create the virtual root EntityDomainType reference - if ( !matchingDescriptors.isEmpty() ) { - final var polymorphicRootDescriptor = new SqmPolymorphicRootDescriptor<>( - typeConfiguration.getJavaTypeRegistry().resolveDescriptor( javaType ), - matchingDescriptors, - this - ); - polymorphicEntityReferenceMap.putIfAbsent( javaType, polymorphicRootDescriptor ); + final var polymorphicDomainType = + polymorphicEntityReferenceMap.get( javaType ); + if ( polymorphicDomainType != null ) { + return polymorphicDomainType; + } + else { + final var polymorphicRootDescriptor = + createPolymorphicRootDescriptor( javaType ); + if ( polymorphicRootDescriptor != null ) { return polymorphicRootDescriptor; } } @@ -590,6 +562,50 @@ public EntityDomainType resolveEntityReference(Class javaType) { ); } + private @Nullable SqmPolymorphicRootDescriptor createPolymorphicRootDescriptor(Class javaType) { + // create a set of descriptors that should be used to build the polymorphic EntityDomainType + final Set> matchingDescriptors = new HashSet<>(); + for ( var managedType : managedTypeByName.values() ) { + if ( managedType.getPersistenceType() == Type.PersistenceType.ENTITY + // see if we should add EntityDomainType as one of the matching descriptors. + && javaType.isAssignableFrom( managedType.getJavaType() ) ) { + // The queried type is assignable from the type of the current entity type. + // We should add it to the collecting set of matching descriptors. It should + // be added aside from a few cases... + + // If the managed type has a supertype and the java type is assignable from the super type, + // do not add the managed type as the supertype itself will get added and the initializers + // for entity mappings already handle loading subtypes - adding it would be redundant and + // lead to incorrect results + final var superType = managedType.getSuperType(); + if ( superType == null + || superType.getPersistenceType() != Type.PersistenceType.ENTITY + || !javaType.isAssignableFrom( superType.getJavaType() ) ) { + final var entityDomainType = (EntityDomainType) managedType; + if ( !javaType.isAssignableFrom( entityDomainType.getJavaType() ) ) { + throw new AssertionFailure( "Supertype mismatch: " + javaType.getTypeName() + + " is not a supertype of " + entityDomainType.getJavaType().getTypeName() ); + } + @SuppressWarnings("unchecked") // Safe, we just checked + final var castDomainType = (EntityDomainType) entityDomainType; + matchingDescriptors.add( castDomainType ); + } + } + } + + // if we found any matching, create the virtual root EntityDomainType reference + if ( !matchingDescriptors.isEmpty() ) { + final var polymorphicRootDescriptor = new SqmPolymorphicRootDescriptor<>( + typeConfiguration.getJavaTypeRegistry().resolveDescriptor( javaType ), + matchingDescriptors, + this + ); + polymorphicEntityReferenceMap.putIfAbsent( javaType, polymorphicRootDescriptor ); + return polymorphicRootDescriptor; + } + return null; + } + @Override public MappingMetamodel getMappingMetamodel() { return mappingMetamodel; @@ -604,10 +620,11 @@ public void processJpa( Collection namedEntityGraphDefinitions, RuntimeModelCreationContext runtimeModelCreationContext) { bootMetamodel.getImports() - .forEach( (key, value) -> this.nameToImportMap.put( key, new ImportInfo<>( value, null ) ) ); + .forEach( (key, value) -> nameToImportMap.put( key, + new ImportInfo( value, null ) ) ); this.entityProxyInterfaceMap.putAll( entityProxyInterfaceMap ); - final MetadataContext context = new MetadataContext( + final var context = new MetadataContext( this, mappingMetamodel, bootMetamodel, @@ -628,31 +645,31 @@ public void processJpa( this.jpaMetaModelPopulationSetting = jpaMetaModelPopulationSetting; // Identifiable types (Entities and MappedSuperclasses) - this.managedTypeByName.putAll( context.getIdentifiableTypesByName() ); - this.managedTypeByClass.putAll( context.getEntityTypeMap() ); - this.managedTypeByClass.putAll( context.getMappedSuperclassTypeMap() ); + managedTypeByName.putAll( context.getIdentifiableTypesByName() ); + managedTypeByClass.putAll( context.getEntityTypeMap() ); + managedTypeByClass.putAll( context.getMappedSuperclassTypeMap() ); // Embeddable types int mapEmbeddables = 0; - for ( EmbeddableDomainType embeddable : context.getEmbeddableTypeSet() ) { + for ( var embeddable : context.getEmbeddableTypeSet() ) { // Do not register the embeddable types for id classes if ( embeddable.getExpressibleJavaType() instanceof EntityJavaType ) { continue; } - final Class embeddableClass = embeddable.getJavaType(); + final var embeddableClass = embeddable.getJavaType(); if ( embeddableClass != Map.class ) { - this.managedTypeByClass.put( embeddable.getJavaType(), embeddable ); - this.managedTypeByName.put( embeddable.getTypeName(), embeddable ); + managedTypeByClass.put( embeddable.getJavaType(), embeddable ); + managedTypeByName.put( embeddable.getTypeName(), embeddable ); } else { - this.managedTypeByName.put( "dynamic-embeddable-" + mapEmbeddables++, embeddable ); + managedTypeByName.put( "dynamic-embeddable-" + mapEmbeddables++, embeddable ); } } typeConfiguration.getJavaTypeRegistry().forEachDescriptor( descriptor -> { if ( descriptor instanceof EnumJavaType> enumJavaType ) { - final Class> enumJavaClass = enumJavaType.getJavaTypeClass(); - for ( Enum enumConstant : enumJavaClass.getEnumConstants() ) { + final var enumJavaClass = enumJavaType.getJavaTypeClass(); + for ( var enumConstant : enumJavaClass.getEnumConstants() ) { addAllowedEnumLiteralsToEnumTypesMap( allowedEnumLiteralsToEnumTypeNames, enumConstant.name(), @@ -731,7 +748,8 @@ private EntityTypeImpl buildEntityType( MetadataContext context, TypeConfiguration typeConfiguration) { context.pushEntityWorkedOn( persistentClass ); - final var entityType = entityType( persistentClass, persistentClass.getMappedClass(), context, typeConfiguration ); + final var entityType = + entityType( persistentClass, persistentClass.getMappedClass(), context, typeConfiguration ); context.registerEntityType( persistentClass, entityType ); context.popEntityWorkedOn( persistentClass ); return entityType; @@ -742,27 +760,31 @@ private EntityTypeImpl entityType( Class mappedClass, MetadataContext context, TypeConfiguration typeConfiguration) { - @SuppressWarnings("unchecked") - final var supertype = - (IdentifiableDomainType) - supertypeForPersistentClass( persistentClass, context, typeConfiguration ); - final JavaType javaType; + return new EntityTypeImpl<>( + entityJavaType( mappedClass, context ), + narrowSupertype( mappedClass, + supertypeForPersistentClass( persistentClass, context, typeConfiguration ) ), + persistentClass, + this + ); + } + + private static JavaType entityJavaType(Class mappedClass, MetadataContext context) { if ( mappedClass == null || Map.class.isAssignableFrom( mappedClass ) ) { // dynamic map //noinspection unchecked - javaType = (JavaType) new DynamicModelJavaType(); + return (JavaType) new DynamicModelJavaType(); } else { - javaType = context.getTypeConfiguration().getJavaTypeRegistry() + return context.getTypeConfiguration().getJavaTypeRegistry() .resolveEntityTypeDescriptor( mappedClass ); } - return new EntityTypeImpl<>( javaType, supertype, persistentClass, this ); } private void handleUnusedMappedSuperclasses(MetadataContext context, TypeConfiguration typeConfiguration) { - final Set unusedMappedSuperclasses = context.getUnusedMappedSuperclasses(); + final var unusedMappedSuperclasses = context.getUnusedMappedSuperclasses(); if ( !unusedMappedSuperclasses.isEmpty() ) { - for ( MappedSuperclass mappedSuperclass : unusedMappedSuperclasses ) { + for ( var mappedSuperclass : unusedMappedSuperclasses ) { CORE_LOGGER.unusedMappedSuperclass( mappedSuperclass.getMappedClass().getName() ); locateOrBuildMappedSuperclassType( mappedSuperclass, context, typeConfiguration ); } @@ -773,7 +795,7 @@ private MappedSuperclassDomainType locateOrBuildMappedSuperclassType( MappedSuperclass mappedSuperclass, MetadataContext context, TypeConfiguration typeConfiguration) { - final MappedSuperclassDomainType mappedSuperclassType = + final var mappedSuperclassType = context.locateMappedSuperclassType( mappedSuperclass ); return mappedSuperclassType == null ? buildMappedSuperclassType( mappedSuperclass, mappedSuperclass.getMappedClass(), context, typeConfiguration ) @@ -785,32 +807,54 @@ private MappedSuperclassTypeImpl buildMappedSuperclassType( Class mappedClass, MetadataContext context, TypeConfiguration typeConfiguration) { - @SuppressWarnings("unchecked") - final IdentifiableDomainType superType = - (IdentifiableDomainType) - supertypeForMappedSuperclass( mappedSuperclass, context, typeConfiguration ); - final JavaType javaType = - context.getTypeConfiguration().getJavaTypeRegistry() - .resolveManagedTypeDescriptor( mappedClass ); - final MappedSuperclassTypeImpl mappedSuperclassType = - new MappedSuperclassTypeImpl<>( javaType, mappedSuperclass, superType, this ); + final var mappedSuperclassType = + new MappedSuperclassTypeImpl<>( + context.getTypeConfiguration().getJavaTypeRegistry() + .resolveManagedTypeDescriptor( mappedClass ), + mappedSuperclass, + narrowSupertype( mappedClass, + supertypeForMappedSuperclass( mappedSuperclass, context, typeConfiguration ) ), + this + ); context.registerMappedSuperclassType( mappedSuperclass, mappedSuperclassType ); return mappedSuperclassType; } - private IdentifiableDomainType supertypeForPersistentClass( + private @Nullable IdentifiableDomainType narrowSupertype( + Class mappedClass, @Nullable IdentifiableDomainType supertypeDomainType) { + if ( supertypeDomainType == null ) { + return null; + } + else if ( mappedClass == null ) { + // dynamic map + //noinspection unchecked + return (IdentifiableDomainType) + supertypeDomainType; + } + else { + if ( !supertypeDomainType.getJavaType().isAssignableFrom( mappedClass ) ) { + throw new AssertionFailure( "Supertype mismatch: " + + supertypeDomainType.getJavaType().getTypeName() + + " is not a supertype of " + mappedClass.getTypeName() ); + } + @SuppressWarnings("unchecked") // Safe, we just checked + final var supertype = (IdentifiableDomainType) supertypeDomainType; + return supertype; + } + } + + private @Nullable IdentifiableDomainType supertypeForPersistentClass( PersistentClass persistentClass, MetadataContext context, TypeConfiguration typeConfiguration) { - final MappedSuperclass superMappedSuperclass = persistentClass.getSuperMappedSuperclass(); - final IdentifiableDomainType supertype = + final var superMappedSuperclass = persistentClass.getSuperMappedSuperclass(); + final var supertype = superMappedSuperclass == null ? null : locateOrBuildMappedSuperclassType( superMappedSuperclass, context, typeConfiguration ); - - //no mappedSuperclass, check for a super entity if ( supertype == null ) { - final PersistentClass superPersistentClass = persistentClass.getSuperclass(); + // no mapped superclass, check for a super entity + final var superPersistentClass = persistentClass.getSuperclass(); return superPersistentClass == null ? null : locateOrBuildEntityType( superPersistentClass, context, typeConfiguration ); @@ -820,18 +864,18 @@ private IdentifiableDomainType supertypeForPersistentClass( } } - private IdentifiableDomainType supertypeForMappedSuperclass( + private @Nullable IdentifiableDomainType supertypeForMappedSuperclass( MappedSuperclass mappedSuperclass, MetadataContext context, TypeConfiguration typeConfiguration) { - final MappedSuperclass superMappedSuperclass = mappedSuperclass.getSuperMappedSuperclass(); - final IdentifiableDomainType superType = + final var superMappedSuperclass = mappedSuperclass.getSuperMappedSuperclass(); + final var superType = superMappedSuperclass == null ? null : locateOrBuildMappedSuperclassType( superMappedSuperclass, context, typeConfiguration ); - //no mappedSuperclass, check for a super entity if ( superType == null ) { - final PersistentClass superPersistentClass = mappedSuperclass.getSuperPersistentClass(); + //no mapped superclass, check for a super entity + final var superPersistentClass = mappedSuperclass.getSuperPersistentClass(); return superPersistentClass == null ? null : locateOrBuildEntityType( superPersistentClass, context, typeConfiguration ); diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/MappedSuperclassTypeImpl.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/MappedSuperclassTypeImpl.java index eb6030e662f8..4f5cb67470a9 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/MappedSuperclassTypeImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/MappedSuperclassTypeImpl.java @@ -9,7 +9,6 @@ import org.hibernate.metamodel.UnsupportedMappingException; import org.hibernate.metamodel.mapping.EntityIdentifierMapping; import org.hibernate.metamodel.model.domain.IdentifiableDomainType; -import org.hibernate.metamodel.model.domain.PersistentAttribute; import org.hibernate.metamodel.model.domain.spi.JpaMetamodelImplementor; import org.hibernate.query.sqm.SqmPathSource; import org.hibernate.query.sqm.tree.domain.SqmDomainType; @@ -89,7 +88,7 @@ public SqmMappedSuperclassDomainType getPathType() { @Override public @Nullable SqmPathSource findSubPathSource(String name) { - final PersistentAttribute attribute = findAttribute( name ); + final var attribute = findAttribute( name ); if ( attribute != null ) { return (SqmPathSource) attribute; } @@ -103,7 +102,7 @@ else if ( "id".equalsIgnoreCase( name ) ) { @Override public @Nullable SqmPathSource getIdentifierDescriptor() { - return (SqmPathSource) super.getIdentifierDescriptor(); + return super.getIdentifierDescriptor(); } @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 33209e400641..ac68541722c1 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 @@ -35,6 +35,7 @@ import org.hibernate.metamodel.model.domain.JpaMetamodel; import org.hibernate.metamodel.model.domain.ManagedDomainType; import org.hibernate.metamodel.model.domain.NavigableRole; +import org.hibernate.query.sqm.tree.domain.SqmManagedDomainType; import org.hibernate.type.BindingContext; import org.hibernate.query.sqm.tuple.TupleType; import org.hibernate.metamodel.model.domain.spi.JpaMetamodelImplementor; @@ -58,7 +59,6 @@ import org.hibernate.type.ComponentType; import org.hibernate.type.descriptor.java.EnumJavaType; import org.hibernate.type.descriptor.java.JavaType; -import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.spi.TypeConfiguration; import jakarta.persistence.metamodel.EmbeddableType; @@ -113,12 +113,12 @@ public class MappingMetamodelImpl // - ignoring Hibernate's representation mode (entity mode), the Class // object for an entity (or mapped superclass) always refers to the same // JPA EntityType and Hibernate EntityPersister. The problem arises with - // embeddables. For an embeddable, as with the rest of its metamodel, + // embeddables. For an embeddable type, as with the rest of its metamodel, // Hibernate combines the embeddable's relational/mapping while JPA does // not. This is perfectly consistent with each paradigm. But it results // in a mismatch since JPA expects a single "type descriptor" for a - // given embeddable class while Hibernate incorporates the - // relational/mapping info so we have a "type descriptor" for each usage + // given embeddable class, while Hibernate incorporates the + // relational/mapping info, so we have a "type descriptor" for each usage // of that embeddable type. (Think embeddable versus embedded.) // // To account for this, we track both paradigms here. @@ -258,11 +258,9 @@ private void processBootCollections( for ( final var model : collectionBindings ) { final String role = model.getRole(); final var persister = - persisterFactory.createCollectionPersister( - model, + persisterFactory.createCollectionPersister( model, cacheImplementor.getCollectionRegionAccess( new NavigableRole( role ) ), - modelCreationContext - ); + modelCreationContext ); collectionPersisterMap.put( role, persister ); if ( persister.getIndexType() instanceof org.hibernate.type.EntityType entityType ) { registerEntityParticipant( entityType, persister ); @@ -360,32 +358,29 @@ public boolean isEntityClass(Class entityJavaType) { @Override public EntityPersister getEntityDescriptor(Class entityJavaType) { - var entityPersister = entityPersisterMap.get( entityJavaType.getName() ); - if ( entityPersister == null ) { - final String mappedEntityName = entityProxyInterfaceMap.get( entityJavaType ); - if ( mappedEntityName != null ) { - entityPersister = entityPersisterMap.get( mappedEntityName ); - } - } - if ( entityPersister == null ) { - throw new UnknownEntityTypeException( entityJavaType ); - } - return entityPersister; + return getEntityPersister( entityJavaType ); } @Override @Deprecated(forRemoval = true) @SuppressWarnings( "removal" ) public EntityPersister locateEntityDescriptor(Class byClass) { - var entityPersister = entityPersisterMap.get( byClass.getName() ); - if ( entityPersister == null ) { + return getEntityPersister( byClass ); + } + + private EntityPersister getEntityPersister(Class byClass) { + final var entityPersister = entityPersisterMap.get( byClass.getName() ); + if ( entityPersister != null ) { + return entityPersister; + } + else { final String mappedEntityName = entityProxyInterfaceMap.get( byClass ); if ( mappedEntityName != null ) { - entityPersister = entityPersisterMap.get( mappedEntityName ); + final var persister = entityPersisterMap.get( mappedEntityName ); + if ( persister != null ) { + return persister; + } } - } - if ( entityPersister == null ) { throw new UnknownEntityTypeException( byClass ); } - return entityPersister; } @Override @@ -419,12 +414,12 @@ public Set> getEmbeddables() { } @Override - public @Nullable ManagedDomainType findManagedType(@Nullable String typeName) { + public @Nullable ManagedDomainType findManagedType(@Nullable String typeName) { return jpaMetamodel.findManagedType( typeName ); } @Override - public ManagedDomainType managedType(String typeName) { + public ManagedDomainType managedType(String typeName) { return jpaMetamodel.managedType( typeName ); } @@ -449,12 +444,12 @@ public EmbeddableDomainType embeddable(String embeddableName) { } @Override - public EntityDomainType getHqlEntityReference(String entityName) { + public EntityDomainType getHqlEntityReference(String entityName) { return jpaMetamodel.getHqlEntityReference( entityName ); } @Override - public EntityDomainType resolveHqlEntityReference(String entityName) { + public EntityDomainType resolveHqlEntityReference(String entityName) { return jpaMetamodel.resolveHqlEntityReference( entityName ); } @@ -489,8 +484,8 @@ public JavaType getJavaConstantType(String className, String fieldName) { } @Override - public T getJavaConstant(String className, String fieldName) { - return jpaMetamodel.getJavaConstant( className, fieldName ); + public E getJavaConstant(String className, String fieldName, Class javaTypeClass) { + return jpaMetamodel.getJavaConstant( className, fieldName, javaTypeClass ); } @Override @@ -644,21 +639,21 @@ public String[] getAllCollectionRoles() { public @Nullable BindableType resolveParameterBindType(Class javaType) { final var typeConfiguration = getTypeConfiguration(); - final BasicType basicType = typeConfiguration.getBasicTypeForJavaType( javaType ); + final var basicType = typeConfiguration.getBasicTypeForJavaType( javaType ); // For enums, we simply don't know the exact mapping if there is no basic type registered if ( basicType != null || javaType.isEnum() ) { return basicType; } - final ManagedDomainType managedType = jpaMetamodel.findManagedType( javaType ); + final var managedType = jpaMetamodel.findManagedType( javaType ); if ( managedType != null ) { - return (BindableType) managedType; + return (SqmManagedDomainType) managedType; } final var javaTypeRegistry = typeConfiguration.getJavaTypeRegistry(); - final JavaType javaTypeDescriptor = javaTypeRegistry.findDescriptor( javaType ); + final var javaTypeDescriptor = javaTypeRegistry.findDescriptor( javaType ); if ( javaTypeDescriptor != null ) { - final JdbcType recommendedJdbcType = + final var recommendedJdbcType = javaTypeDescriptor.getRecommendedJdbcType( typeConfiguration.getCurrentBaseSqlTypeIndicators() ); if ( recommendedJdbcType != null ) { return typeConfiguration.getBasicTypeRegistry().resolve( javaTypeDescriptor, recommendedJdbcType ); @@ -685,25 +680,20 @@ public String[] getAllCollectionRoles() { return null; } - final Class clazz = unproxiedClass( bindValue ); - - // Resolve superclass bindable type if necessary, as we don't register types for e.g. Inet4Address - Class c = clazz; - do { - final BindableType type = resolveParameterBindType( c ); - if ( type != null ) { - return type; - } - c = c.getSuperclass(); + final var clazz = unproxiedClass( bindValue ); + // Resolve the superclass bindable type if necessary, + // as we don't register types for e.g. Inet4Address + var type = searchSupertypesForBindableType( clazz ); + if ( type != null ) { + return type; } - while ( c != Object.class ); if ( clazz.isEnum() ) { return null; //createEnumType( (Class) clazz ); } else if ( Serializable.class.isAssignableFrom( clazz ) ) { final var parameterBindType = resolveParameterBindType( Serializable.class ); - //noinspection unchecked + //noinspection unchecked (completely safe) return (BindableType) parameterBindType; } else { @@ -711,6 +701,19 @@ else if ( Serializable.class.isAssignableFrom( clazz ) ) { } } + private @Nullable BindableType searchSupertypesForBindableType(Class clazz) { + Class c = clazz; + do { + final var type = resolveParameterBindType( c ); + if ( type != null ) { + return type; + } + c = c.getSuperclass(); + } + while ( c != Object.class ); + return null; + } + private static Class unproxiedClass(T bindValue) { final var lazyInitializer = extractLazyInitializer( bindValue ); final var result = diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/PathHelper.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/PathHelper.java index b9889b827173..3a30d128b970 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/PathHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/PathHelper.java @@ -11,8 +11,9 @@ public class PathHelper { public static NavigablePath append(SqmPath lhs, SqmPathSource rhs, @Nullable SqmPathSource intermediatePathSource) { + final var navigablePath = lhs.getNavigablePath(); return intermediatePathSource == null - ? lhs.getNavigablePath().append( rhs.getPathName() ) - : lhs.getNavigablePath().append( intermediatePathSource.getPathName() ).append( rhs.getPathName() ); + ? navigablePath.append( rhs.getPathName() ) + : navigablePath.append( intermediatePathSource.getPathName() ).append( rhs.getPathName() ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/PluralAttributeBuilder.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/PluralAttributeBuilder.java index a47add5cdde1..028144d45019 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/PluralAttributeBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/PluralAttributeBuilder.java @@ -86,7 +86,7 @@ public static PersistentAttribute build( attributeMetadata.getMember() ); - final Class javaClass = attributeJtd.getJavaTypeClass(); + final var javaClass = attributeJtd.getJavaTypeClass(); if ( Map.class.equals( javaClass ) ) { return new MapAttributeImpl( builder ); } @@ -124,7 +124,7 @@ else if ( Collection.class.isAssignableFrom( javaClass ) ) { private static SimpleDomainType determineListIndexOrMapKeyType( PluralAttributeMetadata attributeMetadata, MetadataContext metadataContext) { - final Class javaType = attributeMetadata.getJavaType(); + final var javaType = attributeMetadata.getJavaType(); if ( Map.class.isAssignableFrom( javaType ) ) { return (SimpleDomainType) determineSimpleType( attributeMetadata.getMapKeyValueContext(), metadataContext ); diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/SingularAttributeImpl.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/SingularAttributeImpl.java index 28f1f9c3565d..0fe85954e99c 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/SingularAttributeImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/SingularAttributeImpl.java @@ -10,18 +10,12 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.metamodel.AttributeClassification; -import org.hibernate.metamodel.mapping.CollectionPart; -import org.hibernate.metamodel.model.domain.AnyMappingDomainType; import org.hibernate.metamodel.model.domain.IdentifiableDomainType; import org.hibernate.metamodel.model.domain.ManagedDomainType; -import org.hibernate.metamodel.model.domain.PluralPersistentAttribute; import org.hibernate.metamodel.model.domain.SimpleDomainType; -import org.hibernate.query.SemanticException; -import org.hibernate.query.sqm.NodeBuilder; import org.hibernate.query.sqm.SqmBindableType; import org.hibernate.query.sqm.SqmPathSource; import org.hibernate.query.hql.spi.SqmCreationState; -import org.hibernate.query.sqm.internal.SqmMappingModelHelper; import org.hibernate.query.sqm.tree.SqmJoinType; import org.hibernate.query.sqm.tree.domain.SqmPath; import org.hibernate.query.sqm.tree.domain.SqmSingularJoin; @@ -31,13 +25,14 @@ import org.hibernate.query.sqm.tree.from.SqmFrom; import org.hibernate.query.sqm.tree.from.SqmFunctionJoin; import org.hibernate.query.sqm.tree.from.SqmJoin; -import org.hibernate.query.sqm.tree.from.SqmRoot; import org.hibernate.spi.EntityIdentifierNavigablePath; import org.hibernate.spi.NavigablePath; import org.hibernate.type.BasicPluralType; import org.hibernate.type.descriptor.java.JavaType; import static jakarta.persistence.metamodel.Bindable.BindableType.SINGULAR_ATTRIBUTE; +import static org.hibernate.query.sqm.internal.SqmMappingModelHelper.resolveSqmPathSource; +import static org.hibernate.query.sqm.spi.SqmCreationHelper.buildParentNavigablePath; import static org.hibernate.query.sqm.spi.SqmCreationHelper.buildSubNavigablePath; import static org.hibernate.query.sqm.spi.SqmCreationHelper.determineAlias; @@ -77,7 +72,7 @@ public SingularAttributeImpl( this.isVersion = isVersion; this.isOptional = isOptional; - this.sqmPathSource = SqmMappingModelHelper.resolveSqmPathSource( + this.sqmPathSource = resolveSqmPathSource( name, this, attributeType, @@ -157,22 +152,18 @@ public SqmJoin createSqmJoin( @Nullable String alias, boolean fetched, SqmCreationState creationState) { - final NodeBuilder nodeBuilder = creationState.getCreationContext().getNodeBuilder(); - if ( getType() instanceof AnyMappingDomainType ) { - throw new SemanticException( "An @Any attribute cannot be join fetched" ); - } - else if ( sqmPathSource.getPathType() instanceof BasicPluralType ) { + final var nodeBuilder = creationState.getCreationContext().getNodeBuilder(); + if ( sqmPathSource.getPathType() instanceof BasicPluralType ) { final SqmSetReturningFunction setReturningFunction = nodeBuilder.unnestArray( lhs.get( getName() ) ); - //noinspection unchecked - final SqmFunctionJoin join = new SqmFunctionJoin<>( + final var join = new SqmFunctionJoin<>( createNavigablePath( lhs, alias ), setReturningFunction, true, setReturningFunction.getType(), alias, joinType, - (SqmRoot) lhs + (SqmFrom) lhs ); return (SqmJoin) join; } @@ -232,11 +223,7 @@ public NavigablePath createNavigablePath(SqmPath parent, @Nullable String ali "LHS cannot be null for a sub-navigable reference - " + getName() ); } - final SqmPathSource parentPathSource = parent.getResolvedModel(); - final NavigablePath parentNavigablePath = - parentPathSource instanceof PluralPersistentAttribute - ? parent.getNavigablePath().append( CollectionPart.Nature.ELEMENT.getName() ) - : parent.getNavigablePath(); + final var parentNavigablePath = buildParentNavigablePath( parent, "" ); if ( getDeclaringType() instanceof IdentifiableDomainType declaringType && !declaringType.hasSingleIdAttribute() ) { return new EntityIdentifierNavigablePath( parentNavigablePath, null ) @@ -291,7 +278,7 @@ public boolean isOptional() { @Override public boolean isAssociation() { - final PersistentAttributeType persistentAttributeType = getPersistentAttributeType(); + final var persistentAttributeType = getPersistentAttributeType(); return persistentAttributeType == PersistentAttributeType.MANY_TO_ONE || persistentAttributeType == PersistentAttributeType.ONE_TO_ONE; } 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 8f9d8968cccc..e2c6d551e783 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 @@ -124,6 +124,7 @@ import java.util.Set; import java.util.StringTokenizer; import java.util.function.Consumer; +import java.util.function.UnaryOperator; import static java.util.Collections.emptyList; import static org.hibernate.internal.util.StringHelper.getNonEmptyOrConjunctionIfBothNonEmpty; @@ -137,6 +138,7 @@ import static org.hibernate.pretty.MessageHelper.collectionInfoString; import static org.hibernate.sql.Template.renderWhereStringTemplate; import static org.hibernate.sql.model.ModelMutationLogging.MODEL_MUTATION_LOGGER; +import static org.hibernate.temporal.TemporalTableStrategy.HISTORY_TABLE; /** * Base implementation of the {@code QueryableCollection} interface. @@ -177,6 +179,8 @@ public abstract class AbstractCollectionPersister // columns protected final String[] keyColumnNames; + protected final String[] keyFormulaTemplates; + protected final String[] keyFormulas; protected final String[] indexColumnNames; protected final String[] indexFormulaTemplates; protected final String[] indexFormulas; @@ -297,6 +301,8 @@ public AbstractCollectionPersister( isVersioned = collectionBootDescriptor.isOptimisticLocked(); + final var typeConfiguration = creationContext.getTypeConfiguration(); + // KEY final var key = collectionBootDescriptor.getKey(); @@ -304,16 +310,19 @@ public AbstractCollectionPersister( keyType = key.getType(); final int keySpan = key.getColumnSpan(); keyColumnNames = new String[keySpan]; + keyFormulaTemplates = new String[keySpan]; + keyFormulas = new String[keySpan]; keyColumnAliases = new String[keySpan]; int k = 0; for ( var selectable: key.getSelectables() ) { // NativeSQL: collect key column and auto-aliases keyColumnAliases[k] = selectable.getAlias( dialect, table ); - if ( selectable instanceof Column column ) { - keyColumnNames[k] = column.getQuotedName( dialect ); + if ( selectable instanceof Formula formula ) { + keyFormulaTemplates[k] = formula.getTemplate( dialect, typeConfiguration ); + keyFormulas[k] = formula.getFormula(); } - else { - throw new MappingException( "Collection keys may not contain formulas: " + navigableRole.getFullPath() ); + else if ( selectable instanceof Column column ) { + keyColumnNames[k] = column.getQuotedName( dialect ); } k++; } @@ -334,8 +343,6 @@ public AbstractCollectionPersister( // because it's needed in OneToManyPersister.getTableName() spaces[0] = getTableName(); - final var typeConfiguration = creationContext.getTypeConfiguration(); - final int elementSpan = elementBootDescriptor.getColumnSpan(); elementColumnAliases = new String[elementSpan]; elementColumnNames = new String[elementSpan]; @@ -513,23 +520,23 @@ private FilterHelper manyToManyFilterHelper(Collection collection, RuntimeModelC private FilterHelper filterHelper( Collection collection, EntityPersister elementPersister, RuntimeModelCreationContext context) { final var filters = collection.getFilters(); - if ( filters.isEmpty() ) { - return null; - } - else { - final var entityNameByTableNameMap = - elementPersister == null - ? null - : AbstractEntityPersister.getEntityNameByTableNameMap( - context.getBootModel().getEntityBinding( elementPersister.getEntityName() ), - context.getSessionFactory().getSqlStringGenerationContext() - ); - return new FilterHelper( filters, entityNameByTableNameMap, factory ); - } + return filters.isEmpty() + ? null + : new FilterHelper( filters, entityNameByTableNameMap( elementPersister, context ), factory ); + } + + private static Map entityNameByTableNameMap( + EntityPersister elementPersister, RuntimeModelCreationContext context) { + return elementPersister == null + ? null + : AbstractEntityPersister.getEntityNameByTableNameMap( + context.getBootModel().getEntityBinding( elementPersister.getEntityName() ), + context.getSessionFactory().getSqlStringGenerationContext() + ); } private static int batchSize(Collection collection, SessionFactoryOptions options) { - int batchSize = collection.getBatchSize(); + final int batchSize = collection.getBatchSize(); return batchSize >= 0 ? batchSize : options.getDefaultBatchFetchSize(); @@ -538,8 +545,8 @@ private static int batchSize(Collection collection, SessionFactoryOptions option private static CacheEntryStructure cacheEntryStructure(Collection collection, SessionFactoryOptions options) { if ( options.isStructuredCacheEntriesEnabled() ) { return collection.isMap() - ? StructuredMapCacheEntry.INSTANCE - : StructuredCollectionCacheEntry.INSTANCE; + ? StructuredMapCacheEntry.INSTANCE + : StructuredCollectionCacheEntry.INSTANCE; } else { return UnstructuredCacheEntry.INSTANCE; @@ -887,7 +894,8 @@ public boolean useShallowQueryCacheLayout() { return useShallowQueryCacheLayout; } - protected abstract RowMutationOperations getRowMutationOperations(); + @Override + public abstract RowMutationOperations getRowMutationOperations(); protected abstract RemoveCoordinator getRemoveCoordinator(); @Override @@ -1013,6 +1021,7 @@ protected String generateSelectSizeString(boolean isIntegerIndexed) { return new SimpleSelect( getFactory() ) .setTableName( getTableName() ) .addRestriction( getKeyColumnNames() ) + .addRestriction( keyFormulas ) .addWhereToken( sqlWhereString ) .addColumn( selectValue ) .toStatementString(); @@ -1026,8 +1035,9 @@ protected String generateDetectRowByIndexString() { return new SimpleSelect( getFactory() ) .setTableName( getTableName() ) .addRestriction( getKeyColumnNames() ) + .addRestriction( getKeyFormulas() ) .addRestriction( getIndexColumnNames() ) - .addRestriction( indexFormulas ) + .addRestriction( getIndexFormulas() ) .addWhereToken( sqlWhereString ) .addColumn( "1" ) .toStatementString(); @@ -1039,8 +1049,9 @@ protected String generateDetectRowByElementString() { return new SimpleSelect( getFactory() ) .setTableName( getTableName() ) .addRestriction( getKeyColumnNames() ) + .addRestriction( getKeyFormulas() ) .addRestriction( getElementColumnNames() ) - .addRestriction( elementFormulas ) + .addRestriction( getElementFormulas() ) .addWhereToken( sqlWhereString ) .addColumn( "1" ) .toStatementString(); @@ -1058,6 +1069,18 @@ public String[] getKeyColumnNames() { return keyColumnNames; } + public String[] getKeyFormulas() { + return keyFormulas; + } + + public String[] getElementFormulas() { + return elementFormulas; + } + + public String[] getIndexFormulas() { + return indexFormulas; + } + @Override public boolean hasIndex() { return collectionSemantics.getCollectionClassification().isIndexed(); @@ -1090,7 +1113,12 @@ public void remove(Object id, SharedSessionContractImplementor session) throws H getRemoveCoordinator().deleteAllRows( id, session ); } - protected boolean isRowDeleteEnabled() { + boolean isHistoryStrategy() { + return getFactory().getSessionFactoryOptions().getTemporalTableStrategy() == HISTORY_TABLE; + } + + @Override + public boolean isRowDeleteEnabled() { return keyIsUpdateable; } @@ -1099,10 +1127,26 @@ public boolean needsRemove() { return !isInverse() && isRowDeleteEnabled(); } - protected boolean isRowInsertEnabled() { + @Override + public boolean isRowInsertEnabled() { return keyIsUpdateable; } + @Override + public boolean[] getIndexColumnIsSettable() { + return indexColumnIsSettable; + } + + @Override + public boolean[] getElementColumnIsSettable() { + return elementColumnIsSettable; + } + + @Override + public UnaryOperator getIndexIncrementer() { + return this::incrementIndexByBase; + } + public String getOwnerEntityName() { return ownerPersister.getEntityName(); } 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 95be9c0f067b..010b7ee89dac 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 @@ -19,19 +19,11 @@ import org.hibernate.mapping.Collection; import org.hibernate.metamodel.spi.RuntimeModelCreationContext; import org.hibernate.persister.collection.mutation.DeleteRowsCoordinator; -import org.hibernate.persister.collection.mutation.DeleteRowsCoordinatorNoOp; -import org.hibernate.persister.collection.mutation.DeleteRowsCoordinatorStandard; import org.hibernate.persister.collection.mutation.InsertRowsCoordinator; -import org.hibernate.persister.collection.mutation.InsertRowsCoordinatorNoOp; -import org.hibernate.persister.collection.mutation.InsertRowsCoordinatorStandard; import org.hibernate.persister.collection.mutation.OperationProducer; import org.hibernate.persister.collection.mutation.RemoveCoordinator; -import org.hibernate.persister.collection.mutation.RemoveCoordinatorNoOp; -import org.hibernate.persister.collection.mutation.RemoveCoordinatorStandard; import org.hibernate.persister.collection.mutation.RowMutationOperations; import org.hibernate.persister.collection.mutation.UpdateRowsCoordinator; -import org.hibernate.persister.collection.mutation.UpdateRowsCoordinatorNoOp; -import org.hibernate.persister.collection.mutation.UpdateRowsCoordinatorStandard; import org.hibernate.sql.ast.tree.expression.ColumnReference; import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.model.ast.ColumnValueBinding; @@ -48,6 +40,8 @@ import java.util.List; +import static org.hibernate.temporal.TemporalTableStrategy.NATIVE; +import static org.hibernate.temporal.TemporalTableStrategy.SINGLE_TABLE; import static org.hibernate.internal.util.collections.ArrayHelper.isAnyTrue; import static org.hibernate.internal.util.collections.CollectionHelper.arrayList; import static org.hibernate.persister.collection.mutation.RowMutationOperations.DEFAULT_RESTRICTOR; @@ -77,13 +71,15 @@ public BasicCollectionPersister( throws MappingException, CacheException { super( collectionBinding, cacheAccessStrategy, creationContext ); this.rowMutationOperations = buildRowMutationOperations(); - this.insertRowsCoordinator = buildInsertRowCoordinator(); - this.updateCoordinator = buildUpdateRowCoordinator(); - this.deleteRowsCoordinator = buildDeleteRowCoordinator(); - this.removeCoordinator = buildDeleteAllCoordinator(); + final var stateManagement = collectionBinding.getStateManagement(); + this.insertRowsCoordinator = stateManagement.createInsertRowsCoordinator( this ); + this.updateCoordinator = stateManagement.createUpdateRowsCoordinator( this ); + this.deleteRowsCoordinator = stateManagement.createDeleteRowsCoordinator( this ); + this.removeCoordinator = stateManagement.createRemoveCoordinator( this ); } - protected RowMutationOperations getRowMutationOperations() { + @Override + public RowMutationOperations getRowMutationOperations() { return rowMutationOperations; } @@ -130,93 +126,19 @@ protected void doProcessQueuedOps(PersistentCollection collection, Object id, } private boolean isPerformingUpdates() { - return getCollectionSemantics().getCollectionClassification().isRowUpdatePossible() - && isAnyTrue( elementColumnIsSettable ) - && !isInverse(); - } - - private UpdateRowsCoordinator buildUpdateRowCoordinator() { - if ( !isPerformingUpdates() ) { -// if ( MODEL_MUTATION_LOGGER.isTraceEnabled() ) { -// MODEL_MUTATION_LOGGER.tracef( -// "Skipping collection row updates - %s", -// getRolePath() -// ); -// } - return new UpdateRowsCoordinatorNoOp( this ); - } - else { - return new UpdateRowsCoordinatorStandard( - this, - rowMutationOperations, - getFactory() - ); - } - } - - private InsertRowsCoordinator buildInsertRowCoordinator() { - if ( isInverse() || !isRowInsertEnabled() ) { -// if ( MODEL_MUTATION_LOGGER.isTraceEnabled() ) { -// MODEL_MUTATION_LOGGER.tracef( -// "Skipping collection inserts - %s", -// getRolePath() -// ); -// } - return new InsertRowsCoordinatorNoOp( this ); - } - else { - return new InsertRowsCoordinatorStandard( - this, - rowMutationOperations, - getFactory().getServiceRegistry() - ); - } - } - - private DeleteRowsCoordinator buildDeleteRowCoordinator() { - if ( !needsRemove() ) { -// if ( MODEL_MUTATION_LOGGER.isTraceEnabled() ) { -// MODEL_MUTATION_LOGGER.tracef( -// "Skipping collection row deletions - %s", -// getRolePath() -// ); -// } - return new DeleteRowsCoordinatorNoOp( this ); - } - else { - return new DeleteRowsCoordinatorStandard( - this, - rowMutationOperations, - hasPhysicalIndexColumn(), - getFactory().getServiceRegistry() - ); - } + return !isInverse() + && getCollectionSemantics().getCollectionClassification().isRowUpdatePossible() + && isAnyTrue( elementColumnIsSettable ); } - private RemoveCoordinator buildDeleteAllCoordinator() { - if ( !needsRemove() ) { -// if ( MODEL_MUTATION_LOGGER.isTraceEnabled() ) { -// MODEL_MUTATION_LOGGER.tracef( -// "Skipping collection removals - %s", -// getRolePath() -// ); -// } - return new RemoveCoordinatorNoOp( this ); - } - else { - return new RemoveCoordinatorStandard( - this, - this::buildDeleteAllOperation, - getFactory().getServiceRegistry() - ); - } - } - - @Override public RestrictedTableMutation generateDeleteAllAst(MutatingTableReference tableReference) { final var attributeMapping = getAttributeMapping(); assert attributeMapping != null; + final var temporalMapping = attributeMapping.getTemporalMapping(); + if ( temporalMapping != null && shouldApplyTemporalOperations( tableReference ) ) { + return generateTemporalDeleteAllAst( tableReference ); + } final var softDeleteMapping = attributeMapping.getSoftDeleteMapping(); if ( softDeleteMapping == null ) { return super.generateDeleteAllAst( tableReference ); @@ -243,6 +165,30 @@ public RestrictedTableMutation generateDeleteAllAst(Mutat } } + protected RestrictedTableMutation generateTemporalDeleteAllAst(MutatingTableReference tableReference) { + final var attributeMapping = getAttributeMapping(); + final var temporalMapping = attributeMapping.getTemporalMapping(); + assert temporalMapping != null; + final var foreignKeyDescriptor = attributeMapping.getKeyDescriptor(); + assert foreignKeyDescriptor != null; + final int keyColumnCount = foreignKeyDescriptor.getJdbcTypeCount(); + final var parameterBinders = + new ColumnValueParameterList( tableReference, ParameterUsage.RESTRICT, keyColumnCount ); + final List restrictionBindings = arrayList( keyColumnCount ); + applyKeyRestrictions( parameterBinders, restrictionBindings ); + final var endingColumn = new ColumnReference( tableReference, temporalMapping.getEndingColumnMapping() ); + final var endingBinding = temporalMapping.createEndingValueBinding( endingColumn ); + final var nullEndingBinding = temporalMapping.createNullEndingValueBinding( endingColumn ); + return new TableUpdateStandard( + tableReference, + this, + "temporal removal", + List.of( endingBinding ), + restrictionBindings, + List.of( nullEndingBinding ) + ); + } + protected RowMutationOperations buildRowMutationOperations() { final OperationProducer insertRowOperationProducer; final RowMutationOperations.Values insertRowValues; @@ -281,6 +227,14 @@ protected RowMutationOperations buildRowMutationOperations() { deleteRowRestrictions = null; } + final OperationProducer deleteAllRowsOperationProducer; + if ( !isInverse() && isRowDeleteEnabled() ) { + deleteAllRowsOperationProducer = this::buildDeleteAllOperation; + } + else { + deleteAllRowsOperationProducer = null; + } + return new RowMutationOperations( this, insertRowOperationProducer, @@ -289,7 +243,8 @@ protected RowMutationOperations buildRowMutationOperations() { updateRowValues, updateRowRestrictions, deleteRowOperationProducer, - deleteRowRestrictions + deleteRowRestrictions, + deleteAllRowsOperationProducer ); } @@ -329,6 +284,15 @@ else if ( indexDescriptor != null ) { final var columnReference = new ColumnReference( insertBuilder.getMutatingTable(), softDeleteMapping ); insertBuilder.addValueColumn( softDeleteMapping.createNonDeletedValueBinding( columnReference ) ); } + final var temporalMapping = attributeMapping.getTemporalMapping(); + if ( temporalMapping != null && shouldApplyTemporalOperations( insertBuilder.getMutatingTable() ) ) { + final var startingColumnReference = + new ColumnReference( insertBuilder.getMutatingTable(), temporalMapping.getStartingColumnMapping() ); + insertBuilder.addValueColumn( temporalMapping.createStartingValueBinding( startingColumnReference ) ); + final var endingColumnReference = + new ColumnReference( insertBuilder.getMutatingTable(), temporalMapping.getEndingColumnMapping() ); + insertBuilder.addValueColumn( temporalMapping.createNullEndingValueBinding( endingColumnReference ) ); + } } private JdbcMutationOperation buildGeneratedInsertRowOperation(MutatingTableReference tableReference) { @@ -420,6 +384,15 @@ private void applyInsertRowValues( }, session ); + + final var temporalMapping = attributeMapping.getTemporalMapping(); + if ( temporalMapping != null && isUsingTransactionIdParameters( session ) ) { + jdbcValueBindings.bindValue( + session.getCurrentTransactionIdentifier(), + temporalMapping.getStartingColumnMapping(), + ParameterUsage.SET + ); + } } @@ -565,6 +538,10 @@ private JdbcMutationOperation generateDeleteRowOperation(MutatingTableReference private RestrictedTableMutation generateDeleteRowAst(MutatingTableReference tableReference) { final var pluralAttribute = getAttributeMapping(); assert pluralAttribute != null; + final var temporalMapping = pluralAttribute.getTemporalMapping(); + if ( temporalMapping != null && shouldApplyTemporalOperations( tableReference ) ) { + return generateTemporalDeleteRowsAst( tableReference ); + } final var softDeleteMapping = pluralAttribute.getSoftDeleteMapping(); if ( softDeleteMapping != null ) { return generateSoftDeleteRowsAst( tableReference ); @@ -633,6 +610,39 @@ protected RestrictedTableMutation generateSoftDeleteRowsA return updateBuilder.buildMutation(); } + protected RestrictedTableMutation generateTemporalDeleteRowsAst(MutatingTableReference tableReference) { + final var attributeMapping = getAttributeMapping(); + final var temporalMapping = attributeMapping.getTemporalMapping(); + assert temporalMapping != null; + final var foreignKeyDescriptor = attributeMapping.getKeyDescriptor(); + assert foreignKeyDescriptor != null; + final TableUpdateBuilderStandard updateBuilder = new TableUpdateBuilderStandard<>( + this, + tableReference, + getFactory(), + sqlWhereString + ); + final var identifierDescriptor = attributeMapping.getIdentifierDescriptor(); + if ( identifierDescriptor != null ) { + updateBuilder.addKeyRestrictionsLeniently( identifierDescriptor ); + } + else { + updateBuilder.addKeyRestrictionsLeniently( foreignKeyDescriptor.getKeyPart() ); + if ( hasIndex() && !indexContainsFormula ) { + assert attributeMapping.getIndexDescriptor() != null; + updateBuilder.addKeyRestrictionsLeniently( attributeMapping.getIndexDescriptor() ); + } + else { + updateBuilder.addKeyRestrictions( attributeMapping.getElementDescriptor() ); + } + } + + final var endingColumnReference = new ColumnReference( tableReference, temporalMapping.getEndingColumnMapping() ); + updateBuilder.addValueColumn( temporalMapping.createEndingValueBinding( endingColumnReference ) ); + updateBuilder.addNonKeyRestriction( temporalMapping.createNullEndingValueBinding( endingColumnReference ) ); + return updateBuilder.buildMutation(); + } + private void applyDeleteRowRestrictions( PersistentCollection collection, Object keyValue, @@ -641,6 +651,14 @@ private void applyDeleteRowRestrictions( SharedSessionContractImplementor session, JdbcValueBindings jdbcValueBindings) { final var attributeMapping = getAttributeMapping(); + final var temporalMapping = attributeMapping.getTemporalMapping(); + if ( temporalMapping != null && isUsingTransactionIdParameters( session ) ) { + jdbcValueBindings.bindValue( + session.getCurrentTransactionIdentifier(), + temporalMapping.getEndingColumnMapping(), + ParameterUsage.SET + ); + } final var identifierDescriptor = attributeMapping.getIdentifierDescriptor(); if ( identifierDescriptor != null ) { identifierDescriptor.decompose( @@ -698,6 +716,29 @@ public boolean isManyToMany() { return elementType instanceof EntityType; //instanceof AssociationType; } + private static boolean isUsingTransactionIdParameters(SharedSessionContractImplementor session) { + final var factory = session.getFactory(); + return factory.getSessionFactoryOptions().getTemporalTableStrategy() == SINGLE_TABLE + && !factory.getTransactionIdentifierService().useServerTimestamp( session.getDialect() ); + } + + private boolean isNativeTemporalTablesEnabled() { + return getFactory().getSessionFactoryOptions().getTemporalTableStrategy() == NATIVE; + } + + private boolean shouldApplyTemporalOperations(MutatingTableReference tableReference) { + final var attributeMapping = getAttributeMapping(); + if ( attributeMapping == null ) { + return false; + } + else { + final var temporalMapping = attributeMapping.getTemporalMapping(); + return temporalMapping != null + && !isNativeTemporalTablesEnabled() + && ( !isHistoryStrategy() || temporalMapping.getTableName().equals( tableReference.getTableName() ) ); + } + } + @Override public FilterAliasGenerator getFilterAliasGenerator(String rootAlias) { return new StaticFilterAliasGenerator( rootAlias ); diff --git a/hibernate-core/src/main/java/org/hibernate/persister/collection/CollectionPersister.java b/hibernate-core/src/main/java/org/hibernate/persister/collection/CollectionPersister.java index 0b68859fb23e..53d3f20760de 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/collection/CollectionPersister.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/collection/CollectionPersister.java @@ -8,10 +8,12 @@ import java.util.Map; import java.util.Set; import java.util.function.Consumer; +import java.util.function.UnaryOperator; import org.hibernate.Filter; import org.hibernate.HibernateException; import org.hibernate.Incubating; +import org.hibernate.Internal; import org.hibernate.MappingException; import org.hibernate.cache.spi.access.CollectionDataAccess; import org.hibernate.cache.spi.entry.CacheEntryStructure; @@ -27,6 +29,7 @@ import org.hibernate.metamodel.mapping.Restrictable; import org.hibernate.metamodel.model.domain.NavigableRole; import org.hibernate.metamodel.spi.RuntimeModelCreationContext; +import org.hibernate.persister.collection.mutation.RowMutationOperations; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.sql.ast.spi.SqlAstCreationState; import org.hibernate.sql.ast.tree.from.TableGroup; @@ -124,6 +127,24 @@ default boolean needsRemove() { return true; } + @Internal @Incubating + RowMutationOperations getRowMutationOperations(); + + @Internal @Incubating + boolean isRowInsertEnabled(); + + @Internal @Incubating + boolean isRowDeleteEnabled(); + + @Internal @Incubating + boolean[] getIndexColumnIsSettable(); + + @Internal @Incubating + boolean[] getElementColumnIsSettable(); + + @Internal @Incubating + UnaryOperator getIndexIncrementer(); + /** * Access to the collection's cache region */ diff --git a/hibernate-core/src/main/java/org/hibernate/persister/collection/OneToManyPersister.java b/hibernate-core/src/main/java/org/hibernate/persister/collection/OneToManyPersister.java index d29f16385c89..48cf50062ab7 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/collection/OneToManyPersister.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/collection/OneToManyPersister.java @@ -27,24 +27,11 @@ import org.hibernate.metamodel.mapping.internal.OneToManyCollectionPart; import org.hibernate.metamodel.spi.RuntimeModelCreationContext; import org.hibernate.persister.collection.mutation.DeleteRowsCoordinator; -import org.hibernate.persister.collection.mutation.DeleteRowsCoordinatorNoOp; -import org.hibernate.persister.collection.mutation.DeleteRowsCoordinatorStandard; -import org.hibernate.persister.collection.mutation.DeleteRowsCoordinatorTablePerSubclass; import org.hibernate.persister.collection.mutation.InsertRowsCoordinator; -import org.hibernate.persister.collection.mutation.InsertRowsCoordinatorNoOp; -import org.hibernate.persister.collection.mutation.InsertRowsCoordinatorStandard; -import org.hibernate.persister.collection.mutation.InsertRowsCoordinatorTablePerSubclass; import org.hibernate.persister.collection.mutation.OperationProducer; import org.hibernate.persister.collection.mutation.RemoveCoordinator; -import org.hibernate.persister.collection.mutation.RemoveCoordinatorNoOp; -import org.hibernate.persister.collection.mutation.RemoveCoordinatorStandard; -import org.hibernate.persister.collection.mutation.RemoveCoordinatorTablePerSubclass; import org.hibernate.persister.collection.mutation.RowMutationOperations; import org.hibernate.persister.collection.mutation.UpdateRowsCoordinator; -import org.hibernate.persister.collection.mutation.UpdateRowsCoordinatorNoOp; -import org.hibernate.persister.collection.mutation.UpdateRowsCoordinatorOneToMany; -import org.hibernate.persister.collection.mutation.UpdateRowsCoordinatorTablePerSubclass; -import org.hibernate.persister.entity.UnionSubclassEntityPersister; import org.hibernate.sql.ast.spi.SqlAstCreationState; import org.hibernate.sql.ast.tree.expression.ColumnReference; import org.hibernate.sql.ast.tree.from.TableGroup; @@ -65,7 +52,6 @@ import static org.hibernate.internal.util.collections.ArrayHelper.isAnyTrue; import static org.hibernate.internal.util.collections.CollectionHelper.arrayList; import static org.hibernate.persister.collection.mutation.RowMutationOperations.DEFAULT_RESTRICTOR; -import static org.hibernate.persister.collection.mutation.RowMutationOperations.DEFAULT_VALUE_SETTER; import static org.hibernate.sql.model.ast.builder.TableUpdateBuilder.NULL; import static org.hibernate.sql.model.internal.MutationOperationGroupFactory.singleOperation; @@ -108,16 +94,16 @@ && isAnyTrue( indexColumnIsSettable ) && !getElementPersisterInternal().managesColumns( indexColumnNames ); rowMutationOperations = buildRowMutationOperations(); - - insertRowsCoordinator = buildInsertCoordinator(); - updateRowsCoordinator = buildUpdateCoordinator(); - deleteRowsCoordinator = buildDeleteCoordinator(); - removeCoordinator = buildDeleteAllCoordinator(); + final var stateManagement = collectionBinding.getStateManagement(); + insertRowsCoordinator = stateManagement.createInsertRowsCoordinator( this ); + updateRowsCoordinator = stateManagement.createUpdateRowsCoordinator( this ); + deleteRowsCoordinator = stateManagement.createDeleteRowsCoordinator( this ); + removeCoordinator = stateManagement.createRemoveCoordinator( this ); mutationExecutorService = creationContext.getServiceRegistry().getService( MutationExecutorService.class ); } @Override - protected RowMutationOperations getRowMutationOperations() { + public RowMutationOperations getRowMutationOperations() { return rowMutationOperations; } @@ -139,7 +125,7 @@ protected RemoveCoordinator getRemoveCoordinator() { } @Override - protected boolean isRowDeleteEnabled() { + public boolean isRowDeleteEnabled() { return super.isRowDeleteEnabled() && keyIsNullable; } @@ -369,6 +355,14 @@ private RowMutationOperations buildRowMutationOperations() { deleteEntryRestrictions = null; } + final OperationProducer deleteAllEntriesOperationProducer; + if ( !isInverse() && isRowDeleteEnabled() ) { + deleteAllEntriesOperationProducer = this::buildDeleteAllOperation; + } + else { + deleteAllEntriesOperationProducer = null; + } + return new RowMutationOperations( this, insertRowOperationProducer, @@ -377,79 +371,11 @@ private RowMutationOperations buildRowMutationOperations() { writeIndexValues, writeIndexRestrictions, deleteEntryOperationProducer, - deleteEntryRestrictions + deleteEntryRestrictions, + deleteAllEntriesOperationProducer ); } - private InsertRowsCoordinator buildInsertCoordinator() { - if ( isInverse() || !isRowInsertEnabled() ) { -// if ( MODEL_MUTATION_LOGGER.isTraceEnabled() ) { -// MODEL_MUTATION_LOGGER.tracef( "Skipping collection (re)creation - %s", getRolePath() ); -// } - return new InsertRowsCoordinatorNoOp( this ); - } - else { - final var registry = getFactory().getServiceRegistry(); - final var elementPersister = getElementPersisterInternal(); - return elementPersister != null && elementPersister.hasSubclasses() - && elementPersister instanceof UnionSubclassEntityPersister - ? new InsertRowsCoordinatorTablePerSubclass( this, rowMutationOperations, registry ) - : new InsertRowsCoordinatorStandard( this, rowMutationOperations, registry ); - } - } - - private UpdateRowsCoordinator buildUpdateCoordinator() { - if ( !isRowDeleteEnabled() && !isRowInsertEnabled() ) { -// if ( MODEL_MUTATION_LOGGER.isTraceEnabled() ) { -// MODEL_MUTATION_LOGGER.tracef( "Skipping collection row updates - %s", getRolePath() ); -// } - return new UpdateRowsCoordinatorNoOp( this ); - } - else { - final var elementPersister = getElementPersisterInternal(); - final var factory = getFactory(); - return elementPersister != null && elementPersister.hasSubclasses() - && elementPersister instanceof UnionSubclassEntityPersister - ? new UpdateRowsCoordinatorTablePerSubclass( this, rowMutationOperations, factory ) - : new UpdateRowsCoordinatorOneToMany( this, rowMutationOperations, factory ); - } - } - - private DeleteRowsCoordinator buildDeleteCoordinator() { - if ( !needsRemove() ) { -// if ( MODEL_MUTATION_LOGGER.isTraceEnabled() ) { -// MODEL_MUTATION_LOGGER.tracef( "Skipping collection row deletions - %s", getRolePath() ); -// } - return new DeleteRowsCoordinatorNoOp( this ); - } - else { - final var elementPersister = getElementPersisterInternal(); - final var registry = getFactory().getServiceRegistry(); - return elementPersister != null && elementPersister.hasSubclasses() - && elementPersister instanceof UnionSubclassEntityPersister - // never delete by index for one-to-many - ? new DeleteRowsCoordinatorTablePerSubclass( this, rowMutationOperations, false, registry ) - : new DeleteRowsCoordinatorStandard( this, rowMutationOperations, false, registry ); - } - } - - private RemoveCoordinator buildDeleteAllCoordinator() { - if ( ! needsRemove() ) { -// if ( MODEL_MUTATION_LOGGER.isTraceEnabled() ) { -// MODEL_MUTATION_LOGGER.tracef( "Skipping collection removals - %s", getRolePath() ); -// } - return new RemoveCoordinatorNoOp( this ); - } - else { - final var registry = getFactory().getServiceRegistry(); - final var elementPersister = getElementPersisterInternal(); - return elementPersister != null && elementPersister.hasSubclasses() - && elementPersister instanceof UnionSubclassEntityPersister - ? new RemoveCoordinatorTablePerSubclass( this, this::buildDeleteAllOperation, registry ) - : new RemoveCoordinatorStandard( this, this::buildDeleteAllOperation, registry ); - } - } - private JdbcMutationOperation generateDeleteRowOperation(MutatingTableReference tableReference) { return getSqlAstTranslatorFactory() .buildModelMutationTranslator( generateDeleteRowAst( tableReference ), getFactory() ) @@ -517,7 +443,11 @@ private void applyDeleteRowRestrictions( 0, jdbcValueBindings, null, - DEFAULT_RESTRICTOR, + (valueIndex, bindings, noop, value, jdbcValueMapping) -> { + if ( !jdbcValueMapping.isFormula() ) { + bindings.bindValue( value, jdbcValueMapping, ParameterUsage.RESTRICT ); + } + }, session ); pluralAttribute.getElementDescriptor().decompose( @@ -541,7 +471,7 @@ private TableUpdate buildTableUpdate(MutatingTableReferen final TableUpdateBuilderStandard updateBuilder = new TableUpdateBuilderStandard<>( this, tableReference, getFactory(), sqlWhereString ); final var attributeMapping = getAttributeMapping(); - attributeMapping.getKeyDescriptor().getKeyPart().forEachSelectable( updateBuilder ); + attributeMapping.getKeyDescriptor().getKeyPart().forEachUpdatable( updateBuilder ); final var indexDescriptor = attributeMapping.getIndexDescriptor(); if ( indexDescriptor != null ) { indexDescriptor.forEachUpdatable( updateBuilder ); @@ -566,7 +496,11 @@ private void applyInsertRowValues( 0, jdbcValueBindings, null, - DEFAULT_VALUE_SETTER, + (valueIndex, bindings, noop, value, jdbcValueMapping) -> { + if ( jdbcValueMapping.isUpdateable() && !jdbcValueMapping.isFormula() ) { + bindings.bindValue( value, jdbcValueMapping, ParameterUsage.SET ); + } + }, session ); final var indexDescriptor = attributeMapping.getIndexDescriptor(); diff --git a/hibernate-core/src/main/java/org/hibernate/persister/collection/mutation/AbstractUpdateRowsCoordinator.java b/hibernate-core/src/main/java/org/hibernate/persister/collection/mutation/AbstractUpdateRowsCoordinator.java index 5b5658252627..ced669009629 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/collection/mutation/AbstractUpdateRowsCoordinator.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/collection/mutation/AbstractUpdateRowsCoordinator.java @@ -52,4 +52,18 @@ public void updateRows(Object key, PersistentCollection collection, SharedSes } protected abstract int doUpdate(Object key, PersistentCollection collection, SharedSessionContractImplementor session); + + protected Object resolveDeleteRowValue(PersistentCollection collection, Object entry, int entryPosition) { + final var attributeMapping = getMutationTarget().getTargetPart(); + final var identifierDescriptor = attributeMapping.getIdentifierDescriptor(); + if ( identifierDescriptor != null ) { + return collection.getIdentifier( entry, entryPosition ); + } + else if ( getMutationTarget().hasPhysicalIndexColumn() && attributeMapping.getIndexDescriptor() != null ) { + return collection.getIndex( entry, entryPosition, attributeMapping.getCollectionDescriptor() ); + } + else { + return collection.getSnapshotElement( entry, entryPosition ); + } + } } diff --git a/hibernate-core/src/main/java/org/hibernate/persister/collection/mutation/AuditCollectionHelper.java b/hibernate-core/src/main/java/org/hibernate/persister/collection/mutation/AuditCollectionHelper.java new file mode 100644 index 000000000000..cd462913f021 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/persister/collection/mutation/AuditCollectionHelper.java @@ -0,0 +1,124 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.persister.collection.mutation; + +import java.util.function.UnaryOperator; + +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.metamodel.mapping.AuditMapping; +import org.hibernate.metamodel.mapping.SelectableMapping; +import org.hibernate.sql.model.MutationOperationGroup; +import org.hibernate.sql.model.MutationType; +import org.hibernate.sql.model.ast.builder.TableInsertBuilderStandard; + +import static org.hibernate.sql.model.internal.MutationOperationGroupFactory.singleOperation; + +/** + * Support for building audit log mutations for collections. + */ +final class AuditCollectionHelper { + private final CollectionMutationTarget mutationTarget; + private final SessionFactoryImplementor sessionFactory; + private final CollectionTableMapping auditTableMapping; + private final SelectableMapping transactionIdMapping; + private final SelectableMapping modificationTypeMapping; + private final boolean useServerTransactionTimestamps; + private final String currentTimestampFunctionName; + private final boolean[] indexColumnIsSettable; + private final boolean[] elementColumnIsSettable; + private final UnaryOperator indexIncrementer; + + private MutationOperationGroup auditInsertOperationGroup; + private AuditCollectionRowMutationHelper rowMutationHelper; + + AuditCollectionHelper( + CollectionMutationTarget mutationTarget, + SessionFactoryImplementor sessionFactory, + boolean[] indexColumnIsSettable, + boolean[] elementColumnIsSettable, + UnaryOperator indexIncrementer, + AuditMapping auditMapping) { + this.mutationTarget = mutationTarget; + this.sessionFactory = sessionFactory; + this.indexColumnIsSettable = indexColumnIsSettable; + this.elementColumnIsSettable = elementColumnIsSettable; + this.indexIncrementer = indexIncrementer; + this.auditTableMapping = + new CollectionTableMapping( mutationTarget.getCollectionTableMapping(), + auditMapping.getTableName() ); + this.transactionIdMapping = auditMapping.getTransactionIdMapping(); + this.modificationTypeMapping = auditMapping.getModificationTypeMapping(); + + final var dialect = sessionFactory.getJdbcServices().getDialect(); + this.useServerTransactionTimestamps = + sessionFactory.getTransactionIdentifierService() + .useServerTimestamp( dialect ); + this.currentTimestampFunctionName = useServerTransactionTimestamps + ? dialect.currentTimestamp() + : null; + } + + CollectionTableMapping getAuditTableMapping() { + return auditTableMapping; + } + + MutationOperationGroup getAuditInsertOperationGroup() { + if ( auditInsertOperationGroup == null ) { + auditInsertOperationGroup = buildAuditInsertOperationGroup(); + } + return auditInsertOperationGroup; + } + + AuditCollectionRowMutationHelper getRowMutationHelper() { + if ( rowMutationHelper == null ) { + rowMutationHelper = new AuditCollectionRowMutationHelper( + mutationTarget, + auditTableMapping.getTableName(), + transactionIdMapping, + modificationTypeMapping, + indexColumnIsSettable, + elementColumnIsSettable, + indexIncrementer, + useServerTransactionTimestamps + ); + } + return rowMutationHelper; + } + + private MutationOperationGroup buildAuditInsertOperationGroup() { + final var insertBuilder = + new TableInsertBuilderStandard( mutationTarget, auditTableMapping, sessionFactory ); + applyAuditInsertDetails( insertBuilder ); + final var tableInsert = insertBuilder.buildMutation(); + final var operation = tableInsert.createMutationOperation( null, sessionFactory ); + return operation == null ? null : singleOperation( MutationType.INSERT, mutationTarget, operation ); + } + + private void applyAuditInsertDetails(TableInsertBuilderStandard insertBuilder) { + final var attributeMapping = mutationTarget.getTargetPart(); + attributeMapping.getKeyDescriptor().getKeyPart().forEachSelectable( insertBuilder ); + + final var identifierDescriptor = attributeMapping.getIdentifierDescriptor(); + if ( identifierDescriptor != null ) { + identifierDescriptor.forEachSelectable( insertBuilder ); + } + else { + final var indexDescriptor = attributeMapping.getIndexDescriptor(); + if ( indexDescriptor != null ) { + indexDescriptor.forEachInsertable( insertBuilder ); + } + } + + attributeMapping.getElementDescriptor().forEachInsertable( insertBuilder ); + + if ( useServerTransactionTimestamps ) { + insertBuilder.addValueColumn( currentTimestampFunctionName, transactionIdMapping ); + } + else { + insertBuilder.addValueColumn( "?", transactionIdMapping ); + } + insertBuilder.addValueColumn( "?", modificationTypeMapping ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/persister/collection/mutation/AuditCollectionRowMutationHelper.java b/hibernate-core/src/main/java/org/hibernate/persister/collection/mutation/AuditCollectionRowMutationHelper.java new file mode 100644 index 000000000000..a21516e92c1f --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/persister/collection/mutation/AuditCollectionRowMutationHelper.java @@ -0,0 +1,162 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.persister.collection.mutation; + +import java.util.function.UnaryOperator; + +import org.hibernate.collection.spi.PersistentCollection; +import org.hibernate.engine.jdbc.mutation.JdbcValueBindings; +import org.hibernate.engine.jdbc.mutation.ParameterUsage; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.metamodel.mapping.PluralAttributeMapping; +import org.hibernate.metamodel.mapping.SelectableMapping; +import org.hibernate.persister.state.internal.AuditStateManagement; + +/** + * Binds collection row values for audit table mutations. + */ +final class AuditCollectionRowMutationHelper { + private final CollectionMutationTarget mutationTarget; + private final PluralAttributeMapping attributeMapping; + private final String auditTableName; + private final SelectableMapping transactionIdMapping; + private final SelectableMapping modificationTypeMapping; + private final boolean[] indexColumnIsSettable; + private final boolean[] elementColumnIsSettable; + private final UnaryOperator indexIncrementer; + private final boolean useServerTransactionTimestamps; + + AuditCollectionRowMutationHelper( + CollectionMutationTarget mutationTarget, + String auditTableName, + SelectableMapping transactionIdMapping, + SelectableMapping modificationTypeMapping, + boolean[] indexColumnIsSettable, + boolean[] elementColumnIsSettable, + UnaryOperator indexIncrementer, + boolean useServerTransactionTimestamps) { + this.mutationTarget = mutationTarget; + this.attributeMapping = mutationTarget.getTargetPart(); + this.auditTableName = auditTableName; + this.transactionIdMapping = transactionIdMapping; + this.modificationTypeMapping = modificationTypeMapping; + this.indexColumnIsSettable = indexColumnIsSettable; + this.elementColumnIsSettable = elementColumnIsSettable; + this.indexIncrementer = indexIncrementer; + this.useServerTransactionTimestamps = useServerTransactionTimestamps; + } + + void bindInsertValues( + PersistentCollection collection, + Object key, + Object rowValue, + int rowPosition, + AuditStateManagement.ModificationType modificationType, + SharedSessionContractImplementor session, + JdbcValueBindings jdbcValueBindings) { + if ( key == null ) { + throw new IllegalArgumentException( "null key for collection: " + mutationTarget.getRolePath() ); + } + + attributeMapping.getKeyDescriptor().getKeyPart().decompose( + key, + 0, + jdbcValueBindings, + null, + this::bindSetValue, + session + ); + + final var identifierDescriptor = attributeMapping.getIdentifierDescriptor(); + if ( identifierDescriptor != null ) { + identifierDescriptor.decompose( + collection.getIdentifier( rowValue, rowPosition ), + 0, + jdbcValueBindings, + null, + this::bindSetValue, + session + ); + } + else { + final var indexDescriptor = attributeMapping.getIndexDescriptor(); + if ( indexDescriptor != null ) { + final Object index = indexIncrementer.apply( + collection.getIndex( rowValue, rowPosition, attributeMapping.getCollectionDescriptor() ) + ); + indexDescriptor.decompose( + index, + 0, + indexColumnIsSettable, + jdbcValueBindings, + (valueIndex, settable, bindings, jdbcValue, jdbcValueMapping) -> { + if ( settable[valueIndex] + && auditTableName.equals( jdbcValueMapping.getContainingTableExpression() ) + && !jdbcValueMapping.isFormula() ) { + bindings.bindValue( + jdbcValue, + auditTableName, + jdbcValueMapping.getSelectionExpression(), + ParameterUsage.SET + ); + } + }, + session + ); + } + } + + attributeMapping.getElementDescriptor().decompose( + collection.getElement( rowValue ), + 0, + elementColumnIsSettable, + jdbcValueBindings, + (valueIndex, settable, bindings, jdbcValue, jdbcValueMapping) -> { + if ( settable[valueIndex] && !jdbcValueMapping.isFormula() ) { + bindings.bindValue( + jdbcValue, + auditTableName, + jdbcValueMapping.getSelectionExpression(), + ParameterUsage.SET + ); + } + }, + session + ); + + if ( !useServerTransactionTimestamps ) { + jdbcValueBindings.bindValue( + session.getCurrentTransactionIdentifier(), + auditTableName, + transactionIdMapping.getSelectionExpression(), + ParameterUsage.SET + ); + } + + jdbcValueBindings.bindValue( + Integer.valueOf( modificationType.ordinal() ), + auditTableName, + modificationTypeMapping.getSelectionExpression(), + ParameterUsage.SET + ); + } + + private void bindSetValue( + int valueIndex, + JdbcValueBindings jdbcValueBindings, + Object unused, + Object jdbcValue, + SelectableMapping selectableMapping) { + if ( selectableMapping.isFormula() ) { + return; + } + jdbcValueBindings.bindValue( + jdbcValue, + auditTableName, + selectableMapping.getSelectionExpression(), + ParameterUsage.SET + ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/persister/collection/mutation/CollectionTableMapping.java b/hibernate-core/src/main/java/org/hibernate/persister/collection/mutation/CollectionTableMapping.java index 0290d9f6ea2c..0a1f51630581 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/collection/mutation/CollectionTableMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/collection/mutation/CollectionTableMapping.java @@ -45,6 +45,34 @@ public CollectionTableMapping( this.deleteRowDetails = deleteRowDetails; } + /** + * Creates an auxiliary table mapping (for history or audit tables) + * based on an existing collection table mapping. + */ + public CollectionTableMapping(CollectionTableMapping baseMapping, String tableName) { + this.tableName = tableName; + this.spaces = appendSpace( baseMapping.spaces, tableName ); + this.isJoinTable = baseMapping.isJoinTable; + this.isInverse = baseMapping.isInverse; + this.insertDetails = baseMapping.insertDetails; + this.updateDetails = baseMapping.updateDetails; + this.cascadeDeleteEnabled = baseMapping.cascadeDeleteEnabled; + this.deleteAllDetails = baseMapping.deleteAllDetails; + this.deleteRowDetails = baseMapping.deleteRowDetails; + } + + private static String[] appendSpace(String[] baseSpaces, String newSpace) { + for ( String space : baseSpaces ) { + if ( newSpace.equals( space ) ) { + return baseSpaces; + } + } + final var spaces = new String[baseSpaces.length + 1]; + System.arraycopy( baseSpaces, 0, spaces, 0, baseSpaces.length ); + spaces[baseSpaces.length] = newSpace; + return spaces; + } + @Override public String getTableName() { return tableName; diff --git a/hibernate-core/src/main/java/org/hibernate/persister/collection/mutation/DeleteRowsCoordinatorAudit.java b/hibernate-core/src/main/java/org/hibernate/persister/collection/mutation/DeleteRowsCoordinatorAudit.java new file mode 100644 index 000000000000..2a963cc2d7cb --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/persister/collection/mutation/DeleteRowsCoordinatorAudit.java @@ -0,0 +1,128 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.persister.collection.mutation; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.UnaryOperator; + +import org.hibernate.collection.spi.PersistentCollection; +import org.hibernate.engine.jdbc.batch.internal.BasicBatchKey; +import org.hibernate.engine.jdbc.mutation.spi.MutationExecutorService; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.persister.collection.CollectionPersister; +import org.hibernate.persister.state.internal.AuditStateManagement; +import org.hibernate.sql.model.MutationOperationGroup; + +/** + * DeleteRowsCoordinator for audited collections. + */ +public class DeleteRowsCoordinatorAudit implements DeleteRowsCoordinator { + private final CollectionMutationTarget mutationTarget; + private final DeleteRowsCoordinator currentDeleteCoordinator; + private final SessionFactoryImplementor sessionFactory; + private final boolean deleteByIndex; + private final MutationExecutorService mutationExecutorService; + private final BasicBatchKey auditBatchKey; + private final boolean[] indexColumnIsSettable; + private final boolean[] elementColumnIsSettable; + private final UnaryOperator indexIncrementer; + + private MutationOperationGroup auditOperationGroup; + private AuditCollectionHelper auditHelper; + + public DeleteRowsCoordinatorAudit( + CollectionMutationTarget mutationTarget, + DeleteRowsCoordinator currentDeleteCoordinator, + boolean deleteByIndex, + boolean[] indexColumnIsSettable, + boolean[] elementColumnIsSettable, + UnaryOperator indexIncrementer, + SessionFactoryImplementor sessionFactory) { + this.mutationTarget = mutationTarget; + this.currentDeleteCoordinator = currentDeleteCoordinator; + this.sessionFactory = sessionFactory; + this.deleteByIndex = deleteByIndex; + this.indexColumnIsSettable = indexColumnIsSettable; + this.elementColumnIsSettable = elementColumnIsSettable; + this.indexIncrementer = indexIncrementer; + this.auditBatchKey = new BasicBatchKey( mutationTarget.getRolePath() + "#AUDIT_INSERT" ); + this.mutationExecutorService = sessionFactory.getServiceRegistry().getService( MutationExecutorService.class ); + } + + @Override + public CollectionMutationTarget getMutationTarget() { + return mutationTarget; + } + + @Override + public void deleteRows( + PersistentCollection collection, + Object key, + SharedSessionContractImplementor session) { + final var collectionDescriptor = mutationTarget.getTargetPart().getCollectionDescriptor(); + final var deletions = collectDeletions( collection, collectionDescriptor ); + + currentDeleteCoordinator.deleteRows( collection, key, session ); + + if ( !deletions.isEmpty() ) { + if ( auditOperationGroup == null ) { + auditOperationGroup = getAuditHelper().getAuditInsertOperationGroup(); + } + if ( auditOperationGroup != null ) { + final var auditExecutor = mutationExecutorService.createExecutor( + () -> auditBatchKey, + auditOperationGroup, + session + ); + try { + final var bindings = getAuditHelper().getRowMutationHelper(); + for ( int i = 0; i < deletions.size(); i++ ) { + final Object removal = deletions.get( i ); + bindings.bindInsertValues( + collection, + key, + removal, + i, + AuditStateManagement.ModificationType.DEL, + session, + auditExecutor.getJdbcValueBindings() + ); + auditExecutor.execute( removal, null, null, null, session ); + } + } + finally { + auditExecutor.release(); + } + } + } + } + + private AuditCollectionHelper getAuditHelper() { + if ( auditHelper == null ) { + auditHelper = new AuditCollectionHelper( + mutationTarget, + sessionFactory, + indexColumnIsSettable, + elementColumnIsSettable, + indexIncrementer, + mutationTarget.getTargetPart().getAuditMapping() + ); + } + return auditHelper; + } + + private List collectDeletions( + PersistentCollection collection, + CollectionPersister persister) { + final List deletions = new ArrayList<>(); + final var deletes = collection.getDeletes( persister, !deleteByIndex ); + while ( deletes.hasNext() ) { + deletions.add( deletes.next() ); + } + return deletions; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/persister/collection/mutation/DeleteRowsCoordinatorHistory.java b/hibernate-core/src/main/java/org/hibernate/persister/collection/mutation/DeleteRowsCoordinatorHistory.java new file mode 100644 index 000000000000..9a51692b9afc --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/persister/collection/mutation/DeleteRowsCoordinatorHistory.java @@ -0,0 +1,178 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.persister.collection.mutation; + +import java.util.function.UnaryOperator; + +import org.hibernate.collection.spi.PersistentCollection; +import org.hibernate.engine.jdbc.batch.internal.BasicBatchKey; +import org.hibernate.engine.jdbc.mutation.spi.MutationExecutorService; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.service.ServiceRegistry; +import org.hibernate.sql.model.MutationOperationGroup; +import org.hibernate.sql.model.MutationType; + +import static org.hibernate.sql.model.ModelMutationLogging.MODEL_MUTATION_LOGGER; +import static org.hibernate.sql.model.internal.MutationOperationGroupFactory.singleOperation; + +/** + * {@link DeleteRowsCoordinator} implementation for temporal collection tables + * in the {@link org.hibernate.temporal.TemporalTableStrategy#HISTORY_TABLE} + * temporal table mapping strategy. + * + * @author Gavin King + */ +public class DeleteRowsCoordinatorHistory implements DeleteRowsCoordinator { + private final CollectionMutationTarget mutationTarget; + private final RowMutationOperations rowMutationOperations; + private final boolean deleteByIndex; + private final boolean[] indexColumnIsSettable; + private final boolean[] elementColumnIsSettable; + private final UnaryOperator indexIncrementer; + private final MutationExecutorService mutationExecutorService; + private final BasicBatchKey deleteBatchKey; + private final BasicBatchKey historyBatchKey; + + private MutationOperationGroup deleteOperationGroup; + private MutationOperationGroup historyOperationGroup; + private CollectionTableMapping historyTableMapping; + private HistoryCollectionRowMutationHelper rowMutationHelper; + + public DeleteRowsCoordinatorHistory( + CollectionMutationTarget mutationTarget, + RowMutationOperations rowMutationOperations, + boolean deleteByIndex, + boolean[] indexColumnIsSettable, + boolean[] elementColumnIsSettable, + UnaryOperator indexIncrementer, + ServiceRegistry serviceRegistry) { + this.mutationTarget = mutationTarget; + this.rowMutationOperations = rowMutationOperations; + this.deleteByIndex = deleteByIndex; + this.indexColumnIsSettable = indexColumnIsSettable; + this.elementColumnIsSettable = elementColumnIsSettable; + this.indexIncrementer = indexIncrementer; + this.deleteBatchKey = new BasicBatchKey( mutationTarget.getRolePath() + "#DELETE" ); + this.historyBatchKey = new BasicBatchKey( mutationTarget.getRolePath() + "#HISTORY_DELETE" ); + this.mutationExecutorService = serviceRegistry.getService( MutationExecutorService.class ); + } + + @Override + public CollectionMutationTarget getMutationTarget() { + return mutationTarget; + } + + @Override + public void deleteRows(PersistentCollection collection, Object key, SharedSessionContractImplementor session) { + if ( deleteOperationGroup == null ) { + deleteOperationGroup = createOperationGroup(); + } + if ( deleteOperationGroup == null ) { + return; + } + if ( historyOperationGroup == null ) { + historyOperationGroup = createHistoryOperationGroup(); + } + + if ( MODEL_MUTATION_LOGGER.isTraceEnabled() ) { + MODEL_MUTATION_LOGGER.deletingRemovedCollectionRows( mutationTarget.getRolePath(), key ); + } + + final var mutationExecutor = mutationExecutorService.createExecutor( + () -> deleteBatchKey, + deleteOperationGroup, + session + ); + final var historyExecutor = historyOperationGroup == null ? null : mutationExecutorService.createExecutor( + () -> historyBatchKey, + historyOperationGroup, + session + ); + + try { + final var pluralAttribute = mutationTarget.getTargetPart(); + final var collectionDescriptor = pluralAttribute.getCollectionDescriptor(); + + final var deletes = collection.getDeletes( collectionDescriptor, !deleteByIndex ); + if ( !deletes.hasNext() ) { + MODEL_MUTATION_LOGGER.noRowsToDelete(); + return; + } + + int deletionCount = 0; + final var restrictions = rowMutationOperations.getDeleteRowRestrictions(); + final var historyBindings = getRowMutationHelper(); + + while ( deletes.hasNext() ) { + final Object removal = deletes.next(); + + restrictions.applyRestrictions( + collection, + key, + removal, + deletionCount, + session, + mutationExecutor.getJdbcValueBindings() + ); + mutationExecutor.execute( removal, null, null, null, session ); + + if ( historyExecutor != null ) { + historyBindings.bindDeleteRowRestrictions( + collection, + key, + removal, + deletionCount, + session, + historyExecutor.getJdbcValueBindings() + ); + historyExecutor.execute( removal, null, null, null, session ); + } + + deletionCount++; + } + + MODEL_MUTATION_LOGGER.doneDeletingCollectionRows( deletionCount, mutationTarget.getRolePath() ); + } + finally { + mutationExecutor.release(); + if ( historyExecutor != null ) { + historyExecutor.release(); + } + } + } + + private MutationOperationGroup createOperationGroup() { + final var operation = rowMutationOperations.getDeleteRowOperation(); + return operation == null ? null : singleOperation( MutationType.DELETE, mutationTarget, operation ); + } + + private MutationOperationGroup createHistoryOperationGroup() { + final var operation = rowMutationOperations.getDeleteRowOperation( getHistoryTableMapping() ); + return operation == null ? null : singleOperation( MutationType.DELETE, mutationTarget, operation ); + } + + private CollectionTableMapping getHistoryTableMapping() { + if ( historyTableMapping == null ) { + final var temporalMapping = mutationTarget.getTargetPart().getTemporalMapping(); + historyTableMapping = + new CollectionTableMapping( mutationTarget.getCollectionTableMapping(), + temporalMapping.getTableName() ); + } + return historyTableMapping; + } + + private HistoryCollectionRowMutationHelper getRowMutationHelper() { + if ( rowMutationHelper == null ) { + rowMutationHelper = new HistoryCollectionRowMutationHelper( + mutationTarget, + getHistoryTableMapping().getTableName(), + indexColumnIsSettable, + elementColumnIsSettable, + indexIncrementer + ); + } + return rowMutationHelper; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/persister/collection/mutation/DeleteRowsCoordinatorStandard.java b/hibernate-core/src/main/java/org/hibernate/persister/collection/mutation/DeleteRowsCoordinatorStandard.java index f6698fb4d754..b643a25ea76c 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/collection/mutation/DeleteRowsCoordinatorStandard.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/collection/mutation/DeleteRowsCoordinatorStandard.java @@ -70,31 +70,33 @@ public void deleteRows(PersistentCollection collection, Object key, SharedSes final var deletes = collection.getDeletes( collectionDescriptor, !deleteByIndex ); if ( !deletes.hasNext() ) { MODEL_MUTATION_LOGGER.noRowsToDelete(); - return; } + else { - int deletionCount = 0; + int deletionCount = 0; - final var restrictions = rowMutationOperations.getDeleteRowRestrictions(); + final var restrictions = rowMutationOperations.getDeleteRowRestrictions(); - while ( deletes.hasNext() ) { - final Object removal = deletes.next(); + while ( deletes.hasNext() ) { + final Object removal = deletes.next(); - restrictions.applyRestrictions( - collection, - key, - removal, - deletionCount, - session, - jdbcValueBindings - ); + restrictions.applyRestrictions( + collection, + key, + removal, + deletionCount, + session, + jdbcValueBindings + ); - mutationExecutor.execute( removal, null, null, null, session ); + mutationExecutor.execute( removal, null, null, null, session ); - deletionCount++; - } + deletionCount++; + + } - MODEL_MUTATION_LOGGER.doneDeletingCollectionRows( deletionCount, mutationTarget.getRolePath() ); + MODEL_MUTATION_LOGGER.doneDeletingCollectionRows( deletionCount, mutationTarget.getRolePath() ); + } } finally { mutationExecutor.release(); @@ -105,7 +107,7 @@ private MutationOperationGroup createOperationGroup() { assert mutationTarget.getTargetPart() != null && mutationTarget.getTargetPart().getKeyDescriptor() != null; - final var operation = rowMutationOperations.getDeleteRowOperation(); - return singleOperation( MutationType.DELETE, mutationTarget, operation ); + return singleOperation( MutationType.DELETE, mutationTarget, + rowMutationOperations.getDeleteRowOperation() ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/persister/collection/mutation/HistoryCollectionRowMutationHelper.java b/hibernate-core/src/main/java/org/hibernate/persister/collection/mutation/HistoryCollectionRowMutationHelper.java new file mode 100644 index 000000000000..5d1d6714fde4 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/persister/collection/mutation/HistoryCollectionRowMutationHelper.java @@ -0,0 +1,257 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.persister.collection.mutation; + +import java.util.function.UnaryOperator; + +import org.hibernate.collection.spi.PersistentCollection; +import org.hibernate.engine.jdbc.mutation.JdbcValueBindings; +import org.hibernate.engine.jdbc.mutation.ParameterUsage; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.metamodel.mapping.PluralAttributeMapping; +import org.hibernate.metamodel.mapping.SelectableMapping; +import org.hibernate.metamodel.mapping.TemporalMapping; +import org.hibernate.persister.entity.mutation.TemporalMutationHelper; + +/** + * Binds collection row values and restrictions for history table mutations. + */ +final class HistoryCollectionRowMutationHelper { + private final CollectionMutationTarget mutationTarget; + private final PluralAttributeMapping attributeMapping; + private final TemporalMapping temporalMapping; + private final String historyTableName; + private final String currentTableName; + private final boolean[] indexColumnIsSettable; + private final boolean[] elementColumnIsSettable; + private final UnaryOperator indexIncrementer; + + HistoryCollectionRowMutationHelper( + CollectionMutationTarget mutationTarget, + String historyTableName, + boolean[] indexColumnIsSettable, + boolean[] elementColumnIsSettable, + UnaryOperator indexIncrementer) { + this.mutationTarget = mutationTarget; + this.attributeMapping = mutationTarget.getTargetPart(); + this.temporalMapping = attributeMapping.getTemporalMapping(); + this.historyTableName = historyTableName; + this.currentTableName = mutationTarget.getCollectionTableMapping().getTableName(); + this.indexColumnIsSettable = indexColumnIsSettable; + this.elementColumnIsSettable = elementColumnIsSettable; + this.indexIncrementer = indexIncrementer; + } + + void bindInsertValues( + PersistentCollection collection, + Object key, + Object rowValue, + int rowPosition, + SharedSessionContractImplementor session, + JdbcValueBindings jdbcValueBindings) { + if ( key == null ) { + throw new IllegalArgumentException( "null key for collection: " + mutationTarget.getRolePath() ); + } + attributeMapping.getKeyDescriptor().getKeyPart().decompose( + key, + 0, + jdbcValueBindings, + null, + this::bindSetValue, + session + ); + + final var identifierDescriptor = attributeMapping.getIdentifierDescriptor(); + if ( identifierDescriptor != null ) { + identifierDescriptor.decompose( + collection.getIdentifier( rowValue, rowPosition ), + 0, + jdbcValueBindings, + null, + this::bindSetValue, + session + ); + } + else { + final var indexDescriptor = attributeMapping.getIndexDescriptor(); + if ( indexDescriptor != null ) { + final Object index = + indexIncrementer.apply( collection.getIndex( rowValue, rowPosition, + attributeMapping.getCollectionDescriptor() ) ); + indexDescriptor.decompose( + index, + 0, + indexColumnIsSettable, + jdbcValueBindings, + (valueIndex, settable, bindings, jdbcValue, jdbcValueMapping) -> { + if ( settable[valueIndex] + && currentTableName.equals( jdbcValueMapping.getContainingTableExpression() ) + && !jdbcValueMapping.isFormula() ) { + bindings.bindValue( + jdbcValue, + historyTableName, + jdbcValueMapping.getSelectionExpression(), + ParameterUsage.SET + ); + } + }, + session + ); + } + } + + attributeMapping.getElementDescriptor().decompose( + collection.getElement( rowValue ), + 0, + elementColumnIsSettable, + jdbcValueBindings, + (valueIndex, settable, bindings, jdbcValue, jdbcValueMapping) -> { + if ( settable[valueIndex] && !jdbcValueMapping.isFormula() ) { + bindings.bindValue( + jdbcValue, + historyTableName, + jdbcValueMapping.getSelectionExpression(), + ParameterUsage.SET + ); + } + }, + session + ); + + if ( temporalMapping != null && TemporalMutationHelper.isUsingParameters( session ) ) { + jdbcValueBindings.bindValue( + session.getCurrentTransactionIdentifier(), + historyTableName, + temporalMapping.getStartingColumnMapping().getSelectionExpression(), + ParameterUsage.SET + ); + } + } + + void bindDeleteRowRestrictions( + PersistentCollection collection, + Object keyValue, + Object rowValue, + int rowPosition, + SharedSessionContractImplementor session, + JdbcValueBindings jdbcValueBindings) { + if ( temporalMapping != null && TemporalMutationHelper.isUsingParameters( session ) ) { + jdbcValueBindings.bindValue( + session.getCurrentTransactionIdentifier(), + historyTableName, + temporalMapping.getEndingColumnMapping().getSelectionExpression(), + ParameterUsage.SET + ); + } + + final var identifierDescriptor = attributeMapping.getIdentifierDescriptor(); + if ( identifierDescriptor != null ) { + identifierDescriptor.decompose( + rowValue, + 0, + jdbcValueBindings, + null, + this::bindRestrictValue, + session + ); + } + else { + attributeMapping.getKeyDescriptor().getKeyPart().decompose( + keyValue, + 0, + jdbcValueBindings, + null, + this::bindRestrictValue, + session + ); + + if ( mutationTarget.hasPhysicalIndexColumn() ) { + attributeMapping.getIndexDescriptor().decompose( + indexIncrementer.apply( rowValue ), + 0, + jdbcValueBindings, + null, + this::bindRestrictValue, + session + ); + } + else { + attributeMapping.getElementDescriptor().decompose( + rowValue, + 0, + jdbcValueBindings, + null, + (valueIndex, bindings, unused, jdbcValue, jdbcValueMapping) -> { + if ( !jdbcValueMapping.isNullable() && !jdbcValueMapping.isFormula() ) { + bindings.bindValue( + jdbcValue, + historyTableName, + jdbcValueMapping.getSelectionExpression(), + ParameterUsage.RESTRICT + ); + } + }, + session + ); + } + } + } + + void bindDeleteAllRestrictions( + Object keyValue, + SharedSessionContractImplementor session, + JdbcValueBindings jdbcValueBindings) { + attributeMapping.getKeyDescriptor().getKeyPart().decompose( + keyValue, + 0, + jdbcValueBindings, + null, + this::bindRestrictValue, + session + ); + if ( temporalMapping != null && TemporalMutationHelper.isUsingParameters( session ) ) { + jdbcValueBindings.bindValue( + session.getCurrentTransactionIdentifier(), + historyTableName, + temporalMapping.getEndingColumnMapping().getSelectionExpression(), + ParameterUsage.SET + ); + } + } + + private void bindSetValue( + int valueIndex, + JdbcValueBindings jdbcValueBindings, + Object unused, + Object jdbcValue, + SelectableMapping selectableMapping) { + bindValue( jdbcValueBindings, jdbcValue, selectableMapping, ParameterUsage.SET ); + } + + private void bindRestrictValue( + int valueIndex, + JdbcValueBindings jdbcValueBindings, + Object unused, + Object jdbcValue, + SelectableMapping selectableMapping) { + bindValue( jdbcValueBindings, jdbcValue, selectableMapping, ParameterUsage.RESTRICT ); + } + + private void bindValue( + JdbcValueBindings jdbcValueBindings, + Object jdbcValue, + SelectableMapping selectableMapping, + ParameterUsage usage) { + if ( selectableMapping.isFormula() ) { + return; + } + jdbcValueBindings.bindValue( + jdbcValue, + historyTableName, + selectableMapping.getSelectionExpression(), + usage + ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/persister/collection/mutation/InsertRowsCoordinatorAudit.java b/hibernate-core/src/main/java/org/hibernate/persister/collection/mutation/InsertRowsCoordinatorAudit.java new file mode 100644 index 000000000000..751c57ad5735 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/persister/collection/mutation/InsertRowsCoordinatorAudit.java @@ -0,0 +1,121 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.persister.collection.mutation; + +import java.util.function.UnaryOperator; + +import org.hibernate.collection.spi.PersistentCollection; +import org.hibernate.engine.jdbc.batch.internal.BasicBatchKey; +import org.hibernate.engine.jdbc.mutation.spi.MutationExecutorService; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.persister.state.internal.AuditStateManagement; +import org.hibernate.sql.model.MutationOperationGroup; + +/** + * InsertRowsCoordinator for audited collections. + */ +public class InsertRowsCoordinatorAudit implements InsertRowsCoordinator { + private final CollectionMutationTarget mutationTarget; + private final InsertRowsCoordinator currentInsertCoordinator; + private final SessionFactoryImplementor sessionFactory; + private final MutationExecutorService mutationExecutorService; + private final BasicBatchKey auditBatchKey; + private final boolean[] indexColumnIsSettable; + private final boolean[] elementColumnIsSettable; + private final UnaryOperator indexIncrementer; + + private MutationOperationGroup auditOperationGroup; + private AuditCollectionHelper auditHelper; + + public InsertRowsCoordinatorAudit( + CollectionMutationTarget mutationTarget, + RowMutationOperations rowMutationOperations, + InsertRowsCoordinator currentInsertCoordinator, + boolean[] indexColumnIsSettable, + boolean[] elementColumnIsSettable, + UnaryOperator indexIncrementer, + SessionFactoryImplementor sessionFactory) { + this.mutationTarget = mutationTarget; + this.currentInsertCoordinator = currentInsertCoordinator; + this.sessionFactory = sessionFactory; + this.indexColumnIsSettable = indexColumnIsSettable; + this.elementColumnIsSettable = elementColumnIsSettable; + this.indexIncrementer = indexIncrementer; + this.auditBatchKey = new BasicBatchKey( mutationTarget.getRolePath() + "#AUDIT_INSERT" ); + this.mutationExecutorService = sessionFactory.getServiceRegistry().getService( MutationExecutorService.class ); + } + + @Override + public CollectionMutationTarget getMutationTarget() { + return mutationTarget; + } + + @Override + public void insertRows( + PersistentCollection collection, + Object id, + EntryFilter entryChecker, + SharedSessionContractImplementor session) { + currentInsertCoordinator.insertRows( collection, id, entryChecker, session ); + + if ( auditOperationGroup == null ) { + auditOperationGroup = getAuditHelper().getAuditInsertOperationGroup(); + } + if ( auditOperationGroup == null ) { + return; + } + + final var pluralAttribute = mutationTarget.getTargetPart(); + final var collectionDescriptor = pluralAttribute.getCollectionDescriptor(); + final var entries = collection.entries( collectionDescriptor ); + if ( entries.hasNext() ) { + final var mutationExecutor = mutationExecutorService.createExecutor( + () -> auditBatchKey, + auditOperationGroup, + session + ); + + try { + int entryCount = 0; + final var bindings = getAuditHelper().getRowMutationHelper(); + while ( entries.hasNext() ) { + final Object entry = entries.next(); + if ( entryChecker == null || entryChecker.include( entry, entryCount, collection, + pluralAttribute ) ) { + bindings.bindInsertValues( + collection, + id, + entry, + entryCount, + AuditStateManagement.ModificationType.ADD, + session, + mutationExecutor.getJdbcValueBindings() + ); + mutationExecutor.execute( entry, null, null, null, session ); + } + entryCount++; + } + } + finally { + mutationExecutor.release(); + } + } + } + + private AuditCollectionHelper getAuditHelper() { + if ( auditHelper == null ) { + auditHelper = new AuditCollectionHelper( + mutationTarget, + sessionFactory, + indexColumnIsSettable, + elementColumnIsSettable, + indexIncrementer, + mutationTarget.getTargetPart().getAuditMapping() + ); + } + return auditHelper; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/persister/collection/mutation/InsertRowsCoordinatorHistory.java b/hibernate-core/src/main/java/org/hibernate/persister/collection/mutation/InsertRowsCoordinatorHistory.java new file mode 100644 index 000000000000..11f10787c975 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/persister/collection/mutation/InsertRowsCoordinatorHistory.java @@ -0,0 +1,142 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.persister.collection.mutation; + +import java.util.function.UnaryOperator; + +import org.hibernate.collection.spi.PersistentCollection; +import org.hibernate.engine.jdbc.batch.internal.BasicBatchKey; +import org.hibernate.engine.jdbc.mutation.spi.MutationExecutorService; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.service.ServiceRegistry; +import org.hibernate.sql.model.MutationOperationGroup; +import org.hibernate.sql.model.MutationType; + +import static org.hibernate.sql.model.internal.MutationOperationGroupFactory.singleOperation; + +/** + * {@link InsertRowsCoordinator} implementation for temporal collection tables + * in the {@link org.hibernate.temporal.TemporalTableStrategy#HISTORY_TABLE} + * temporal table mapping strategy. + * + * @author Gavin King + */ +public class InsertRowsCoordinatorHistory implements InsertRowsCoordinator { + private final CollectionMutationTarget mutationTarget; + private final RowMutationOperations rowMutationOperations; + private final InsertRowsCoordinator currentInsertCoordinator; + private final MutationExecutorService mutationExecutorService; + private final BasicBatchKey historyBatchKey; + private final boolean[] indexColumnIsSettable; + private final boolean[] elementColumnIsSettable; + private final UnaryOperator indexIncrementer; + + private MutationOperationGroup historyOperationGroup; + private CollectionTableMapping historyTableMapping; + private HistoryCollectionRowMutationHelper rowMutationHelper; + + public InsertRowsCoordinatorHistory( + CollectionMutationTarget mutationTarget, + RowMutationOperations rowMutationOperations, + InsertRowsCoordinator currentInsertCoordinator, + boolean[] indexColumnIsSettable, + boolean[] elementColumnIsSettable, + UnaryOperator indexIncrementer, + ServiceRegistry serviceRegistry) { + this.mutationTarget = mutationTarget; + this.rowMutationOperations = rowMutationOperations; + this.currentInsertCoordinator = currentInsertCoordinator; + this.indexColumnIsSettable = indexColumnIsSettable; + this.elementColumnIsSettable = elementColumnIsSettable; + this.indexIncrementer = indexIncrementer; + this.historyBatchKey = new BasicBatchKey( mutationTarget.getRolePath() + "#HISTORY_INSERT" ); + this.mutationExecutorService = serviceRegistry.getService( MutationExecutorService.class ); + } + + @Override + public CollectionMutationTarget getMutationTarget() { + return mutationTarget; + } + + @Override + public void insertRows( + PersistentCollection collection, + Object id, + EntryFilter entryChecker, + SharedSessionContractImplementor session) { + currentInsertCoordinator.insertRows( collection, id, entryChecker, session ); + + if ( historyOperationGroup == null ) { + historyOperationGroup = createHistoryOperationGroup(); + } + if ( historyOperationGroup == null ) { + return; + } + + final var pluralAttribute = mutationTarget.getTargetPart(); + final var collectionDescriptor = pluralAttribute.getCollectionDescriptor(); + final var entries = collection.entries( collectionDescriptor ); + if ( !entries.hasNext() ) { + return; + } + + final var mutationExecutor = mutationExecutorService.createExecutor( + () -> historyBatchKey, + historyOperationGroup, + session + ); + + try { + int entryCount = 0; + final var historyBindings = getRowMutationHelper(); + while ( entries.hasNext() ) { + final Object entry = entries.next(); + if ( entryChecker == null || entryChecker.include( entry, entryCount, collection, pluralAttribute ) ) { + historyBindings.bindInsertValues( + collection, + id, + entry, + entryCount, + session, + mutationExecutor.getJdbcValueBindings() + ); + mutationExecutor.execute( entry, null, null, null, session ); + } + entryCount++; + } + } + finally { + mutationExecutor.release(); + } + } + + private MutationOperationGroup createHistoryOperationGroup() { + final var operation = rowMutationOperations.getInsertRowOperation( getHistoryTableMapping() ); + return operation == null ? null : singleOperation( MutationType.INSERT, mutationTarget, operation ); + } + + private CollectionTableMapping getHistoryTableMapping() { + if ( historyTableMapping == null ) { + final var temporalMapping = mutationTarget.getTargetPart().getTemporalMapping(); + historyTableMapping = + new CollectionTableMapping( mutationTarget.getCollectionTableMapping(), + temporalMapping.getTableName() ); + } + return historyTableMapping; + } + + private HistoryCollectionRowMutationHelper getRowMutationHelper() { + if ( rowMutationHelper == null ) { + rowMutationHelper = new HistoryCollectionRowMutationHelper( + mutationTarget, + getHistoryTableMapping().getTableName(), + indexColumnIsSettable, + elementColumnIsSettable, + indexIncrementer + ); + } + return rowMutationHelper; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/persister/collection/mutation/RemoveCoordinatorHistory.java b/hibernate-core/src/main/java/org/hibernate/persister/collection/mutation/RemoveCoordinatorHistory.java new file mode 100644 index 000000000000..79bd3d121687 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/persister/collection/mutation/RemoveCoordinatorHistory.java @@ -0,0 +1,151 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.persister.collection.mutation; + +import java.util.function.UnaryOperator; + +import org.hibernate.engine.jdbc.batch.internal.BasicBatchKey; +import org.hibernate.engine.jdbc.mutation.spi.MutationExecutorService; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.service.ServiceRegistry; +import org.hibernate.sql.model.MutationOperationGroup; +import org.hibernate.sql.model.MutationType; +import org.hibernate.sql.model.ast.MutatingTableReference; +import org.hibernate.sql.model.jdbc.JdbcMutationOperation; + +import static org.hibernate.sql.model.ModelMutationLogging.MODEL_MUTATION_LOGGER; +import static org.hibernate.sql.model.internal.MutationOperationGroupFactory.singleOperation; + +/** + * {@link RemoveCoordinator} implementation for temporal collection tables + * in the {@link org.hibernate.temporal.TemporalTableStrategy#HISTORY_TABLE} + * temporal table mapping strategy. + * + * @author Gavin King + */ +public class RemoveCoordinatorHistory implements RemoveCoordinator { + private final CollectionMutationTarget mutationTarget; + private final OperationProducer operationProducer; + private final MutationExecutorService mutationExecutorService; + private final BasicBatchKey batchKey; + private final BasicBatchKey historyBatchKey; + private final boolean[] indexColumnIsSettable; + private final boolean[] elementColumnIsSettable; + private final UnaryOperator indexIncrementer; + + private MutationOperationGroup operationGroup; + private MutationOperationGroup historyOperationGroup; + private CollectionTableMapping historyTableMapping; + private HistoryCollectionRowMutationHelper rowMutationHelper; + + public RemoveCoordinatorHistory( + CollectionMutationTarget mutationTarget, + RowMutationOperations mutationOperations, + boolean[] indexColumnIsSettable, + boolean[] elementColumnIsSettable, + UnaryOperator indexIncrementer, + ServiceRegistry serviceRegistry) { + this.mutationTarget = mutationTarget; + this.operationProducer = mutationOperations.getDeleteAllRowsOperationProducer(); + this.indexColumnIsSettable = indexColumnIsSettable; + this.elementColumnIsSettable = elementColumnIsSettable; + this.indexIncrementer = indexIncrementer; + this.batchKey = new BasicBatchKey( mutationTarget.getRolePath() + "#REMOVE" ); + this.historyBatchKey = new BasicBatchKey( mutationTarget.getRolePath() + "#HISTORY_REMOVE" ); + this.mutationExecutorService = serviceRegistry.getService( MutationExecutorService.class ); + } + + @Override + public CollectionMutationTarget getMutationTarget() { + return mutationTarget; + } + + @Override + public String getSqlString() { + if ( operationGroup == null ) { + operationGroup = buildOperationGroup( mutationTarget.getCollectionTableMapping() ); + } + final var operation = (JdbcMutationOperation) operationGroup.getSingleOperation(); + return operation.getSqlString(); + } + + @Override + public void deleteAllRows(Object key, SharedSessionContractImplementor session) { + if ( MODEL_MUTATION_LOGGER.isTraceEnabled() ) { + MODEL_MUTATION_LOGGER.removingCollection( mutationTarget.getRolePath(), key ); + } + + if ( operationGroup == null ) { + operationGroup = buildOperationGroup( mutationTarget.getCollectionTableMapping() ); + } + if ( historyOperationGroup == null ) { + historyOperationGroup = buildOperationGroup( getHistoryTableMapping() ); + } + + final var mutationExecutor = mutationExecutorService.createExecutor( + () -> batchKey, + operationGroup, + session + ); + final var historyExecutor = mutationExecutorService.createExecutor( + () -> historyBatchKey, + historyOperationGroup, + session + ); + + try { + final var jdbcValueBindings = mutationExecutor.getJdbcValueBindings(); + final var foreignKeyDescriptor = mutationTarget.getTargetPart().getKeyDescriptor(); + foreignKeyDescriptor.getKeyPart().decompose( + key, + 0, + jdbcValueBindings, + null, + RowMutationOperations.DEFAULT_RESTRICTOR, + session + ); + mutationExecutor.execute( key, null, null, null, session ); + + getRowMutationHelper().bindDeleteAllRestrictions( + key, + session, + historyExecutor.getJdbcValueBindings() + ); + historyExecutor.execute( key, null, null, null, session ); + } + finally { + mutationExecutor.release(); + historyExecutor.release(); + } + } + + private MutationOperationGroup buildOperationGroup(CollectionTableMapping tableMapping) { + final var tableReference = new MutatingTableReference( tableMapping ); + return singleOperation( MutationType.DELETE, mutationTarget, operationProducer.createOperation( tableReference ) ); + } + + private CollectionTableMapping getHistoryTableMapping() { + if ( historyTableMapping == null ) { + final var temporalMapping = mutationTarget.getTargetPart().getTemporalMapping(); + historyTableMapping = + new CollectionTableMapping( mutationTarget.getCollectionTableMapping(), + temporalMapping.getTableName() ); + } + return historyTableMapping; + } + + private HistoryCollectionRowMutationHelper getRowMutationHelper() { + if ( rowMutationHelper == null ) { + rowMutationHelper = new HistoryCollectionRowMutationHelper( + mutationTarget, + getHistoryTableMapping().getTableName(), + indexColumnIsSettable, + elementColumnIsSettable, + indexIncrementer + ); + } + return rowMutationHelper; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/persister/collection/mutation/RemoveCoordinatorStandard.java b/hibernate-core/src/main/java/org/hibernate/persister/collection/mutation/RemoveCoordinatorStandard.java index ca83d28999ed..a771cc46a527 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/collection/mutation/RemoveCoordinatorStandard.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/collection/mutation/RemoveCoordinatorStandard.java @@ -5,8 +5,10 @@ package org.hibernate.persister.collection.mutation; import org.hibernate.engine.jdbc.batch.internal.BasicBatchKey; +import org.hibernate.engine.jdbc.mutation.ParameterUsage; import org.hibernate.engine.jdbc.mutation.spi.MutationExecutorService; import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.persister.entity.mutation.TemporalMutationHelper; import org.hibernate.service.ServiceRegistry; import org.hibernate.sql.model.MutationOperationGroup; import org.hibernate.sql.model.MutationType; @@ -37,10 +39,10 @@ public class RemoveCoordinatorStandard implements RemoveCoordinator { */ public RemoveCoordinatorStandard( CollectionMutationTarget mutationTarget, - OperationProducer operationProducer, + RowMutationOperations mutationOperations, ServiceRegistry serviceRegistry) { this.mutationTarget = mutationTarget; - this.operationProducer = operationProducer; + this.operationProducer = mutationOperations.getDeleteAllRowsOperationProducer(); batchKey = new BasicBatchKey( mutationTarget.getRolePath() + "#REMOVE" ); mutationExecutorService = serviceRegistry.getService( MutationExecutorService.class ); @@ -86,8 +88,7 @@ public void deleteAllRows(Object key, SharedSessionContractImplementor session) try { final var jdbcValueBindings = mutationExecutor.getJdbcValueBindings(); - final var foreignKeyDescriptor = mutationTarget.getTargetPart().getKeyDescriptor(); - foreignKeyDescriptor.getKeyPart().decompose( + mutationTarget.getTargetPart().getKeyDescriptor().getKeyPart().decompose( key, 0, jdbcValueBindings, @@ -95,6 +96,14 @@ public void deleteAllRows(Object key, SharedSessionContractImplementor session) RowMutationOperations.DEFAULT_RESTRICTOR, session ); + final var temporalMapping = mutationTarget.getTargetPart().getTemporalMapping(); + if ( temporalMapping != null && TemporalMutationHelper.isUsingParameters( session ) ) { + jdbcValueBindings.bindValue( + session.getCurrentTransactionIdentifier(), + temporalMapping.getEndingColumnMapping(), + ParameterUsage.SET + ); + } mutationExecutor.execute( key, diff --git a/hibernate-core/src/main/java/org/hibernate/persister/collection/mutation/RemoveCoordinatorTablePerSubclass.java b/hibernate-core/src/main/java/org/hibernate/persister/collection/mutation/RemoveCoordinatorTablePerSubclass.java index 01b63ad1c67c..4da36998b872 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/collection/mutation/RemoveCoordinatorTablePerSubclass.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/collection/mutation/RemoveCoordinatorTablePerSubclass.java @@ -35,10 +35,10 @@ public class RemoveCoordinatorTablePerSubclass implements RemoveCoordinator { */ public RemoveCoordinatorTablePerSubclass( OneToManyPersister mutationTarget, - OperationProducer operationProducer, + RowMutationOperations mutationOperations, ServiceRegistry serviceRegistry) { this.mutationTarget = mutationTarget; - this.operationProducer = operationProducer; + this.operationProducer = mutationOperations.getDeleteAllRowsOperationProducer(); mutationExecutorService = serviceRegistry.getService( MutationExecutorService.class ); } diff --git a/hibernate-core/src/main/java/org/hibernate/persister/collection/mutation/RowMutationOperations.java b/hibernate-core/src/main/java/org/hibernate/persister/collection/mutation/RowMutationOperations.java index b932b32cb317..61745447608e 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/collection/mutation/RowMutationOperations.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/collection/mutation/RowMutationOperations.java @@ -43,6 +43,8 @@ public class RowMutationOperations { private final OperationProducer deleteRowOperationProducer; private final Restrictions deleteRowRestrictions; + private final OperationProducer deleteAllRowsOperationProducer; + private JdbcMutationOperation insertRowOperation; private JdbcMutationOperation updateRowOperation; private JdbcMutationOperation deleteRowOperation; @@ -55,7 +57,8 @@ public RowMutationOperations( Values updateRowValues, Restrictions updateRowRestrictions, OperationProducer deleteRowOperationProducer, - Restrictions deleteRowRestrictions) { + Restrictions deleteRowRestrictions, + OperationProducer deleteAllRowsOperationProducer) { this.target = target; assert areSameNullness( insertRowOperationProducer, insertRowValues ); @@ -71,6 +74,8 @@ public RowMutationOperations( this.deleteRowOperationProducer = deleteRowOperationProducer; this.deleteRowRestrictions = deleteRowRestrictions; + + this.deleteAllRowsOperationProducer = deleteAllRowsOperationProducer; } @Override @@ -180,6 +185,9 @@ public JdbcMutationOperation getDeleteRowOperation(TableMapping tableMapping) { } } + public OperationProducer getDeleteAllRowsOperationProducer() { + return deleteAllRowsOperationProducer; + } @FunctionalInterface public interface Restrictions { diff --git a/hibernate-core/src/main/java/org/hibernate/persister/collection/mutation/UpdateRowsCoordinatorAudit.java b/hibernate-core/src/main/java/org/hibernate/persister/collection/mutation/UpdateRowsCoordinatorAudit.java new file mode 100644 index 000000000000..288f96b73fe6 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/persister/collection/mutation/UpdateRowsCoordinatorAudit.java @@ -0,0 +1,151 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.persister.collection.mutation; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.UnaryOperator; + +import org.hibernate.collection.spi.PersistentCollection; +import org.hibernate.engine.jdbc.batch.internal.BasicBatchKey; +import org.hibernate.engine.jdbc.mutation.spi.MutationExecutorService; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.metamodel.mapping.PluralAttributeMapping; +import org.hibernate.persister.collection.CollectionPersister; +import org.hibernate.persister.state.internal.AuditStateManagement; +import org.hibernate.sql.model.MutationOperationGroup; + +/** + * UpdateRowsCoordinator for audited collections. + */ +public class UpdateRowsCoordinatorAudit implements UpdateRowsCoordinator { + private final CollectionMutationTarget mutationTarget; + private final UpdateRowsCoordinator currentUpdateCoordinator; + private final SessionFactoryImplementor sessionFactory; + private final MutationExecutorService mutationExecutorService; + private final BasicBatchKey auditBatchKey; + private final boolean[] indexColumnIsSettable; + private final boolean[] elementColumnIsSettable; + private final UnaryOperator indexIncrementer; + + private MutationOperationGroup auditOperationGroup; + private AuditCollectionHelper auditHelper; + + public UpdateRowsCoordinatorAudit( + CollectionMutationTarget mutationTarget, + UpdateRowsCoordinator currentUpdateCoordinator, + boolean[] indexColumnIsSettable, + boolean[] elementColumnIsSettable, + UnaryOperator indexIncrementer, + SessionFactoryImplementor sessionFactory) { + this.mutationTarget = mutationTarget; + this.currentUpdateCoordinator = currentUpdateCoordinator; + this.sessionFactory = sessionFactory; + this.indexColumnIsSettable = indexColumnIsSettable; + this.elementColumnIsSettable = elementColumnIsSettable; + this.indexIncrementer = indexIncrementer; + this.auditBatchKey = new BasicBatchKey( mutationTarget.getRolePath() + "#AUDIT_INSERT" ); + this.mutationExecutorService = sessionFactory.getServiceRegistry().getService( MutationExecutorService.class ); + } + + @Override + public CollectionMutationTarget getMutationTarget() { + return mutationTarget; + } + + @Override + public void updateRows(Object key, PersistentCollection collection, SharedSessionContractImplementor session) { + final var attribute = mutationTarget.getTargetPart(); + final var collectionDescriptor = attribute.getCollectionDescriptor(); + final var rowsToAudit = collectUpdatedRows( collection, attribute, collectionDescriptor ); + + currentUpdateCoordinator.updateRows( key, collection, session ); + + if ( !rowsToAudit.isEmpty() ) { + if ( auditOperationGroup == null ) { + auditOperationGroup = getAuditHelper().getAuditInsertOperationGroup(); + } + if ( auditOperationGroup != null ) { + final var auditExecutor = mutationExecutorService.createExecutor( + () -> auditBatchKey, + auditOperationGroup, + session + ); + try { + final var bindings = getAuditHelper().getRowMutationHelper(); + for ( var row : rowsToAudit ) { + bindings.bindInsertValues( + collection, + key, + row.entry, + row.position, + AuditStateManagement.ModificationType.MOD, + session, + auditExecutor.getJdbcValueBindings() + ); + auditExecutor.execute( row.entry, null, null, null, session ); + } + } + finally { + auditExecutor.release(); + } + } + } + } + + private AuditCollectionHelper getAuditHelper() { + if ( auditHelper == null ) { + auditHelper = new AuditCollectionHelper( + mutationTarget, + sessionFactory, + indexColumnIsSettable, + elementColumnIsSettable, + indexIncrementer, + mutationTarget.getTargetPart().getAuditMapping() + ); + } + return auditHelper; + } + + private List collectUpdatedRows( + PersistentCollection collection, + PluralAttributeMapping attribute, + CollectionPersister persister) { + final List rows = new ArrayList<>(); + final var entries = collection.entries( persister ); + if ( !entries.hasNext() ) { + return rows; + } + + if ( collection.isElementRemoved() ) { + final List elements = new ArrayList<>(); + while ( entries.hasNext() ) { + elements.add( entries.next() ); + } + for ( int i = elements.size() - 1; i >= 0; i-- ) { + final Object entry = elements.get( i ); + if ( collection.needsUpdating( entry, i, attribute ) ) { + rows.add( new RowReference( entry, i ) ); + } + } + } + else { + int position = 0; + while ( entries.hasNext() ) { + final Object entry = entries.next(); + if ( collection.needsUpdating( entry, position, attribute ) ) { + rows.add( new RowReference( entry, position ) ); + } + position++; + } + } + + return rows; + } + + private record RowReference(Object entry, int position) { + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/persister/collection/mutation/UpdateRowsCoordinatorHistory.java b/hibernate-core/src/main/java/org/hibernate/persister/collection/mutation/UpdateRowsCoordinatorHistory.java new file mode 100644 index 000000000000..2a5597272a10 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/persister/collection/mutation/UpdateRowsCoordinatorHistory.java @@ -0,0 +1,261 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.persister.collection.mutation; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.UnaryOperator; + +import org.hibernate.collection.spi.PersistentCollection; +import org.hibernate.engine.jdbc.batch.internal.BasicBatchKey; +import org.hibernate.engine.jdbc.mutation.MutationExecutor; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.sql.model.MutationOperationGroup; +import org.hibernate.sql.model.MutationType; + +import static org.hibernate.sql.model.internal.MutationOperationGroupFactory.noOperations; +import static org.hibernate.sql.model.internal.MutationOperationGroupFactory.singleOperation; + +/** + * {@link UpdateRowsCoordinator} implementation for temporal collection tables + * in the {@link org.hibernate.temporal.TemporalTableStrategy#HISTORY_TABLE} + * temporal table mapping strategy. + * + * @author Gavin King + */ +public class UpdateRowsCoordinatorHistory extends AbstractUpdateRowsCoordinator implements UpdateRowsCoordinator { + private final RowMutationOperations rowMutationOperations; + private final BasicBatchKey historyDeleteBatchKey; + private final BasicBatchKey historyInsertBatchKey; + private final boolean[] indexColumnIsSettable; + private final boolean[] elementColumnIsSettable; + private final UnaryOperator indexIncrementer; + + private MutationOperationGroup updateOperationGroup; + private MutationOperationGroup historyDeleteOperationGroup; + private MutationOperationGroup historyInsertOperationGroup; + private CollectionTableMapping historyTableMapping; + private HistoryCollectionRowMutationHelper rowMutationHelper; + + public UpdateRowsCoordinatorHistory( + CollectionMutationTarget mutationTarget, + RowMutationOperations rowMutationOperations, + SessionFactoryImplementor sessionFactory, + boolean[] indexColumnIsSettable, + boolean[] elementColumnIsSettable, + UnaryOperator indexIncrementer) { + super( mutationTarget, sessionFactory ); + this.rowMutationOperations = rowMutationOperations; + this.indexColumnIsSettable = indexColumnIsSettable; + this.elementColumnIsSettable = elementColumnIsSettable; + this.indexIncrementer = indexIncrementer; + this.historyDeleteBatchKey = new BasicBatchKey( mutationTarget.getRolePath() + "#HISTORY_DELETE" ); + this.historyInsertBatchKey = new BasicBatchKey( mutationTarget.getRolePath() + "#HISTORY_INSERT" ); + } + + @Override + protected int doUpdate(Object key, PersistentCollection collection, SharedSessionContractImplementor session) { + if ( rowMutationOperations.getUpdateRowOperation() == null ) { + return 0; + } + + final var updateOperationGroup = getUpdateOperationGroup(); + final var historyDeleteGroup = getHistoryDeleteOperationGroup(); + final var historyInsertGroup = getHistoryInsertOperationGroup(); + + final var updateExecutor = mutationExecutorService.createExecutor( + () -> new BasicBatchKey( getMutationTarget().getRolePath() + "#UPDATE" ), + updateOperationGroup, + session + ); + final var historyDeleteExecutor = historyDeleteGroup == null + ? null + : mutationExecutorService.createExecutor( + () -> historyDeleteBatchKey, + historyDeleteGroup, + session + ); + final var historyInsertExecutor = historyInsertGroup == null + ? null + : mutationExecutorService.createExecutor( + () -> historyInsertBatchKey, + historyInsertGroup, + session + ); + + try { + final var entries = + collection.entries( getMutationTarget().getTargetPart().getCollectionDescriptor() ); + int count = 0; + + if ( collection.isElementRemoved() ) { + final List elements = new ArrayList<>(); + while ( entries.hasNext() ) { + elements.add( entries.next() ); + } + for ( int i = elements.size() - 1; i >= 0; i-- ) { + final Object entry = elements.get( i ); + final boolean updated = processRow( + key, + collection, + entry, + i, + updateExecutor, + historyDeleteExecutor, + historyInsertExecutor, + session + ); + if ( updated ) { + count++; + } + } + } + else { + int position = 0; + while ( entries.hasNext() ) { + final Object entry = entries.next(); + final boolean updated = processRow( + key, + collection, + entry, + position++, + updateExecutor, + historyDeleteExecutor, + historyInsertExecutor, + session + ); + if ( updated ) { + count++; + } + } + } + + return count; + } + finally { + updateExecutor.release(); + if ( historyDeleteExecutor != null ) { + historyDeleteExecutor.release(); + } + if ( historyInsertExecutor != null ) { + historyInsertExecutor.release(); + } + } + } + + private boolean processRow( + Object key, + PersistentCollection collection, + Object entry, + int entryPosition, + MutationExecutor updateExecutor, + MutationExecutor historyDeleteExecutor, + MutationExecutor historyInsertExecutor, + SharedSessionContractImplementor session) { + final var attribute = getMutationTarget().getTargetPart(); + if ( !collection.needsUpdating( entry, entryPosition, attribute ) ) { + return false; + } + + final Object deleteRowValue = resolveDeleteRowValue( collection, entry, entryPosition ); + rowMutationOperations.getUpdateRowValues().applyValues( + collection, + key, + entry, + entryPosition, + session, + updateExecutor.getJdbcValueBindings() + ); + rowMutationOperations.getUpdateRowRestrictions().applyRestrictions( + collection, + key, + entry, + entryPosition, + session, + updateExecutor.getJdbcValueBindings() + ); + updateExecutor.execute( collection, null, null, null, session ); + + if ( historyDeleteExecutor != null && historyInsertExecutor != null ) { + final var historyBindings = getRowMutationHelper(); + historyBindings.bindDeleteRowRestrictions( + collection, + key, + deleteRowValue, + entryPosition, + session, + historyDeleteExecutor.getJdbcValueBindings() + ); + historyDeleteExecutor.execute( deleteRowValue, null, null, null, session ); + + historyBindings.bindInsertValues( + collection, + key, + entry, + entryPosition, + session, + historyInsertExecutor.getJdbcValueBindings() + ); + historyInsertExecutor.execute( entry, null, null, null, session ); + } + + return true; + } + + private MutationOperationGroup getUpdateOperationGroup() { + if ( updateOperationGroup == null ) { + final var updateRowOperation = rowMutationOperations.getUpdateRowOperation(); + final var mutationTarget = getMutationTarget(); + updateOperationGroup = updateRowOperation == null + ? noOperations( MutationType.UPDATE, mutationTarget ) + : singleOperation( MutationType.UPDATE, mutationTarget, updateRowOperation ); + } + return updateOperationGroup; + } + + private MutationOperationGroup getHistoryDeleteOperationGroup() { + if ( historyDeleteOperationGroup == null ) { + final var operation = rowMutationOperations.getDeleteRowOperation( getHistoryTableMapping() ); + historyDeleteOperationGroup = operation == null + ? null + : singleOperation( MutationType.DELETE, getMutationTarget(), operation ); + } + return historyDeleteOperationGroup; + } + + private MutationOperationGroup getHistoryInsertOperationGroup() { + if ( historyInsertOperationGroup == null ) { + final var operation = rowMutationOperations.getInsertRowOperation( getHistoryTableMapping() ); + historyInsertOperationGroup = operation == null + ? null + : singleOperation( MutationType.INSERT, getMutationTarget(), operation ); + } + return historyInsertOperationGroup; + } + + private CollectionTableMapping getHistoryTableMapping() { + if ( historyTableMapping == null ) { + final var mutationTarget = getMutationTarget(); + historyTableMapping = + new CollectionTableMapping( mutationTarget.getCollectionTableMapping(), + mutationTarget.getTargetPart().getTemporalMapping().getTableName() ); + } + return historyTableMapping; + } + + private HistoryCollectionRowMutationHelper getRowMutationHelper() { + if ( rowMutationHelper == null ) { + rowMutationHelper = new HistoryCollectionRowMutationHelper( + getMutationTarget(), + getHistoryTableMapping().getTableName(), + indexColumnIsSettable, + elementColumnIsSettable, + indexIncrementer + ); + } + return rowMutationHelper; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/persister/collection/mutation/UpdateRowsCoordinatorTemporal.java b/hibernate-core/src/main/java/org/hibernate/persister/collection/mutation/UpdateRowsCoordinatorTemporal.java new file mode 100644 index 000000000000..49c0e1c883ad --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/persister/collection/mutation/UpdateRowsCoordinatorTemporal.java @@ -0,0 +1,150 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.persister.collection.mutation; + +import java.util.ArrayList; +import java.util.List; + +import org.hibernate.collection.spi.PersistentCollection; +import org.hibernate.engine.jdbc.batch.internal.BasicBatchKey; +import org.hibernate.engine.jdbc.mutation.MutationExecutor; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.sql.model.MutationOperationGroup; +import org.hibernate.sql.model.MutationType; + +import static org.hibernate.sql.model.internal.MutationOperationGroupFactory.singleOperation; + +/** + * {@link UpdateRowsCoordinator} implementation for temporal collection tables + * in the {@link org.hibernate.temporal.TemporalTableStrategy#SINGLE_TABLE} + * temporal table mapping strategy. + * + * @author Gavin King + */ +public class UpdateRowsCoordinatorTemporal extends AbstractUpdateRowsCoordinator implements UpdateRowsCoordinator { + + private final RowMutationOperations rowMutationOperations; + private final BasicBatchKey deleteBatchKey; + private final BasicBatchKey insertBatchKey; + + private MutationOperationGroup deleteOperationGroup; + private MutationOperationGroup insertOperationGroup; + + public UpdateRowsCoordinatorTemporal( + CollectionMutationTarget mutationTarget, + RowMutationOperations rowMutationOperations, + SessionFactoryImplementor sessionFactory) { + super( mutationTarget, sessionFactory ); + this.rowMutationOperations = rowMutationOperations; + this.deleteBatchKey = new BasicBatchKey( mutationTarget.getRolePath() + "#DELETE" ); + this.insertBatchKey = new BasicBatchKey( mutationTarget.getRolePath() + "#INSERT" ); + } + + @Override + protected int doUpdate(Object key, PersistentCollection collection, SharedSessionContractImplementor session) { + if ( rowMutationOperations.getDeleteRowOperation() == null + || rowMutationOperations.getInsertRowOperation() == null ) { + return 0; + } + + if ( deleteOperationGroup == null ) { + deleteOperationGroup = singleOperation( + MutationType.DELETE, + getMutationTarget(), + rowMutationOperations.getDeleteRowOperation() + ); + } + if ( insertOperationGroup == null ) { + insertOperationGroup = singleOperation( + MutationType.INSERT, + getMutationTarget(), + rowMutationOperations.getInsertRowOperation() + ); + } + + final MutationExecutor deleteExecutor = mutationExecutorService.createExecutor( + () -> deleteBatchKey, + deleteOperationGroup, + session + ); + final MutationExecutor insertExecutor = mutationExecutorService.createExecutor( + () -> insertBatchKey, + insertOperationGroup, + session + ); + + try { + final var attribute = getMutationTarget().getTargetPart(); + final var collectionDescriptor = attribute.getCollectionDescriptor(); + final var entries = collection.entries( collectionDescriptor ); + + int count = 0; + if ( collection.isElementRemoved() ) { + final List elements = new ArrayList<>(); + while ( entries.hasNext() ) { + elements.add( entries.next() ); + } + for ( int i = elements.size() - 1; i >= 0; i-- ) { + final Object entry = elements.get( i ); + if ( processRow( key, collection, entry, i, deleteExecutor, insertExecutor, session ) ) { + count++; + } + } + } + else { + int position = 0; + while ( entries.hasNext() ) { + final Object entry = entries.next(); + if ( processRow( key, collection, entry, position++, deleteExecutor, insertExecutor, session ) ) { + count++; + } + } + } + return count; + } + finally { + deleteExecutor.release(); + insertExecutor.release(); + } + } + + private boolean processRow( + Object key, + PersistentCollection collection, + Object entry, + int entryPosition, + MutationExecutor deleteExecutor, + MutationExecutor insertExecutor, + SharedSessionContractImplementor session) { + final var attribute = getMutationTarget().getTargetPart(); + if ( !collection.needsUpdating( entry, entryPosition, attribute ) ) { + return false; + } + + final Object deleteRowValue = resolveDeleteRowValue( collection, entry, entryPosition ); + rowMutationOperations.getDeleteRowRestrictions().applyRestrictions( + collection, + key, + deleteRowValue, + entryPosition, + session, + deleteExecutor.getJdbcValueBindings() + ); + deleteExecutor.execute( deleteRowValue, null, null, null, session ); + + rowMutationOperations.getInsertRowValues().applyValues( + collection, + key, + entry, + entryPosition, + session, + insertExecutor.getJdbcValueBindings() + ); + insertExecutor.execute( entry, null, null, null, session ); + + return true; + } +} 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 76d9652c8e94..a02fc10b248f 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 @@ -53,7 +53,6 @@ import org.hibernate.engine.spi.EntityEntryFactory; import org.hibernate.engine.spi.EntityKey; import org.hibernate.engine.spi.LoadQueryInfluencers; -import org.hibernate.engine.spi.PersistenceContext; import org.hibernate.engine.spi.PersistentAttributeInterceptable; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SessionImplementor; @@ -68,12 +67,15 @@ import org.hibernate.generator.values.GeneratedValues; import org.hibernate.generator.values.GeneratedValuesMutationDelegate; import org.hibernate.id.BulkInsertionCapableIdentifierGenerator; +import org.hibernate.id.CompositeNestedGeneratedValueGenerator; import org.hibernate.id.IdentifierGenerator; import org.hibernate.id.OptimizableGenerator; +import org.hibernate.metamodel.mapping.AuditMapping; +import org.hibernate.metamodel.mapping.AuxiliaryMapping; +import org.hibernate.metamodel.mapping.TemporalMapping; import org.hibernate.persister.filter.internal.FilterHelper; import org.hibernate.internal.util.ImmutableBitSet; import org.hibernate.internal.util.IndexedConsumer; -import org.hibernate.internal.util.MarkerObject; import org.hibernate.internal.util.collections.LockModeEnumMap; import org.hibernate.jdbc.Expectation; import org.hibernate.loader.ast.internal.EntityConcreteTypeLoader; @@ -106,6 +108,7 @@ import org.hibernate.metamodel.mapping.AttributeMapping; import org.hibernate.metamodel.mapping.AttributeMappingsList; import org.hibernate.metamodel.mapping.AttributeMappingsMap; +import org.hibernate.metamodel.mapping.DiscriminatorValue; import org.hibernate.metamodel.mapping.DiscriminatorType; import org.hibernate.metamodel.mapping.EmbeddableValuedModelPart; import org.hibernate.metamodel.mapping.EntityDiscriminatorMapping; @@ -150,17 +153,12 @@ import org.hibernate.models.internal.util.CollectionHelper; import org.hibernate.persister.collection.CollectionPersister; import org.hibernate.persister.entity.mutation.DeleteCoordinator; -import org.hibernate.persister.entity.mutation.DeleteCoordinatorSoft; -import org.hibernate.persister.entity.mutation.DeleteCoordinatorStandard; import org.hibernate.persister.entity.mutation.EntityMutationTarget; import org.hibernate.persister.entity.mutation.EntityTableMapping; import org.hibernate.persister.entity.mutation.InsertCoordinator; -import org.hibernate.persister.entity.mutation.InsertCoordinatorStandard; -import org.hibernate.persister.entity.mutation.MergeCoordinator; import org.hibernate.persister.entity.mutation.UpdateCoordinator; -import org.hibernate.persister.entity.mutation.UpdateCoordinatorNoOp; -import org.hibernate.persister.entity.mutation.UpdateCoordinatorStandard; import org.hibernate.persister.internal.SqlFragmentPredicate; +import org.hibernate.persister.state.spi.StateManagement; import org.hibernate.property.access.spi.Getter; import org.hibernate.property.access.spi.PropertyAccess; import org.hibernate.property.access.spi.Setter; @@ -189,6 +187,7 @@ import org.hibernate.sql.ast.tree.expression.ColumnReference; import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.QueryLiteral; +import org.hibernate.sql.ast.tree.from.AuxiliaryTableReference; import org.hibernate.sql.ast.tree.from.NamedTableReference; import org.hibernate.sql.ast.tree.from.StandardTableGroup; import org.hibernate.sql.ast.tree.from.TableGroup; @@ -204,7 +203,6 @@ import org.hibernate.sql.exec.spi.JdbcOperation; import org.hibernate.sql.exec.spi.JdbcParametersList; import org.hibernate.sql.model.ast.builder.MutationGroupBuilder; -import org.hibernate.sql.model.ast.builder.TableInsertBuilder; import org.hibernate.sql.results.graph.DomainResult; import org.hibernate.sql.results.graph.DomainResultCreationState; import org.hibernate.sql.results.graph.FetchParent; @@ -222,6 +220,7 @@ import org.hibernate.type.CompositeType; import org.hibernate.type.EntityType; import org.hibernate.type.ManyToOneType; +import org.hibernate.type.MappingContext; import org.hibernate.type.Type; import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.spi.TypeConfiguration; @@ -244,13 +243,14 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.function.BiConsumer; import java.util.function.Consumer; +import java.util.function.Function; import java.util.function.Supplier; import static java.util.Collections.emptyList; import static java.util.Collections.emptyMap; import static java.util.Collections.emptySet; import static java.util.Collections.unmodifiableList; -import static org.hibernate.boot.model.internal.SoftDeleteHelper.resolveSoftDeleteMapping; +import static java.util.function.Function.identity; import static org.hibernate.engine.internal.CacheHelper.fromSharedCache; import static org.hibernate.engine.internal.ManagedTypeHelper.asPersistentAttributeInterceptable; import static org.hibernate.engine.internal.ManagedTypeHelper.isPersistentAttributeInterceptable; @@ -273,7 +273,6 @@ import static org.hibernate.internal.util.collections.ArrayHelper.isAllTrue; import static org.hibernate.internal.util.collections.ArrayHelper.to2DStringArray; import static org.hibernate.internal.util.collections.ArrayHelper.toIntArray; -import static org.hibernate.internal.util.collections.ArrayHelper.toObjectArray; import static org.hibernate.internal.util.collections.ArrayHelper.toStringArray; import static org.hibernate.internal.util.collections.ArrayHelper.toTypeArray; import static org.hibernate.internal.util.collections.CollectionHelper.combine; @@ -289,8 +288,6 @@ import static org.hibernate.metamodel.mapping.internal.MappingModelCreationHelper.buildNonEncapsulatedCompositeIdentifierMapping; import static org.hibernate.metamodel.mapping.internal.MappingModelCreationHelper.resolveAggregateColumnBasicType; import static org.hibernate.metamodel.mapping.internal.MappingModelHelper.isCompatibleModelPart; -import static org.hibernate.persister.entity.DiscriminatorHelper.NOT_NULL_DISCRIMINATOR; -import static org.hibernate.persister.entity.DiscriminatorHelper.NULL_DISCRIMINATOR; import static org.hibernate.pretty.MessageHelper.infoString; import static org.hibernate.sql.ast.spi.SqlExpressionResolver.createColumnReferenceKey; import static org.hibernate.sql.model.ModelMutationLogging.MODEL_MUTATION_LOGGER; @@ -357,6 +354,9 @@ public abstract class AbstractEntityPersister private final boolean[][] propertyColumnUpdateable; private final boolean[][] propertyColumnInsertable; private final Set sharedColumnNames; + private final boolean[] propertyTemporalExcluded; + private final boolean[] propertyAuditedExcluded; + private final boolean hasTemporalExcludedProperties; //information about lazy properties of this class private final String[] lazyPropertyNames; @@ -373,6 +373,8 @@ public abstract class AbstractEntityPersister private final String[][] subclassPropertyColumnReaderTemplateClosure; private final FetchMode[] subclassPropertyFetchModeClosure; + private final StateManagement stateManagement; + private Map lazyLoadPlanByFetchGroup; private final LockModeEnumMap lockers = new LockModeEnumMap<>(); private String sqlVersionSelectString; @@ -423,10 +425,11 @@ public abstract class AbstractEntityPersister private EntityVersionMapping versionMapping; private EntityRowIdMapping rowIdMapping; private EntityDiscriminatorMapping discriminatorMapping; - private SoftDeleteMapping softDeleteMapping; + private AuxiliaryMapping auxiliaryMapping; private AttributeMappingsList attributeMappings; protected AttributeMappingsMap declaredAttributeMappings = AttributeMappingsMap.builder().build(); + protected AttributeMappingsMap declaredGenericAttributeMappings = AttributeMappingsMap.builder().build(); protected AttributeMappingsList staticFetchableList; // We build a cache for getters and setters to avoid megamorphic calls private Getter[] getterCache; @@ -439,7 +442,7 @@ public abstract class AbstractEntityPersister protected ReflectionOptimizer.AccessOptimizer accessOptimizer; protected final String[] fullDiscriminatorSQLValues; - private final Object[] fullDiscriminatorValues; + private final DiscriminatorValue[] fullDiscriminatorValues; /** * Warning: @@ -459,6 +462,22 @@ public AbstractEntityPersister( final EntityDataAccess cacheAccessStrategy, final NaturalIdDataAccess naturalIdRegionAccessStrategy, final RuntimeModelCreationContext creationContext) + throws HibernateException { + this( + persistentClass, + cacheAccessStrategy, + naturalIdRegionAccessStrategy, + creationContext, + identity() + ); + } + + protected AbstractEntityPersister( + final PersistentClass persistentClass, + final EntityDataAccess cacheAccessStrategy, + final NaturalIdDataAccess naturalIdRegionAccessStrategy, + final RuntimeModelCreationContext creationContext, + final Function statementManagerConverter) throws HibernateException { super( persistentClass, creationContext ); jpaEntityName = persistentClass.getJpaEntityName(); @@ -583,6 +602,8 @@ public AbstractEntityPersister( propertyColumnFormulaTemplates = new String[hydrateSpan][]; propertyColumnUpdateable = new boolean[hydrateSpan][]; propertyColumnInsertable = new boolean[hydrateSpan][]; + propertyTemporalExcluded = new boolean[hydrateSpan]; + propertyAuditedExcluded = new boolean[hydrateSpan]; sharedColumnNames = new HashSet<>(); nonLazyPropertyNames = new HashSet<>(); @@ -590,6 +611,8 @@ public AbstractEntityPersister( final ArrayList lazyNames = new ArrayList<>(); final ArrayList lazyNumbers = new ArrayList<>(); final ArrayList lazyTypes = new ArrayList<>(); + boolean foundTemporalExcluded = false; + boolean foundNonExcludedCollection = false; final var propertyClosure = persistentClass.getPropertyClosure(); boolean foundFormula = false; @@ -598,6 +621,14 @@ public AbstractEntityPersister( thisClassProperties.add( property ); final var propertyValue = property.getValue(); + final boolean temporalExcluded = property.isTemporalExcluded(); + propertyTemporalExcluded[i] = temporalExcluded; + foundTemporalExcluded = foundTemporalExcluded || temporalExcluded; + propertyAuditedExcluded[i] = property.isAuditedExcluded(); + foundNonExcludedCollection = foundNonExcludedCollection + || propertyValue instanceof org.hibernate.mapping.Collection + && !temporalExcluded; + final int span = property.getColumnSpan(); final String[] colNames = new String[span]; final String[] colAliases = new String[span]; @@ -644,6 +675,7 @@ else if ( selectable instanceof Column column ) { propertyColumnUpdateable[i] = propertyValue.getColumnUpdateability(); propertyColumnInsertable[i] = propertyValue.getColumnInsertability(); } + hasTemporalExcludedProperties = foundTemporalExcluded; hasFormulaProperties = foundFormula; lazyPropertyNames = toStringArray( lazyNames ); lazyPropertyNumbers = toIntArray( lazyNumbers ); @@ -740,7 +772,7 @@ else if ( selectable instanceof Column column ) { && canWriteToCache && shouldInvalidateCache( persistentClass, creationContext ); - final List values = new ArrayList<>(); + final List values = new ArrayList<>(); final List sqlValues = new ArrayList<>(); if ( persistentClass.isPolymorphic() && persistentClass.getDiscriminator() != null ) { @@ -761,11 +793,14 @@ else if ( selectable instanceof Column column ) { } fullDiscriminatorSQLValues = toStringArray( sqlValues ); - fullDiscriminatorValues = toObjectArray( values ); + fullDiscriminatorValues = values.toArray( DiscriminatorValue[]::new ); if ( hasNamedQueryLoader() ) { getNamedQueryMemento( creationContext.getBootModel() ); } + + // Hibernate Reactive needs to convert the stateManagement so that it can create reactive coordinators + stateManagement = statementManagerConverter.apply( persistentClass.getRootClass().getStateManagement() ); } private static String renderSqlWhereStringTemplate( @@ -871,17 +906,59 @@ && supportsSqlArrayType( getDialect() ) } private String getIdentitySelectString(Dialect dialect) { - try { - final var identifierType = (BasicType) getIdentifierType(); - final int idTypeCode = identifierType.getJdbcType().getDdlTypeCode(); - return dialect.getIdentityColumnSupport() - .getIdentitySelectString( getTableName(0), getKeyColumns(0)[0], idTypeCode ); + final BasicType identifierType; + if ( getIdentifierType() instanceof BasicType type ) { + identifierType = type; + } + else { + final ComponentType componentType = (ComponentType) getIdentifierType(); + final CompositeNestedGeneratedValueGenerator compositeGenerator = (CompositeNestedGeneratedValueGenerator) getGenerator(); + int position = 0; + for ( boolean generatedOnExecution : compositeGenerator.getGeneratedOnExecutionColumnInclusions() ) { + if ( generatedOnExecution ) { + break; + } + position++; + } + identifierType = getUnderlyingType( factory.getRuntimeMetamodels(), componentType, position ); } - catch (MappingException ex) { + if ( identifierType != null ) { + try { + return dialect.getIdentityColumnSupport() + .getIdentitySelectString( getTableName( 0 ), getKeyColumns( 0 )[0], + identifierType.getJdbcType().getDdlTypeCode() ); + } + catch (MappingException ex) { + // no proper IdentityColumnSupport in the dialect + return null; + } + } + else { return null; } } + private static BasicType getUnderlyingType(MappingContext mappingContext, Type type, int typeIndex) { + if ( type instanceof ComponentType componentType ) { + int cols = 0; + for ( var subtype : componentType.getSubtypes() ) { + final int columnSpan = subtype.getColumnSpan( mappingContext ); + if ( cols+columnSpan > typeIndex ) { + return getUnderlyingType( mappingContext, subtype, typeIndex-cols ); + } + cols += columnSpan; + } + throw new IndexOutOfBoundsException(); + } + else if ( type instanceof EntityType entityType ) { + final var idType = entityType.getIdentifierOrUniqueKeyType( mappingContext ); + return getUnderlyingType( mappingContext, idType, typeIndex ); + } + else { + return (BasicType) type; + } + } + static boolean isAbstract(PersistentClass subclass) { final Boolean knownAbstract = subclass.isAbstract(); return knownAbstract == null @@ -1249,6 +1326,7 @@ private SingleIdArrayLoadPlan createLazyLoanPlan(List partsToSelect) new LoadQueryInfluencers( factory ), lockOptions, jdbcParametersBuilder::add, + new SqlAliasBaseManager(), factory ); return new SingleIdArrayLoadPlan( @@ -1282,14 +1360,13 @@ public DomainResult createDomainResult( TableGroup tableGroup, String resultVariable, DomainResultCreationState creationState) { - final var entityResult = new EntityResultImpl( + final var entityResult = new EntityResultImpl( navigablePath, this, tableGroup, resultVariable ); entityResult.afterInitialize( entityResult, creationState ); - //noinspection unchecked return entityResult; } @@ -1327,8 +1404,22 @@ public NaturalIdMapping getNaturalIdMapping() { @Override public TableReference createPrimaryTableReference( SqlAliasBase sqlAliasBase, - SqlAstCreationState sqlAstCreationState) { - return new NamedTableReference( getTableName(), sqlAliasBase.generateNewAlias() ); + SqlAstCreationState creationState) { + final var loadQueryInfluencers = creationState.getLoadQueryInfluencers(); + final boolean useAuxiliaryTable = + auxiliaryMapping != null + && auxiliaryMapping.useAuxiliaryTable( loadQueryInfluencers ); + final String primaryTableName = + useAuxiliaryTable + ? auxiliaryMapping.getTableName() + : getTableName(); + final String primaryAlias = sqlAliasBase.generateNewAlias(); + final var tableReference = + useAuxiliaryTable + ? new AuxiliaryTableReference( primaryTableName, getTableName(), primaryAlias ) + : new NamedTableReference( primaryTableName, primaryAlias ); + tableReference.applyAuxiliaryTable( auxiliaryMapping, loadQueryInfluencers ); + return tableReference; } @Override @@ -1365,6 +1456,8 @@ protected TableReferenceJoin generateTableReferenceJoin( sqlAliasBase.generateNewAlias(), !innerJoin ); + joinedTableReference.applyAuxiliaryTable( auxiliaryMapping, + creationState.getLoadQueryInfluencers() ); return new TableReferenceJoin( innerJoin, joinedTableReference, @@ -1544,7 +1637,8 @@ private static PersistentCollection getCollection( if ( collection == null ) { final var newCollection = collectionType.instantiate( session, persister, key ); newCollection.setOwner( entity ); - persistenceContext.addUninitializedCollection( persister, newCollection, key ); + persistenceContext.addUninitializedCollection( persister, newCollection, key, + entry != null && entry.isReadOnly() ); return newCollection; } else { @@ -2795,7 +2889,7 @@ protected void logStaticSQL() { } } - public abstract Map getSubclassByDiscriminatorValue(); + public abstract Map getSubclassByDiscriminatorValue(); public abstract String[] getConstraintOrderedTableNameClosure(); @@ -2811,18 +2905,35 @@ public TableGroup createRootTableGroup( NavigablePath navigablePath, String explicitSourceAlias, SqlAliasBase explicitSqlAliasBase, - Supplier> additionalPredicateCollectorAccess, + Supplier> additionalPredicateCollector, SqlAstCreationState creationState) { + final var loadQueryInfluencers = creationState.getLoadQueryInfluencers(); + final var sqlAliasBase = SqlAliasBase.from( explicitSqlAliasBase, explicitSourceAlias, this, creationState.getSqlAliasBaseGenerator() ); - final var rootTableReference = new NamedTableReference( - needsDiscriminator() ? getRootTableName() : getTableName(), - sqlAliasBase.generateNewAlias() - ); + final boolean useAuxiliaryTable = + auxiliaryMapping != null + && auxiliaryMapping.useAuxiliaryTable( loadQueryInfluencers ); + final String rootTableName = + useAuxiliaryTable + ? auxiliaryMapping.getTableName() + : needsDiscriminator() ? getRootTableName() : getTableName(); + final String rootAlias = sqlAliasBase.generateNewAlias(); + final var rootTableReference = + useAuxiliaryTable + ? new AuxiliaryTableReference( + rootTableName, + needsDiscriminator() + ? getRootTableName() + : getTableName(), + rootAlias + ) + : new NamedTableReference( rootTableName, rootAlias ); + rootTableReference.applyAuxiliaryTable( auxiliaryMapping, loadQueryInfluencers ); final var tableGroup = new StandardTableGroup( canUseInnerJoins, @@ -2842,10 +2953,11 @@ public TableGroup createRootTableGroup( sqlAliasBase.generateNewAlias(), isNullableSubclassTable( i ) ); + joinedTableReference.applyAuxiliaryTable( auxiliaryMapping, loadQueryInfluencers ); return new TableReferenceJoin( shouldInnerJoinSubclassTable( i, emptySet() ), joinedTableReference, - additionalPredicateCollectorAccess == null + additionalPredicateCollector == null ? null : generateJoinPredicate( rootTableReference, @@ -2864,24 +2976,21 @@ public TableGroup createRootTableGroup( getFactory() ); - if ( additionalPredicateCollectorAccess != null ) { + if ( additionalPredicateCollector != null ) { if ( needsDiscriminator() ) { final String alias = tableGroup.getPrimaryTableReference().getIdentificationVariable(); final var discriminatorPredicate = createDiscriminatorPredicate( alias, tableGroup, creationState ); - additionalPredicateCollectorAccess.get().accept( discriminatorPredicate ); + additionalPredicateCollector.get().accept( discriminatorPredicate ); } - if ( softDeleteMapping != null ) { - final var tableReference = - tableGroup.resolveTableReference( getSoftDeleteTableDetails().getTableName() ); - final var softDeletePredicate = - softDeleteMapping.createNonDeletedRestriction( tableReference, - creationState.getSqlExpressionResolver() ); - additionalPredicateCollectorAccess.get().accept( softDeletePredicate ); - if ( tableReference != rootTableReference && creationState.supportsEntityNameUsage() ) { - // Register entity name usage for the hierarchy root table to avoid pruning - creationState.registerEntityNameUsage( tableGroup, EntityNameUse.EXPRESSION, getRootEntityName() ); - } + if ( auxiliaryMapping != null ) { + auxiliaryMapping.applyPredicate( + additionalPredicateCollector, + creationState, + tableGroup, + rootTableReference, + this + ); } } @@ -2958,27 +3067,33 @@ private Predicate createDisciminatorPredicate(BasicType discriminatorType, Ex return createInListPredicate( discriminatorType, sqlExpression ); } else { - final Object value = getDiscriminatorValue(); - if ( value == NULL_DISCRIMINATOR ) { + final DiscriminatorValue value = getDiscriminatorValue(); + if ( value == DiscriminatorValue.Special.NULL ) { return new NullnessPredicate( sqlExpression ); } - else if ( value == NOT_NULL_DISCRIMINATOR ) { + else if ( value == DiscriminatorValue.Special.NOT_NULL ) { return new NullnessPredicate( sqlExpression, true ); } + else if ( value instanceof DiscriminatorValue.Literal literal ) { + return new ComparisonPredicate( + sqlExpression, + ComparisonOperator.EQUAL, + new QueryLiteral<>( literal.value(), discriminatorType ) + ); + } else { - return new ComparisonPredicate( sqlExpression, ComparisonOperator.EQUAL, - new QueryLiteral<>( value, discriminatorType ) ); + throw new IllegalStateException( "Unexpected discriminator value: " + value ); } } } private Predicate createInListPredicate(BasicType discriminatorType, Expression sqlExpression) { boolean hasNull = false, hasNonNull = false; - for ( Object discriminatorValue : fullDiscriminatorValues ) { - if ( discriminatorValue == NULL_DISCRIMINATOR ) { + for ( DiscriminatorValue discriminatorValue : fullDiscriminatorValues ) { + if ( discriminatorValue == DiscriminatorValue.Special.NULL ) { hasNull = true; } - else if ( discriminatorValue == NOT_NULL_DISCRIMINATOR ) { + else if ( discriminatorValue == DiscriminatorValue.Special.NOT_NULL ) { hasNonNull = true; } } @@ -3005,12 +3120,12 @@ else if ( hasNull ) { private InListPredicate discriminatorValuesPredicate(BasicType discriminatorType, Expression sqlExpression) { final List values = new ArrayList<>( fullDiscriminatorValues.length ); - for ( Object discriminatorValue : fullDiscriminatorValues ) { - if ( !(discriminatorValue instanceof MarkerObject) ) { - values.add( new QueryLiteral<>( discriminatorValue, discriminatorType) ); + for ( DiscriminatorValue discriminatorValue : fullDiscriminatorValues ) { + if ( discriminatorValue instanceof DiscriminatorValue.Literal literal ) { + values.add( new QueryLiteral<>( literal.value(), discriminatorType ) ); } } - return new InListPredicate( sqlExpression, values); + return new InListPredicate( sqlExpression, values ); } protected String getPrunedDiscriminatorPredicate( @@ -3222,10 +3337,10 @@ private void doLateInit() { createGeneratedValuesProcessor( UPDATE, updateGeneratedAttributes ); } - insertCoordinator = buildInsertCoordinator(); - updateCoordinator = buildUpdateCoordinator(); - deleteCoordinator = buildDeleteCoordinator(); - mergeCoordinator = buildMergeCoordinator(); + insertCoordinator = stateManagement.createInsertCoordinator( this ); + updateCoordinator = stateManagement.createUpdateCoordinator( this ); + deleteCoordinator = stateManagement.createDeleteCoordinator( this ); + mergeCoordinator = stateManagement.createMergeCoordinator( this ); //select SQL sqlVersionSelectString = generateSelectVersionString(); @@ -3445,50 +3560,14 @@ private void collectAttributesIndexesForTable(int naturalTableIndex, Consumer 1; + return optimizer != null && optimizer.getIncrementSize() > 1 + || !bulkInsertionCapableGenerator.supportsBulkInsertionIdentifierGeneration(); } else { return false; @@ -4831,9 +4983,8 @@ private void prepareMappingModel(MappingModelCreationProcess creationProcess, Pe (role, process) -> new EntityRowIdMappingImpl( rowIdName, getTableName(), this ) ); discriminatorMapping = generateDiscriminatorMapping( bootEntityDescriptor ); final var rootClass = bootEntityDescriptor.getRootClass(); - softDeleteMapping = - resolveSoftDeleteMapping( this, rootClass, getIdentifierTableName(), creationProcess ); - if ( softDeleteMapping != null && rootClass.getCustomSQLDelete() != null ) { + auxiliaryMapping = stateManagement.createAuxiliaryMapping( this, rootClass, creationProcess ); + if ( auxiliaryMapping instanceof SoftDeleteMapping && rootClass.getCustomSQLDelete() != null ) { throw new UnsupportedMappingException( "Entity may not define both @SoftDelete and @SQLDelete" ); } } @@ -4851,8 +5002,9 @@ else if ( bootEntityDescriptor.hasNaturalId() ) { } } - protected NaturalIdMapping generateNaturalIdMapping - (MappingModelCreationProcess creationProcess, PersistentClass bootEntityDescriptor) { + protected NaturalIdMapping generateNaturalIdMapping( + MappingModelCreationProcess creationProcess, + PersistentClass bootEntityDescriptor) { //noinspection AssertWithSideEffects assert bootEntityDescriptor.hasNaturalId(); @@ -4860,9 +5012,16 @@ else if ( bootEntityDescriptor.hasNaturalId() ) { assert naturalIdAttributeIndexes.length > 0; if ( naturalIdAttributeIndexes.length == 1 ) { + if ( bootEntityDescriptor.getRootClass().getNaturalIdClass() != null ) { + throw new UnsupportedMappingException( "NaturalIdClass not supported for simple naturaal-id mappings" ); + } final String propertyName = getPropertyNames()[ naturalIdAttributeIndexes[ 0 ] ]; final var attributeMapping = (SingularAttributeMapping) findAttributeMapping( propertyName ); - return new SimpleNaturalIdMapping( attributeMapping, this, creationProcess ); + return new SimpleNaturalIdMapping( + attributeMapping, + this, + creationProcess + ); } // collect the names of the attributes making up the natural-id. @@ -4886,7 +5045,12 @@ else if ( bootEntityDescriptor.hasNaturalId() ) { throw new MappingException( "Expected multiple natural-id attributes, but found only one: " + getEntityName() ); } - return new CompoundNaturalIdMapping(this, collectedAttrMappings, creationProcess ); + return new CompoundNaturalIdMapping( + this, + bootEntityDescriptor.getRootClass().getNaturalIdClass(), + collectedAttrMappings, + creationProcess + ); } protected static SqmMultiTableMutationStrategy interpretSqmMultiTableStrategy( @@ -5234,15 +5398,39 @@ protected AttributeMapping generateNonIdAttributeMapping( int stateArrayPosition, int fetchableIndex, MappingModelCreationProcess creationProcess) { - final var creationContext = creationProcess.getCreationContext(); - - final String attrName = tupleAttrDefinition.getName(); - final Type attrType = tupleAttrDefinition.getType(); - + final Type type = tupleAttrDefinition.getType(); final int propertyIndex = getPropertyIndex( bootProperty.getName() ); + final String[] attrColumnExpression = + type instanceof BasicType + && bootProperty.getSelectables().get( 0 ).isFormula() + ? propertyColumnFormulaTemplates[ propertyIndex ] + : getPropertyColumnNames( propertyIndex ) ; + return generateNonIdAttributeMapping( + tupleAttrDefinition.getName(), + type, + tupleAttrDefinition.getCascadeStyle(), + propertyIndex, + getTableName( getPropertyTableNumbers()[propertyIndex] ), + attrColumnExpression, + bootProperty, + stateArrayPosition, + fetchableIndex, + creationProcess + ); + } - final String tableExpression = getTableName( getPropertyTableNumbers()[propertyIndex] ); - final String[] attrColumnNames = getPropertyColumnNames( propertyIndex ); + protected AttributeMapping generateNonIdAttributeMapping( + String attrName, + Type attrType, + CascadeStyle cascadeStyle, + int propertyIndex, + String tableExpression, + String[] attrColumnNames, + Property bootProperty, + int stateArrayPosition, + int fetchableIndex, + MappingModelCreationProcess creationProcess) { + final var creationContext = creationProcess.getCreationContext(); final var propertyAccess = getRepresentationStrategy().resolvePropertyAccess( bootProperty ); @@ -5274,7 +5462,7 @@ protected AttributeMapping generateNonIdAttributeMapping( value.isColumnInsertable( 0 ), value.isColumnUpdateable( 0 ), propertyAccess, - tupleAttrDefinition.getCascadeStyle(), + cascadeStyle, creationProcess ); } @@ -5312,7 +5500,7 @@ protected AttributeMapping generateNonIdAttributeMapping( else { final var basicBootValue = (BasicValue) value; - if ( attrColumnNames[ 0 ] != null ) { + if ( !value.getSelectables().get( 0 ).isFormula() ) { attrColumnExpression = attrColumnNames[ 0 ]; isAttrColumnExpressionFormula = false; @@ -5345,8 +5533,7 @@ protected AttributeMapping generateNonIdAttributeMapping( resolveAggregateColumnBasicType( creationProcess, role, column ); } else { - final String[] attrColumnFormulaTemplate = propertyColumnFormulaTemplates[ propertyIndex ]; - attrColumnExpression = attrColumnFormulaTemplate[ 0 ]; + attrColumnExpression = attrColumnNames[ 0 ]; isAttrColumnExpressionFormula = true; customReadExpr = null; customWriteExpr = null; @@ -5386,7 +5573,7 @@ protected AttributeMapping generateNonIdAttributeMapping( value.isColumnInsertable( 0 ), value.isColumnUpdateable( 0 ), propertyAccess, - tupleAttrDefinition.getCascadeStyle(), + cascadeStyle, creationProcess ); } @@ -5431,7 +5618,7 @@ else if ( attrType instanceof CompositeType ) { tableExpression, null, propertyAccess, - tupleAttrDefinition.getCascadeStyle(), + cascadeStyle, creationProcess ); } @@ -5443,7 +5630,7 @@ else if ( attrType instanceof CollectionType ) { bootProperty, this, propertyAccess, - tupleAttrDefinition.getCascadeStyle(), + cascadeStyle, getFetchMode( stateArrayPosition ), creationProcess ); @@ -5459,7 +5646,7 @@ else if ( attrType instanceof EntityType entityType ) { this, entityType, propertyAccess, - tupleAttrDefinition.getCascadeStyle(), + cascadeStyle, creationProcess ); } @@ -5591,7 +5778,25 @@ public EntityDiscriminatorMapping getDiscriminatorMapping() { @Override public SoftDeleteMapping getSoftDeleteMapping() { - return softDeleteMapping; + return auxiliaryMapping instanceof SoftDeleteMapping softDeleteMapping + ? softDeleteMapping : null; + } + + @Override + public TemporalMapping getTemporalMapping() { + return auxiliaryMapping instanceof TemporalMapping temporalMapping + ? temporalMapping : null; + } + + @Override + public AuditMapping getAuditMapping() { + return auxiliaryMapping instanceof AuditMapping auditMapping + ? auditMapping : null; + } + + @Override + public AuxiliaryMapping getAuxiliaryMapping() { + return auxiliaryMapping; } @Override @@ -5722,6 +5927,11 @@ else if ( treatTargetType != this ) { } private ModelPart findSubPartInSubclassMappings(String name) { + final var declaredGenericAttribute = declaredGenericAttributeMappings.get( name ); + if ( declaredGenericAttribute != null ) { + return declaredGenericAttribute; + } + ModelPart attribute = null; if ( isNotEmpty( subclassMappingTypes ) ) { for ( var subMappingType : subclassMappingTypes.values() ) { 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 d8eb504ad3f6..9819b2cdb036 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 @@ -96,9 +96,7 @@ public static int[] findDirty( 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]; @@ -106,7 +104,6 @@ public static int[] findDirty( results[count++] = i; } } - return count == 0 ? null : ArrayHelper.trim( results, count ); } diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/DiscriminatorHelper.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/DiscriminatorHelper.java index a20b51d75e40..64bc7d439c5b 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/DiscriminatorHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/DiscriminatorHelper.java @@ -7,9 +7,9 @@ import org.hibernate.Internal; import org.hibernate.MappingException; import org.hibernate.dialect.Dialect; -import org.hibernate.internal.util.MarkerObject; import org.hibernate.mapping.Component; import org.hibernate.mapping.PersistentClass; +import org.hibernate.metamodel.mapping.DiscriminatorValue; import org.hibernate.query.sqm.NodeBuilder; import org.hibernate.query.sqm.SqmBindableType; import org.hibernate.query.sqm.SqmPathSource; @@ -29,9 +29,6 @@ @Internal public class DiscriminatorHelper { - public static final Object NULL_DISCRIMINATOR = new MarkerObject( "" ); - public static final Object NOT_NULL_DISCRIMINATOR = new MarkerObject( "" ); - /** * The underlying BasicType as the "JDBC mapping" between the relational {@link org.hibernate.type.descriptor.java.JavaType} * and the {@link org.hibernate.type.descriptor.jdbc.JdbcType}. @@ -68,11 +65,13 @@ else if ( persistentClass.isDiscriminatorValueNotNull() ) { } } - private static Object parseDiscriminatorValue(PersistentClass persistentClass) { + private static DiscriminatorValue parseDiscriminatorValue(PersistentClass persistentClass) { final BasicType discriminatorType = getDiscriminatorType( persistentClass ); final String discriminatorValue = persistentClass.getDiscriminatorValue(); try { - return discriminatorType.getJavaTypeDescriptor().fromString( discriminatorValue ); + return new DiscriminatorValue.Literal( + discriminatorType.getJavaTypeDescriptor().fromString( discriminatorValue ) + ); } catch ( Exception e ) { throw new MappingException( "Could not parse discriminator value '" + discriminatorValue @@ -80,18 +79,30 @@ private static Object parseDiscriminatorValue(PersistentClass persistentClass) { } } - public static Object getDiscriminatorValue(PersistentClass persistentClass) { + public static DiscriminatorValue getDiscriminatorValue(PersistentClass persistentClass) { if ( persistentClass.isDiscriminatorValueNull() ) { - return NULL_DISCRIMINATOR; + return DiscriminatorValue.Special.NULL; } else if ( persistentClass.isDiscriminatorValueNotNull() ) { - return NOT_NULL_DISCRIMINATOR; + return DiscriminatorValue.Special.NOT_NULL; } else { return parseDiscriminatorValue( persistentClass ); } } + public static Object toRelationalValue(DiscriminatorValue discriminatorValue) { + if ( discriminatorValue instanceof DiscriminatorValue.Literal literal ) { + return literal.value(); + } + else if ( discriminatorValue == DiscriminatorValue.Special.NULL ) { + return null; + } + else { + throw new IllegalStateException( "Cannot convert NOT_NULL discriminator marker to a relational literal" ); + } + } + private static String discriminatorSqlLiteral( BasicType discriminatorType, PersistentClass persistentClass, 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 1647bb84e45d..5aafe8020f56 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 @@ -139,7 +139,7 @@ public interface EntityPersister extends EntityMappingType, EntityMutationTarget * Prepare loaders associated with the persister. Distinct "phase" * in building the persister after {@linkplain InFlightEntityMappingType#prepareMappingModel} * and {@linkplain #postInstantiate()} have occurred. - *

        + *

        * The distinct phase is used to ensure that all {@linkplain org.hibernate.metamodel.mapping.TableDetails} * are available across the entire model */ @@ -862,6 +862,20 @@ default UpdateCoordinator getMergeCoordinator() { */ boolean[] getPropertyUpdateability(); + /** + * Is the property excluded from temporal versioning. + */ + default boolean isPropertyTemporalExcluded(int attributeIndex) { + return false; + } + + /** + * Is the property excluded from audit logging. + */ + default boolean isPropertyAuditedExcluded(int attributeIndex) { + return false; + } + /** * Get the "checkability" of the properties of this class * (is the property dirty checked, does the cache need @@ -1557,6 +1571,9 @@ default DiscriminatorMetadata getTypeDiscriminatorMetadata() { @Deprecated(since = "7.0", forRemoval = true) String[] toColumns(String propertyName); + @Incubating + boolean excludedFromTemporalVersioning(int[] dirtyAttributeIndexes, boolean hasDirtyCollection); + boolean isSharedColumn(String columnExpression); String[][] getConstraintOrderedTableKeyColumnClosure(); 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 5bfc852cd76a..c9dcb90e0f1e 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 @@ -10,6 +10,7 @@ import java.util.Map; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Function; import java.util.function.Supplier; import org.hibernate.AssertionFailure; @@ -26,6 +27,7 @@ import org.hibernate.mapping.Column; import org.hibernate.mapping.PersistentClass; import org.hibernate.mapping.Table; +import org.hibernate.metamodel.mapping.DiscriminatorValue; import org.hibernate.metamodel.mapping.EntityDiscriminatorMapping; import org.hibernate.metamodel.mapping.EntityIdentifierMapping; import org.hibernate.metamodel.mapping.EntityVersionMapping; @@ -36,6 +38,7 @@ import org.hibernate.metamodel.spi.MappingMetamodelImplementor; import org.hibernate.metamodel.spi.RuntimeModelCreationContext; import org.hibernate.persister.filter.internal.DynamicFilterAliasGenerator; +import org.hibernate.persister.state.spi.StateManagement; import org.hibernate.sql.ast.SqlAstJoinType; import org.hibernate.sql.ast.tree.from.NamedTableReference; import org.hibernate.sql.ast.tree.from.TableGroup; @@ -51,6 +54,7 @@ import org.jboss.logging.Logger; import static java.util.Collections.emptyMap; +import static java.util.function.Function.identity; import static org.hibernate.internal.util.collections.ArrayHelper.contains; import static org.hibernate.internal.util.collections.ArrayHelper.join; import static org.hibernate.internal.util.collections.ArrayHelper.reverseFirst; @@ -62,8 +66,6 @@ import static org.hibernate.jdbc.Expectations.createExpectation; import static org.hibernate.metamodel.mapping.internal.MappingModelCreationHelper.buildEncapsulatedCompositeIdentifierMapping; import static org.hibernate.metamodel.mapping.internal.MappingModelCreationHelper.buildNonEncapsulatedCompositeIdentifierMapping; -import static org.hibernate.persister.entity.DiscriminatorHelper.NOT_NULL_DISCRIMINATOR; -import static org.hibernate.persister.entity.DiscriminatorHelper.NULL_DISCRIMINATOR; /** * An {@link EntityPersister} implementing the normalized @@ -119,7 +121,7 @@ public class JoinedSubclassEntityPersister extends AbstractEntityPersister { // subclass discrimination works by assigning particular // values to certain combinations of not-null primary key // values in the outer join using an SQL CASE - private final Map subclassesByDiscriminatorValue = new HashMap<>(); + private final Map subclassesByDiscriminatorValue = new HashMap<>(); private final String[] discriminatorValues; private final boolean[] discriminatorAbstract; private final String[] notNullColumnNames; @@ -128,7 +130,7 @@ public class JoinedSubclassEntityPersister extends AbstractEntityPersister { private final String[] constraintOrderedTableNames; private final String[][] constraintOrderedKeyColumnNames; - private final Object discriminatorValue; + private final DiscriminatorValue discriminatorValue; private final String discriminatorSQLString; private final BasicType discriminatorType; private final String explicitDiscriminatorColumnName; @@ -142,7 +144,7 @@ public class JoinedSubclassEntityPersister extends AbstractEntityPersister { private final boolean[] isNullableTable; private final boolean[] isInverseTable; - private final Map discriminatorValuesByTableName; + private final Map discriminatorValuesByTableName; private final Map discriminatorColumnNameByTableName; public JoinedSubclassEntityPersister( @@ -150,8 +152,18 @@ public JoinedSubclassEntityPersister( final EntityDataAccess cacheAccessStrategy, final NaturalIdDataAccess naturalIdRegionAccessStrategy, final RuntimeModelCreationContext creationContext) + throws HibernateException { + this( persistentClass, cacheAccessStrategy, naturalIdRegionAccessStrategy, creationContext, identity() ); + } + + protected JoinedSubclassEntityPersister( + final PersistentClass persistentClass, + final EntityDataAccess cacheAccessStrategy, + final NaturalIdDataAccess naturalIdRegionAccessStrategy, + final RuntimeModelCreationContext creationContext, + final Function stateManagementConverter) throws HibernateException { - super( persistentClass, cacheAccessStrategy, naturalIdRegionAccessStrategy, creationContext ); + super( persistentClass, cacheAccessStrategy, naturalIdRegionAccessStrategy, creationContext, stateManagementConverter ); final var dialect = creationContext.getDialect(); final var typeConfiguration = creationContext.getTypeConfiguration(); @@ -181,8 +193,8 @@ public JoinedSubclassEntityPersister( discriminatorAlias = IMPLICIT_DISCRIMINATOR_ALIAS; discriminatorType = basicTypeRegistry.resolve( StandardBasicTypes.INTEGER ); try { - discriminatorValue = persistentClass.getSubclassId(); - discriminatorSQLString = discriminatorValue.toString(); + discriminatorValue = new DiscriminatorValue.Literal( persistentClass.getSubclassId() ); + discriminatorSQLString = Integer.toString( persistentClass.getSubclassId() ); } catch ( Exception e ) { throw new MappingException( "Could not format discriminator value to SQL string", e ); @@ -480,12 +492,12 @@ public JoinedSubclassEntityPersister( final var subclass = subclasses.get(k); final var subclassTable = subclass.getTable(); if ( persistentClass.isPolymorphic() ) { - final Object discriminatorValue = explicitDiscriminatorColumnName != null + final DiscriminatorValue discriminatorValue = explicitDiscriminatorColumnName != null ? DiscriminatorHelper.getDiscriminatorValue( subclass ) // we now use subclass ids that are consistent across all // persisters for a class hierarchy, so that the use of // "foo.class = Bar" works in HQL - : subclass.getSubclassId(); + : new DiscriminatorValue.Literal( subclass.getSubclassId() ); initDiscriminatorProperties( dialect, k, subclassTable, discriminatorValue, isAbstract( subclass ) ); subclassesByDiscriminatorValue.put( discriminatorValue, subclass.getEntityName() ); final int tableId = getTableId( @@ -509,17 +521,30 @@ public JoinedSubclassEntityPersister( } - private void initDiscriminatorProperties(Dialect dialect, int k, Table table, Object discriminatorValue, boolean isAbstract) { + private void initDiscriminatorProperties( + Dialect dialect, + int k, + Table table, + DiscriminatorValue discriminatorValue, + boolean isAbstract) { final String tableName = determineTableName( table ); final String columnName = table.getPrimaryKey().getColumn( 0 ).getQuotedName( dialect ); discriminatorValuesByTableName.put( tableName, discriminatorValue ); discriminatorColumnNameByTableName.put( tableName, columnName ); - discriminatorValues[k] = discriminatorValue.toString(); + if ( discriminatorValue instanceof DiscriminatorValue.Literal literal ) { + discriminatorValues[k] = String.valueOf( literal.value() ); + } + else if ( discriminatorValue == DiscriminatorValue.Special.NULL ) { + discriminatorValues[k] = "null"; + } + else { + discriminatorValues[k] = "not null"; + } discriminatorAbstract[k] = isAbstract; } @Override - public Map getSubclassByDiscriminatorValue() { + public Map getSubclassByDiscriminatorValue() { return subclassesByDiscriminatorValue; } @@ -704,7 +729,7 @@ public BasicType getDiscriminatorType() { } @Override - public Object getDiscriminatorValue() { + public DiscriminatorValue getDiscriminatorValue() { return discriminatorValue; } @@ -748,10 +773,10 @@ public void addDiscriminatorToInsertGroup(MutationGroupBuilder insertGroupBuilde } private String getDiscriminatorValueString() { - if ( discriminatorValue == NULL_DISCRIMINATOR ) { + if ( discriminatorValue == DiscriminatorValue.Special.NULL ) { return "null"; } - else if ( discriminatorValue == NOT_NULL_DISCRIMINATOR ) { + else if ( discriminatorValue == DiscriminatorValue.Special.NOT_NULL ) { return "not null"; } else { diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/SingleTableEntityPersister.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/SingleTableEntityPersister.java index 4573d6ccbf1f..faa088965af3 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/SingleTableEntityPersister.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/SingleTableEntityPersister.java @@ -7,6 +7,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.Map; +import java.util.function.Function; import org.hibernate.AssertionFailure; import org.hibernate.HibernateException; @@ -21,15 +22,18 @@ import org.hibernate.mapping.Formula; import org.hibernate.mapping.PersistentClass; import org.hibernate.metamodel.MappingMetamodel; +import org.hibernate.metamodel.mapping.DiscriminatorValue; import org.hibernate.metamodel.mapping.TableDetails; import org.hibernate.metamodel.spi.RuntimeModelCreationContext; import org.hibernate.persister.filter.internal.DynamicFilterAliasGenerator; +import org.hibernate.persister.state.spi.StateManagement; import org.hibernate.sql.ast.tree.from.NamedTableReference; import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.model.ast.builder.MutationGroupBuilder; import org.hibernate.sql.model.ast.builder.TableInsertBuilder; import org.hibernate.type.BasicType; +import static java.util.function.Function.identity; import static org.hibernate.internal.util.collections.ArrayHelper.indexOf; import static org.hibernate.internal.util.collections.ArrayHelper.join; import static org.hibernate.internal.util.collections.ArrayHelper.to2DStringArray; @@ -38,7 +42,6 @@ import static org.hibernate.internal.util.collections.ArrayHelper.toStringArray; import static org.hibernate.internal.util.collections.CollectionHelper.toSmallMap; import static org.hibernate.jdbc.Expectations.createExpectation; -import static org.hibernate.persister.entity.DiscriminatorHelper.NULL_DISCRIMINATOR; import static org.hibernate.sql.model.ast.builder.TableMutationBuilder.NULL; /** @@ -87,14 +90,14 @@ public class SingleTableEntityPersister extends AbstractEntityPersister { private final int[] subclassPropertyTableNumberClosure; // discriminator column - private final Map subclassesByDiscriminatorValue; + private final Map subclassesByDiscriminatorValue; private final boolean forceDiscriminator; private final String discriminatorColumnName; private final String discriminatorColumnReaders; private final String discriminatorColumnReaderTemplate; private final String discriminatorFormulaTemplate; private final BasicType discriminatorType; - private final Object discriminatorValue; + private final DiscriminatorValue discriminatorValue; private final String discriminatorSQLValue; private final boolean discriminatorInsertable; @@ -106,8 +109,18 @@ public SingleTableEntityPersister( final EntityDataAccess cacheAccessStrategy, final NaturalIdDataAccess naturalIdRegionAccessStrategy, final RuntimeModelCreationContext creationContext) + throws HibernateException { + this( persistentClass, cacheAccessStrategy, naturalIdRegionAccessStrategy, creationContext, identity() ); + } + + protected SingleTableEntityPersister( + final PersistentClass persistentClass, + final EntityDataAccess cacheAccessStrategy, + final NaturalIdDataAccess naturalIdRegionAccessStrategy, + final RuntimeModelCreationContext creationContext, + final Function statementManagerConverter) throws HibernateException { - super( persistentClass, cacheAccessStrategy, naturalIdRegionAccessStrategy, creationContext ); + super( persistentClass, cacheAccessStrategy, naturalIdRegionAccessStrategy, creationContext, statementManagerConverter ); final var dialect = creationContext.getDialect(); final var typeConfiguration = creationContext.getTypeConfiguration(); @@ -282,7 +295,7 @@ else if ( selectable instanceof Column column ) { //TODO: code duplication with JoinedSubclassEntityPersister final ArrayList propertyJoinNumbers = new ArrayList<>(); - final Map subclassesByDiscriminatorValueLocal = new HashMap<>(); + final Map subclassesByDiscriminatorValueLocal = new HashMap<>(); for ( var property : persistentClass.getSubclassPropertyClosure() ) { propertyJoinNumbers.add( persistentClass.getJoinNumber( property ) ); @@ -329,8 +342,8 @@ private static boolean isDiscriminatorInsertable(PersistentClass persistentClass } private static void addSubclassByDiscriminatorValue( - Map subclassesByDiscriminatorValue, - Object discriminatorValue, + Map subclassesByDiscriminatorValue, + DiscriminatorValue discriminatorValue, String entityName) { final String mappedEntityName = subclassesByDiscriminatorValue.put( discriminatorValue, entityName ); if ( mappedEntityName != null ) { @@ -377,7 +390,7 @@ public BasicType getDiscriminatorType() { } @Override - public Map getSubclassByDiscriminatorValue() { + public Map getSubclassByDiscriminatorValue() { return subclassesByDiscriminatorValue; } @@ -392,7 +405,7 @@ public TableDetails getIdentifierTableDetails() { } @Override - public Object getDiscriminatorValue() { + public DiscriminatorValue getDiscriminatorValue() { return discriminatorValue; } @@ -472,7 +485,7 @@ public void addDiscriminatorToInsertGroup(MutationGroupBuilder insertGroupBuilde final TableInsertBuilder tableInsertBuilder = insertGroupBuilder.getTableDetailsBuilder( getRootTableName() ); tableInsertBuilder.addValueColumn( - discriminatorValue == NULL_DISCRIMINATOR ? NULL : discriminatorSQLValue, + discriminatorValue == DiscriminatorValue.Special.NULL ? NULL : discriminatorSQLValue, getDiscriminatorMapping() ); } diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/UnionSubclassEntityPersister.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/UnionSubclassEntityPersister.java index 68ca65971a64..431e71d5ded3 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/UnionSubclassEntityPersister.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/UnionSubclassEntityPersister.java @@ -15,6 +15,7 @@ import java.util.Map; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Function; import java.util.function.Supplier; import org.hibernate.AssertionFailure; @@ -31,6 +32,7 @@ import org.hibernate.jdbc.Expectation; import org.hibernate.mapping.Column; import org.hibernate.mapping.PersistentClass; +import org.hibernate.metamodel.mapping.DiscriminatorValue; import org.hibernate.metamodel.mapping.EntityDiscriminatorMapping; import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.SelectableConsumer; @@ -38,6 +40,7 @@ import org.hibernate.metamodel.mapping.TableDetails; import org.hibernate.metamodel.mapping.internal.SqlTypedMappingImpl; import org.hibernate.metamodel.spi.RuntimeModelCreationContext; +import org.hibernate.persister.state.spi.StateManagement; import org.hibernate.spi.NavigablePath; import org.hibernate.sql.ast.spi.SqlAliasBase; import org.hibernate.sql.ast.spi.SqlAstCreationState; @@ -52,6 +55,7 @@ import static java.util.Collections.addAll; import static java.util.Collections.unmodifiableList; +import static java.util.function.Function.identity; import static org.hibernate.internal.util.collections.ArrayHelper.to2DStringArray; import static org.hibernate.internal.util.collections.ArrayHelper.toStringArray; import static org.hibernate.jdbc.Expectations.createExpectation; @@ -77,10 +81,10 @@ public class UnionSubclassEntityPersister extends AbstractEntityPersister { private final String[] spaces; private final String[] subclassSpaces; private final String[] subclassTableExpressions; - private final Object discriminatorValue; + private final DiscriminatorValue discriminatorValue; private final String discriminatorSQLValue; private final BasicType discriminatorType; - private final Map subclassByDiscriminatorValue = new HashMap<>(); + private final Map subclassByDiscriminatorValue = new HashMap<>(); private final String[] constraintOrderedTableNames; private final String[][] constraintOrderedKeyColumnNames; @@ -90,8 +94,18 @@ public UnionSubclassEntityPersister( final EntityDataAccess cacheAccessStrategy, final NaturalIdDataAccess naturalIdRegionAccessStrategy, final RuntimeModelCreationContext creationContext) + throws HibernateException { + this( persistentClass, cacheAccessStrategy, naturalIdRegionAccessStrategy, creationContext, identity() ); + } + + protected UnionSubclassEntityPersister( + final PersistentClass persistentClass, + final EntityDataAccess cacheAccessStrategy, + final NaturalIdDataAccess naturalIdRegionAccessStrategy, + final RuntimeModelCreationContext creationContext, + final Function stateManagementConverter) throws HibernateException { - super( persistentClass, cacheAccessStrategy, naturalIdRegionAccessStrategy, creationContext ); + super( persistentClass, cacheAccessStrategy, naturalIdRegionAccessStrategy, creationContext, stateManagementConverter ); validateGenerator(); @@ -118,17 +132,17 @@ public UnionSubclassEntityPersister( deleteExpectations = new Expectation[] { createExpectation( persistentClass.getDeleteExpectation(), persistentClass.isCustomDeleteCallable() ) }; - discriminatorValue = persistentClass.getSubclassId(); + discriminatorValue = new DiscriminatorValue.Literal( persistentClass.getSubclassId() ); discriminatorSQLValue = String.valueOf( persistentClass.getSubclassId() ); discriminatorType = creationContext.getTypeConfiguration().getBasicTypeRegistry().resolve( StandardBasicTypes.INTEGER ); // PROPERTIES // SUBCLASSES - subclassByDiscriminatorValue.put( persistentClass.getSubclassId(), persistentClass.getEntityName() ); + subclassByDiscriminatorValue.put( new DiscriminatorValue.Literal( persistentClass.getSubclassId() ), persistentClass.getEntityName() ); if ( persistentClass.isPolymorphic() ) { for ( var subclass : persistentClass.getSubclasses() ) { - subclassByDiscriminatorValue.put( subclass.getSubclassId(), subclass.getEntityName() ); + subclassByDiscriminatorValue.put( new DiscriminatorValue.Literal( subclass.getSubclassId() ), subclass.getEntityName() ); } } @@ -236,13 +250,25 @@ public TableGroup createRootTableGroup( SqlAliasBase sqlAliasBase, Supplier> additionalPredicateCollectorAccess, SqlAstCreationState creationState) { - return new UnionTableGroup( + final var tableGroup = new UnionTableGroup( canUseInnerJoins, navigablePath, createPrimaryTableReference( sqlAliasBase, creationState ), this, explicitSourceAlias ); + final var softDeleteMapping = getSoftDeleteMapping(); + if ( additionalPredicateCollectorAccess != null && softDeleteMapping != null ) { + final var tableReference = + tableGroup.resolveTableReference( getSoftDeleteTableDetails().getTableName() ); + additionalPredicateCollectorAccess.get().accept( + softDeleteMapping.createNonDeletedRestriction( + tableReference, + creationState.getSqlExpressionResolver() + ) + ); + } + return tableGroup; } @Override @@ -271,7 +297,7 @@ public BasicType getDiscriminatorType() { } @Override - public Map getSubclassByDiscriminatorValue() { + public Map getSubclassByDiscriminatorValue() { return subclassByDiscriminatorValue; } @@ -286,7 +312,7 @@ public TableDetails getIdentifierTableDetails() { } @Override - public Object getDiscriminatorValue() { + public DiscriminatorValue getDiscriminatorValue() { return discriminatorValue; } diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/AbstractAuditCoordinator.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/AbstractAuditCoordinator.java new file mode 100644 index 000000000000..4f6ff8741384 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/AbstractAuditCoordinator.java @@ -0,0 +1,289 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.persister.entity.mutation; + +import java.sql.SQLException; + +import org.hibernate.engine.jdbc.batch.internal.BasicBatchKey; +import org.hibernate.engine.jdbc.batch.spi.BatchKey; +import org.hibernate.engine.jdbc.mutation.JdbcValueBindings; +import org.hibernate.engine.jdbc.mutation.ParameterUsage; +import org.hibernate.engine.jdbc.mutation.group.PreparedStatementDetails; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.MappingException; +import org.hibernate.generator.Generator; +import org.hibernate.generator.OnExecutionGenerator; +import org.hibernate.metamodel.mapping.AttributeMapping; +import org.hibernate.metamodel.mapping.PluralAttributeMapping; +import org.hibernate.metamodel.mapping.SelectableMapping; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.persister.state.internal.AuditStateManagement; +import org.hibernate.sql.model.MutationOperationGroup; +import org.hibernate.sql.model.MutationType; +import org.hibernate.sql.model.ast.builder.TableInsertBuilderStandard; +import org.hibernate.sql.model.internal.MutationGroupSingle; + +import static org.hibernate.persister.entity.mutation.InsertCoordinatorStandard.getPropertiesToInsert; +import static org.hibernate.sql.model.internal.MutationOperationGroupFactory.singleOperation; + +/** + * Base support for audit log insert coordinators. + */ +abstract class AbstractAuditCoordinator extends AbstractMutationCoordinator { + protected final EntityTableMapping identifierTableMapping; + protected final EntityTableMapping auditTableMapping; + protected final String auditTableName; + protected final boolean[] auditedPropertyMask; + private final SelectableMapping transactionIdMapping; + private final SelectableMapping modificationTypeMapping; + protected final BasicBatchKey auditBatchKey; + private final boolean useServerTransactionTimestamps; + private final String currentTimestampFunctionName; + private final MutationOperationGroup staticAuditInsertGroup; + + protected AbstractAuditCoordinator(EntityPersister entityPersister, SessionFactoryImplementor factory) { + super( entityPersister, factory ); + this.identifierTableMapping = entityPersister.getIdentifierTableMapping(); + final var auditMapping = entityPersister.getAuditMapping(); + if ( auditMapping == null ) { + throw new MappingException( "No audit mapping available for " + entityPersister.getEntityName() ); + } + this.auditTableName = auditMapping.getTableName(); + this.auditTableMapping = createAuxiliaryTableMapping( identifierTableMapping, entityPersister, auditTableName ); + this.auditedPropertyMask = new boolean[entityPersister.getPropertySpan()]; + for ( int i = 0; i < this.auditedPropertyMask.length; i++ ) { + this.auditedPropertyMask[i] = !entityPersister.isPropertyAuditedExcluded( i ); + } + this.transactionIdMapping = auditMapping.getTransactionIdMapping(); + this.modificationTypeMapping = auditMapping.getModificationTypeMapping(); + this.useServerTransactionTimestamps = + factory.getTransactionIdentifierService().useServerTimestamp( dialect() ); + this.currentTimestampFunctionName = useServerTransactionTimestamps ? dialect().currentTimestamp() : null; + this.auditBatchKey = new BasicBatchKey( entityPersister.getEntityName() + "#AUDIT_INSERT" ); + this.staticAuditInsertGroup = entityPersister.isDynamicInsert() + ? null + : buildAuditInsertGroup( applyAuditMask( entityPersister.getPropertyInsertability() ), null, null ); + } + + protected void insertAuditRow( + Object entity, + Object id, + Object[] values, + AuditStateManagement.ModificationType modificationType, + SharedSessionContractImplementor session) { + if ( values != null ) { + final boolean dynamicInsert = entityPersister().isDynamicInsert(); + final boolean[] propertyInclusions = applyAuditMask( + dynamicInsert + ? getPropertiesToInsert( entityPersister(), values ) + : entityPersister().getPropertyInsertability() + ); + final MutationOperationGroup operationGroup = dynamicInsert + ? buildAuditInsertGroup( propertyInclusions, entity, session ) + : staticAuditInsertGroup; + + final Object resolvedId = id != null + ? id + : entity != null ? entityPersister().getIdentifier( entity, session ) : null; + if ( resolvedId == null || operationGroup == null ) { + return; + } + + final var mutationExecutor = mutationExecutorService.createExecutor( + resolveBatchKeyAccess( dynamicInsert, session ), + operationGroup, + session + ); + try { + bindAuditValues( resolvedId, values, propertyInclusions, modificationType, session, + mutationExecutor.getJdbcValueBindings() ); + mutationExecutor.execute( entity, null, null, AbstractAuditCoordinator::verifyOutcome, session ); + } + finally { + mutationExecutor.release(); + } + } + } + + protected BatchKey getAuditBatchKey() { + return auditBatchKey; + } + + @Override + protected BatchKey getBatchKey() { + return auditBatchKey; + } + + private MutationOperationGroup buildAuditInsertGroup( + boolean[] propertyInclusions, + Object entity, + SharedSessionContractImplementor session) { + final var insertBuilder = + new TableInsertBuilderStandard( entityPersister(), auditTableMapping, factory() ); + applyAuditInsertDetails( insertBuilder, propertyInclusions, entity, session ); + final var tableMutation = insertBuilder.buildMutation(); + return singleOperation( + new MutationGroupSingle( MutationType.INSERT, entityPersister(), tableMutation ), + tableMutation.createMutationOperation( null, factory() ) + ); + } + + private void applyAuditInsertDetails( + TableInsertBuilderStandard insertBuilder, + boolean[] propertyInclusions, + Object entity, + SharedSessionContractImplementor session) { + final var attributeMappings = entityPersister().getAttributeMappings(); + for ( final int attributeIndex : identifierTableMapping.getAttributeIndexes() ) { + final var attributeMapping = attributeMappings.get( attributeIndex ); + if ( propertyInclusions[attributeIndex] ) { + attributeMapping.forEachInsertable( insertBuilder ); + } + else { + final var generator = attributeMapping.getGenerator(); + if ( isValueGenerated( generator ) ) { + if ( entity != null && generator.generatedBeforeExecution( entity, session ) ) { + propertyInclusions[attributeIndex] = true; + attributeMapping.forEachInsertable( insertBuilder ); + } + else if ( isValueGenerationInSql( generator ) ) { + addSqlGeneratedValue( insertBuilder, attributeMapping, (OnExecutionGenerator) generator ); + } + } + } + } + + if ( useServerTransactionTimestamps ) { + insertBuilder.addValueColumn( currentTimestampFunctionName, getTransactionIdMapping() ); + } + else { + insertBuilder.addValueColumn( "?", getTransactionIdMapping() ); + } + insertBuilder.addValueColumn( "?", getModificationTypeMapping() ); + + identifierTableMapping.getKeyMapping().forEachKeyColumn( insertBuilder::addKeyColumn ); + } + + private void bindAuditValues( + Object id, + Object[] values, + boolean[] propertyInclusions, + AuditStateManagement.ModificationType modificationType, + SharedSessionContractImplementor session, + JdbcValueBindings jdbcValueBindings) { + final String tableName = auditTableName; + auditTableMapping.getKeyMapping().breakDownKeyJdbcValues( + id, + (jdbcValue, columnMapping) -> jdbcValueBindings.bindValue( + jdbcValue, + tableName, + columnMapping.getColumnName(), + ParameterUsage.SET + ), + session + ); + + final var attributeMappings = entityPersister().getAttributeMappings(); + for ( final int attributeIndex : identifierTableMapping.getAttributeIndexes() ) { + if ( propertyInclusions[attributeIndex] ) { + final var attributeMapping = attributeMappings.get( attributeIndex ); + if ( !(attributeMapping instanceof PluralAttributeMapping) ) { + attributeMapping.decompose( + values[attributeIndex], + 0, + jdbcValueBindings, + null, + (valueIndex, bindings, noop, jdbcValue, selectableMapping) -> { + if ( selectableMapping.isInsertable() && !selectableMapping.isFormula() ) { + bindings.bindValue( + jdbcValue, + tableName, + selectableMapping.getSelectionExpression(), + ParameterUsage.SET + ); + } + }, + session + ); + } + } + } + + if ( !useServerTransactionTimestamps ) { + jdbcValueBindings.bindValue( + session.getCurrentTransactionIdentifier(), + tableName, + getTransactionIdMapping().getSelectionExpression(), + ParameterUsage.SET + ); + } + + jdbcValueBindings.bindValue( + Integer.valueOf( modificationType.ordinal() ), + tableName, + getModificationTypeMapping().getSelectionExpression(), + ParameterUsage.SET + ); + } + + private boolean[] applyAuditMask(boolean[] propertyInclusions) { + if ( auditedPropertyMask == null ) { + return propertyInclusions; + } + final boolean[] masked = propertyInclusions.clone(); + for ( int i = 0; i < masked.length; i++ ) { + if ( !auditedPropertyMask[i] ) { + masked[i] = false; + } + } + return masked; + } + + private static boolean isValueGenerated(Generator generator) { + return generator != null + && generator.generatesOnInsert() + && generator.generatedOnExecution(); + } + + private boolean isValueGenerationInSql(Generator generator) { + assert isValueGenerated( generator ); + return ( (OnExecutionGenerator) generator ).referenceColumnsInSql( dialect() ); + } + + private void addSqlGeneratedValue( + TableInsertBuilderStandard insertBuilder, + AttributeMapping attributeMapping, + OnExecutionGenerator generator) { + final boolean writePropertyValue = generator.writePropertyValue(); + final String[] columnValues = + writePropertyValue + ? null + : generator.getReferencedColumnValues( factory.getJdbcServices().getDialect() ); + attributeMapping.forEachSelectable( (j, mapping) -> + insertBuilder.addValueColumn( writePropertyValue ? "?" : columnValues[j], mapping ) ); + } + + protected SelectableMapping getTransactionIdMapping() { + return transactionIdMapping; + } + + protected SelectableMapping getModificationTypeMapping() { + return modificationTypeMapping; + } + + private static boolean verifyOutcome( + PreparedStatementDetails statementDetails, + int affectedRowCount, + int batchPosition) throws SQLException { + statementDetails.getExpectation().verifyOutcome( + affectedRowCount, + statementDetails.getStatement(), + batchPosition, + statementDetails.getSqlString() + ); + return true; + } +} 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 6ca1007386eb..b764dac8b919 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 @@ -4,19 +4,25 @@ */ package org.hibernate.persister.entity.mutation; -import org.hibernate.StaleObjectStateException; -import org.hibernate.StaleStateException; +import org.hibernate.engine.OptimisticLockStyle; import org.hibernate.engine.jdbc.batch.internal.BasicBatchKey; import org.hibernate.engine.jdbc.mutation.JdbcValueBindings; import org.hibernate.engine.jdbc.mutation.MutationExecutor; import org.hibernate.engine.jdbc.mutation.ParameterUsage; -import org.hibernate.engine.jdbc.mutation.group.PreparedStatementDetails; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.metamodel.mapping.AttributeMapping; import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.sql.model.MutationOperation; import org.hibernate.sql.model.MutationOperationGroup; +import org.hibernate.sql.model.MutationType; +import org.hibernate.sql.model.ast.builder.RestrictedTableMutationBuilder; +import org.hibernate.sql.model.ast.builder.TableUpdateBuilderStandard; +import org.hibernate.sql.model.internal.MutationGroupSingle; -import static org.hibernate.engine.jdbc.mutation.internal.ModelMutationHelper.identifiedResultsCheck; +import java.util.function.Function; + +import static org.hibernate.sql.model.internal.MutationOperationGroupFactory.singleOperation; /** * Template support for DeleteCoordinator implementations. Mainly @@ -115,7 +121,7 @@ protected void doDynamicDelete( (statementDetails, affectedRowCount, batchPosition) -> resultCheck( id, statementDetails, affectedRowCount, batchPosition ), session, - staleStateException -> staleObjectState( id, staleStateException ) + staleStateException -> staleObjectStateException( id, staleStateException ) ); } finally { @@ -272,7 +278,7 @@ protected void doStaticDelete( (statementDetails, affectedRowCount, batchPosition) -> resultCheck( id, statementDetails, affectedRowCount, batchPosition ), session, - staleStateException -> staleObjectState( id, staleStateException ) + staleStateException -> staleObjectStateException( id, staleStateException ) ); } finally { @@ -280,22 +286,6 @@ protected void doStaticDelete( } } - private StaleObjectStateException staleObjectState(Object id, StaleStateException staleStateException) { - return new StaleObjectStateException( entityPersister.getEntityName(), id, staleStateException ); - } - - private boolean resultCheck( - Object id, PreparedStatementDetails statementDetails, int affectedRowCount, int batchPosition) { - return identifiedResultsCheck( - statementDetails, - affectedRowCount, - batchPosition, - entityPersister, - id, - factory - ); - } - protected void applyStaticDeleteTableDetails( Object id, Object rowId, @@ -308,8 +298,8 @@ protected void applyStaticDeleteTableDetails( applyLocking( version, null, mutationExecutor, session ); } - final var jdbcValueBindings = mutationExecutor.getJdbcValueBindings(); - bindPartitionColumnValueBindings( loadedState, session, jdbcValueBindings ); + bindPartitionColumnValueBindings( loadedState, session, + mutationExecutor.getJdbcValueBindings() ); applyId( id, rowId, mutationExecutor, staticOperationGroup, session ); } @@ -324,4 +314,115 @@ protected MutationOperationGroup resolveNoVersionDeleteGroup(SharedSessionContra } return noVersionDeleteGroup; } + + protected void applyOptimisticLocking( + OptimisticLockStyle optimisticLockStyle, + Function> resolver, + Object[] loadedState, + SharedSessionContractImplementor session) { + if ( optimisticLockStyle.isVersion() && entityPersister().getVersionMapping() != null ) { + applyVersionBasedOptLocking( resolver ); + } + else if ( loadedState != null && optimisticLockStyle.isAllOrDirty() ) { + applyNonVersionOptLocking( optimisticLockStyle, resolver, loadedState, session ); + } + } + + protected void applyVersionBasedOptLocking(Function> resolver) { + final var versionMapping = entityPersister().getVersionMapping(); + if ( versionMapping != null ) { + final String tableNameForMutation = + entityPersister().physicalTableNameForMutation( versionMapping ); + final var tableMutationBuilder = resolver.apply( tableNameForMutation ); + if ( tableMutationBuilder != null ) { + applyVersionOptimisticLocking( tableMutationBuilder ); + } + } + } + + protected void applyNonVersionOptLocking( + OptimisticLockStyle lockStyle, + Function> resolver, + Object[] loadedState, + SharedSessionContractImplementor session) { + final var persister = entityPersister(); + assert loadedState != null; + assert lockStyle.isAllOrDirty(); + assert persister.optimisticLockStyle().isAllOrDirty(); + assert session != null; + + final boolean[] versionability = persister.getPropertyVersionability(); + for ( int attributeIndex = 0; attributeIndex < versionability.length; attributeIndex++ ) { + // only makes sense to lock on singular attributes which are not excluded from optimistic locking + if ( versionability[attributeIndex] ) { + final var attribute = persister.getAttributeMapping( attributeIndex ); + if ( !attribute.isPluralAttributeMapping() ) { + breakDownJdbcValues( resolver, session, attribute, loadedState[attributeIndex] ); + } + } + } + } + + private void breakDownJdbcValues( + Function> resolver, + SharedSessionContractImplementor session, + AttributeMapping attribute, + Object loadedValue) { + final var tableMutationBuilder = + resolver.apply( attribute.getContainingTableExpression() ); + if ( tableMutationBuilder != null ) { + final var optimisticLockBindings = tableMutationBuilder.getOptimisticLockBindings(); + if ( optimisticLockBindings != null ) { + attribute.breakDownJdbcValues( + loadedValue, + (valueIndex, value, jdbcValueMapping) -> { + if ( !tableMutationBuilder.getKeyRestrictionBindings() + .containsColumn( + jdbcValueMapping.getSelectableName(), + jdbcValueMapping.getJdbcMapping() + ) ) { + optimisticLockBindings.consume( valueIndex, value, jdbcValueMapping ); + } + }, + session + ); + } + } + } + + protected Function> tableMutationBuilderResolver( + RestrictedTableMutationBuilder tableMutationBuilder) { + final String tableName = tableMutationBuilder.getMutatingTable().getTableName(); + return name -> tableName.equals( name ) ? tableMutationBuilder : null; + } + + protected void applyPartitionKeyRestriction(Function> resolver) { + final var persister = entityPersister(); + if ( persister.hasPartitionedSelectionMapping() ) { + final var attributeMappings = persister.getAttributeMappings(); + for ( int m = 0; m < attributeMappings.size(); m++ ) { + final var attributeMapping = attributeMappings.get( m ); + final int jdbcTypeCount = attributeMapping.getJdbcTypeCount(); + for ( int i = 0; i < jdbcTypeCount; i++ ) { + final var selectableMapping = attributeMapping.getSelectable( i ); + if ( selectableMapping.isPartitioned() ) { + final String tableNameForMutation = + persister.physicalTableNameForMutation( selectableMapping ); + final var tableMutationBuilder = resolver.apply( tableNameForMutation ); + if ( tableMutationBuilder != null ) { + tableMutationBuilder.addKeyRestrictionLeniently( selectableMapping ); + } + } + } + } + } + } + + MutationOperationGroup createMutationOperationGroup(TableUpdateBuilderStandard tableUpdateBuilder) { + final var tableMutation = tableUpdateBuilder.buildMutation(); + return singleOperation( + new MutationGroupSingle( MutationType.DELETE, entityPersister(), tableMutation ), + tableMutation.createMutationOperation( null, factory() ) + ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/AbstractMutationCoordinator.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/AbstractMutationCoordinator.java index 9c80593e111f..6336fbe95fb4 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/AbstractMutationCoordinator.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/AbstractMutationCoordinator.java @@ -5,15 +5,20 @@ package org.hibernate.persister.entity.mutation; import org.hibernate.Internal; +import org.hibernate.StaleObjectStateException; +import org.hibernate.StaleStateException; import org.hibernate.dialect.Dialect; +import org.hibernate.engine.OptimisticLockStyle; import org.hibernate.engine.jdbc.batch.spi.BatchKey; import org.hibernate.engine.jdbc.mutation.JdbcValueBindings; import org.hibernate.engine.jdbc.mutation.ParameterUsage; +import org.hibernate.engine.jdbc.mutation.group.PreparedStatementDetails; import org.hibernate.engine.jdbc.mutation.internal.NoBatchKeyAccess; import org.hibernate.engine.jdbc.mutation.spi.BatchKeyAccess; import org.hibernate.engine.jdbc.mutation.spi.MutationExecutorService; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.generator.EventType; import org.hibernate.generator.OnExecutionGenerator; import org.hibernate.metamodel.mapping.AttributeMapping; import org.hibernate.persister.entity.EntityPersister; @@ -27,6 +32,7 @@ import org.hibernate.sql.model.ast.builder.RestrictedTableMutationBuilder; import static java.lang.System.arraycopy; +import static org.hibernate.engine.jdbc.mutation.internal.ModelMutationHelper.identifiedResultsCheck; import static org.hibernate.sql.model.ModelMutationLogging.MODEL_MUTATION_LOGGER; import static org.hibernate.sql.model.internal.MutationOperationGroupFactory.manyOperations; import static org.hibernate.sql.model.internal.MutationOperationGroupFactory.noOperations; @@ -55,6 +61,43 @@ public AbstractMutationCoordinator(EntityPersister entityPersister, SessionFacto mutationExecutorService = factory.getServiceRegistry().getService( MutationExecutorService.class ); } + static boolean hasValueGenerationOnExecution( + OnExecutionGenerator generator, + Dialect dialect, + EventType eventType) { + if ( generator.getEventTypes().contains( eventType ) ) { + final boolean[] columnInclusions = generator.getColumnInclusions( dialect, eventType ); + if ( columnInclusions != null ) { + for ( boolean included : columnInclusions ) { + if ( !included ) { + return true; + } + } + } + if ( !generator.referenceColumnsInSql( dialect, eventType ) ) { + return false; + } + else if ( !generator.writePropertyValue( eventType ) ) { + return true; + } + else { + final String[] columnValues = generator.getReferencedColumnValues( dialect, eventType ); + if ( columnValues != null ) { + for ( int i = 0; i < columnValues.length; i++ ) { + if ( (columnInclusions == null || columnInclusions[i]) + && !"?".equals( columnValues[i] ) ) { + return true; + } + } + } + return false; + } + } + else { + return false; + } + } + protected EntityPersister entityPersister() { return entityPersister; } @@ -68,11 +111,11 @@ protected Dialect dialect() { } protected BatchKeyAccess resolveBatchKeyAccess(boolean dynamicUpdate, SharedSessionContractImplementor session) { - if ( !dynamicUpdate - && !entityPersister().optimisticLockStyle().isAllOrDirty() - && session.getTransactionCoordinator() != null - && session.getTransactionCoordinator().isTransactionActive() ) { - return this::getBatchKey; + if ( !dynamicUpdate && !entityPersister().optimisticLockStyle().isAllOrDirty() ) { + final var transactionCoordinator = session.getTransactionCoordinator(); + if ( transactionCoordinator != null && transactionCoordinator.isTransactionActive() ) { + return this::getBatchKey; + } } return NoBatchKeyAccess.INSTANCE; @@ -123,21 +166,39 @@ protected MutationOperation createOperation(ValuesAnalysis valuesAnalysis, Table return singleTableMutation.createMutationOperation( valuesAnalysis, factory() ); } + // Used by Hibernate Reactive + protected boolean hasValueGenerationOnExecution( + Object entity, + SharedSessionContractImplementor session, + OnExecutionGenerator generator, + EventType eventType) { + final boolean generatedOnExecution = + session == null + ? generator.generatedOnExecution() + : generator.generatedOnExecution( entity, session ); + return generatedOnExecution + && hasValueGenerationOnExecution( generator, dialect(), eventType ); + } + protected void handleValueGeneration( AttributeMapping attributeMapping, MutationGroupBuilder mutationGroupBuilder, - OnExecutionGenerator generator) { - final Dialect dialect = factory.getJdbcServices().getDialect(); - final boolean writePropertyValue = generator.writePropertyValue(); - final String[] columnValues = writePropertyValue ? null : generator.getReferencedColumnValues( dialect ); + OnExecutionGenerator generator, + EventType eventType) { + final var dialect = dialect(); + final var columnValues = generator.getReferencedColumnValues( dialect, eventType ); + final var columnInclusions = generator.getColumnInclusions( dialect, eventType ); attributeMapping.forEachSelectable( (j, mapping) -> { - final String tableName = entityPersister.physicalTableNameForMutation( mapping ); - final ColumnValuesTableMutationBuilder tableUpdateBuilder = - mutationGroupBuilder.findTableDetailsBuilder( tableName ); - tableUpdateBuilder.addValueColumn( - writePropertyValue ? "?" : columnValues[j], - mapping - ); + if ( columnInclusions == null || columnInclusions[j] ) { + final ColumnValuesTableMutationBuilder tableUpdateBuilder = + mutationGroupBuilder.findTableDetailsBuilder( + entityPersister.physicalTableNameForMutation( mapping ) ); + final String columnValue = + columnValues != null && columnValues[j] != null + ? columnValues[j] + : "?"; + tableUpdateBuilder.addValueColumn( columnValue, mapping ); + } } ); } @@ -220,4 +281,82 @@ protected void breakDownKeyJdbcValues( ); } } -} + + boolean resultCheck( + Object id, + PreparedStatementDetails statementDetails, + int affectedRowCount, + int batchPosition) { + return identifiedResultsCheck( + statementDetails, + affectedRowCount, + batchPosition, + entityPersister(), + id, + factory() + ); + } + + void applyOptimisticLocking(RestrictedTableMutationBuilder tableMutationBuilder) { + if ( entityPersister().optimisticLockStyle() == OptimisticLockStyle.VERSION ) { + applyVersionOptimisticLocking( tableMutationBuilder ); + } + } + + void applyVersionOptimisticLocking(RestrictedTableMutationBuilder tableMutationBuilder) { + final var versionMapping = entityPersister().getVersionMapping(); + if ( versionMapping != null ) { + tableMutationBuilder.addOptimisticLockRestriction( versionMapping ); + } + } + + StaleObjectStateException staleObjectStateException(Object id, StaleStateException cause) { + return new StaleObjectStateException( entityPersister().getEntityName(), id, cause ); + } + + void applyPartitionKeyRestriction(RestrictedTableMutationBuilder tableMutationBuilder) { + final var persister = entityPersister(); + if ( persister.hasPartitionedSelectionMapping() ) { + final var attributeMappings = persister.getAttributeMappings(); + for ( int m = 0; m < attributeMappings.size(); m++ ) { + final var attributeMapping = attributeMappings.get( m ); + final int jdbcTypeCount = attributeMapping.getJdbcTypeCount(); + for ( int i = 0; i < jdbcTypeCount; i++ ) { + final var selectableMapping = attributeMapping.getSelectable( i ); + if ( selectableMapping.isPartitioned() ) { + tableMutationBuilder.addKeyRestrictionLeniently( selectableMapping ); + } + } + } + } + } + + /** + * For temporal history tables and audit log tables. + */ + static EntityTableMapping createAuxiliaryTableMapping( + EntityTableMapping identifierTableMapping, + EntityPersister persister, + String tableName) { + return new EntityTableMapping( + tableName, + identifierTableMapping.getRelativePosition(), + identifierTableMapping.getKeyMapping(), + identifierTableMapping.isOptional(), + identifierTableMapping.isInverse(), + identifierTableMapping.isIdentifierTable(), + identifierTableMapping.getAttributeIndexes(), + identifierTableMapping.getInsertExpectation(), + identifierTableMapping.getInsertCustomSql(), + identifierTableMapping.isInsertCallable(), + identifierTableMapping.getUpdateExpectation(), + identifierTableMapping.getUpdateCustomSql(), + identifierTableMapping.isUpdateCallable(), + identifierTableMapping.isCascadeDeleteEnabled(), + identifierTableMapping.getDeleteExpectation(), + identifierTableMapping.getDeleteCustomSql(), + identifierTableMapping.isDeleteCallable(), + persister.isDynamicUpdate(), + persister.isDynamicInsert() + ); + }} diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/AbstractTemporalUpdateCoordinator.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/AbstractTemporalUpdateCoordinator.java new file mode 100644 index 000000000000..33709eaa64be --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/AbstractTemporalUpdateCoordinator.java @@ -0,0 +1,105 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.persister.entity.mutation; + +import org.hibernate.engine.jdbc.mutation.JdbcValueBindings; +import org.hibernate.engine.jdbc.mutation.OperationResultChecker; +import org.hibernate.engine.jdbc.mutation.ParameterUsage; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.metamodel.mapping.TemporalMapping; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.sql.ast.tree.expression.ColumnReference; +import org.hibernate.sql.model.MutationOperation; +import org.hibernate.sql.model.MutationOperationGroup; +import org.hibernate.sql.model.MutationType; +import org.hibernate.sql.model.ast.builder.TableUpdateBuilder; +import org.hibernate.sql.model.ast.builder.TableUpdateBuilderStandard; +import org.hibernate.sql.model.internal.MutationGroupSingle; + +import static org.hibernate.sql.model.internal.MutationOperationGroupFactory.singleOperation; + +/** + * @author Gavin King + */ +abstract class AbstractTemporalUpdateCoordinator extends AbstractMutationCoordinator implements UpdateCoordinator { + AbstractTemporalUpdateCoordinator(EntityPersister entityPersister, SessionFactoryImplementor factory) { + super( entityPersister, factory ); + } + + static void applyTemporalEnding(TableUpdateBuilder tableUpdateBuilder, TemporalMapping temporalMapping) { + final var endingColumnReference = + new ColumnReference( tableUpdateBuilder.getMutatingTable(), temporalMapping.getEndingColumnMapping() ); + tableUpdateBuilder.addValueColumn( temporalMapping.createEndingValueBinding( endingColumnReference ) ); + tableUpdateBuilder.addNonKeyRestriction( temporalMapping.createNullEndingValueBinding( endingColumnReference ) ); + } + + MutationOperationGroup buildEndingUpdateGroup(EntityTableMapping tableMapping, TemporalMapping temporalMapping) { + final var tableUpdateBuilder = + new TableUpdateBuilderStandard<>( entityPersister(), tableMapping, factory() ); + + applyKeyRestriction( null, entityPersister(), tableUpdateBuilder, tableMapping ); + applyTemporalEnding( tableUpdateBuilder, temporalMapping ); + applyPartitionKeyRestriction( tableUpdateBuilder ); + applyOptimisticLocking( tableUpdateBuilder ); + + return createMutationOperationGroup( tableUpdateBuilder ); + } + + MutationOperationGroup createMutationOperationGroup(TableUpdateBuilderStandard tableUpdateBuilder) { + final var tableMutation = tableUpdateBuilder.buildMutation(); + return singleOperation( + new MutationGroupSingle( MutationType.UPDATE, entityPersister(), tableMutation ), + tableMutation.createMutationOperation( null, factory() ) + ); + } + + abstract void bindVersionRestriction(Object oldVersion, JdbcValueBindings jdbcValueBindings, String temporalTableName); + + void performRowEndUpdate( + Object entity, + Object id, + Object rowId, + Object oldVersion, + SharedSessionContractImplementor session, + TemporalMapping temporalMapping, + MutationOperationGroup endUpdateGroup, + String temporalTableName, + OperationResultChecker resultChecker) { + final var mutationExecutor = + mutationExecutorService.createExecutor( resolveBatchKeyAccess( false, session ), + endUpdateGroup, session ); + try { + final var jdbcValueBindings = mutationExecutor.getJdbcValueBindings(); + for ( int i = 0; i < endUpdateGroup.getNumberOfOperations(); i++ ) { + breakDownKeyJdbcValues( id, rowId, session, jdbcValueBindings, + (EntityTableMapping) endUpdateGroup.getOperation( i ).getTableDetails() ); + } + + bindVersionRestriction( oldVersion, jdbcValueBindings, temporalTableName ); + + if ( TemporalMutationHelper.isUsingParameters( session ) ) { + jdbcValueBindings.bindValue( + session.getCurrentTransactionIdentifier(), + temporalTableName, + temporalMapping.getEndingColumnMapping().getSelectionExpression(), + ParameterUsage.SET + ); + } + + mutationExecutor.execute( + entity, + null, + null, + resultChecker, + session, + staleStateException -> staleObjectStateException( id, staleStateException ) + ); + } + finally { + mutationExecutor.release(); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/DeleteCoordinatorAudit.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/DeleteCoordinatorAudit.java new file mode 100644 index 000000000000..f4b2ce87468f --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/DeleteCoordinatorAudit.java @@ -0,0 +1,58 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.persister.entity.mutation; + +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.persister.state.internal.AuditStateManagement; +import org.hibernate.sql.model.MutationOperationGroup; + +/** + * Delete coordinator for audited entities. + */ +public class DeleteCoordinatorAudit extends AbstractAuditCoordinator implements DeleteCoordinator { + private final DeleteCoordinator currentDeleteCoordinator; + + public DeleteCoordinatorAudit( + EntityPersister entityPersister, + SessionFactoryImplementor factory, + DeleteCoordinator currentDeleteCoordinator) { + super( entityPersister, factory ); + this.currentDeleteCoordinator = currentDeleteCoordinator; + } + + @Override + public MutationOperationGroup getStaticMutationOperationGroup() { + return currentDeleteCoordinator.getStaticMutationOperationGroup(); + } + + @Override + public void delete( + Object entity, + Object id, + Object version, + SharedSessionContractImplementor session) { + currentDeleteCoordinator.delete( entity, id, version, session ); + final var state = resolveDeleteState( entity, session ); + if ( state != null ) { + insertAuditRow( entity, id, state, AuditStateManagement.ModificationType.DEL, session ); + } + } + + private Object[] resolveDeleteState(Object entity, SharedSessionContractImplementor session) { + if ( entity == null ) { + return null; + } + else { + final var persistenceContext = session.getPersistenceContextInternal(); + final var entry = persistenceContext.getEntry( entity ); + return entry != null + && entry.getLoadedState() != null + ? entry.getLoadedState() + : entityPersister().getValues( entity ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/DeleteCoordinatorHistory.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/DeleteCoordinatorHistory.java new file mode 100644 index 000000000000..5ff7f01a5ab6 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/DeleteCoordinatorHistory.java @@ -0,0 +1,139 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.persister.entity.mutation; + +import org.hibernate.engine.jdbc.batch.internal.BasicBatchKey; +import org.hibernate.engine.jdbc.batch.spi.BatchKey; +import org.hibernate.engine.jdbc.mutation.ParameterUsage; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.metamodel.mapping.TemporalMapping; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.sql.model.MutationOperationGroup; +import org.hibernate.sql.model.MutationType; +import org.hibernate.sql.model.ast.builder.TableUpdateBuilderStandard; +import org.hibernate.sql.model.internal.MutationGroupSingle; + +import static org.hibernate.persister.entity.mutation.AbstractTemporalUpdateCoordinator.applyTemporalEnding; +import static org.hibernate.sql.model.internal.MutationOperationGroupFactory.singleOperation; + +/** + * Delete coordinator for + * {@link org.hibernate.temporal.TemporalTableStrategy#HISTORY_TABLE} + * temporal strategy. + * + * @author Gavin King + */ +public class DeleteCoordinatorHistory + extends AbstractMutationCoordinator + implements DeleteCoordinator { + + private final DeleteCoordinator currentDeleteCoordinator; + private final EntityTableMapping historyTableMapping; + private final TemporalMapping temporalMapping; + private final BasicBatchKey historyBatchKey; + private final MutationOperationGroup historyEndUpdateGroup; + + public DeleteCoordinatorHistory( + EntityPersister entityPersister, + SessionFactoryImplementor factory, + DeleteCoordinator currentDeleteCoordinator) { + super( entityPersister, factory ); + this.currentDeleteCoordinator = currentDeleteCoordinator; + this.temporalMapping = entityPersister.getTemporalMapping(); + this.historyTableMapping = + createAuxiliaryTableMapping( entityPersister.getIdentifierTableMapping(), + entityPersister, temporalMapping.getTableName() ); + this.historyBatchKey = new BasicBatchKey( entityPersister.getEntityName() + "#HISTORY_DELETE" ); + this.historyEndUpdateGroup = buildHistoryEndUpdateGroup(); + } + + @Override + public MutationOperationGroup getStaticMutationOperationGroup() { + return currentDeleteCoordinator.getStaticMutationOperationGroup(); + } + + @Override + protected BatchKey getBatchKey() { + return historyBatchKey; + } + + @Override + public void delete( + Object entity, + Object id, + Object version, + SharedSessionContractImplementor session) { + currentDeleteCoordinator.delete( entity, id, version, session ); + performHistoryEndingUpdate( entity, id, version, session ); + } + + private void performHistoryEndingUpdate( + Object entity, + Object id, + Object oldVersion, + SharedSessionContractImplementor session) { + final var mutationExecutor = + mutationExecutorService.createExecutor( resolveBatchKeyAccess( false, session ), + historyEndUpdateGroup, session ); + try { + final var jdbcValueBindings = mutationExecutor.getJdbcValueBindings(); + for ( int i = 0; i < historyEndUpdateGroup.getNumberOfOperations(); i++ ) { + final var operation = historyEndUpdateGroup.getOperation( i ); + breakDownKeyJdbcValues( id, null, session, jdbcValueBindings, + (EntityTableMapping) operation.getTableDetails() ); + } + + final var versionMapping = entityPersister().getVersionMapping(); + if ( versionMapping != null && entityPersister().optimisticLockStyle().isVersion() ) { + jdbcValueBindings.bindValue( + oldVersion, + historyTableMapping.getTableName(), + versionMapping.getSelectionExpression(), + ParameterUsage.RESTRICT + ); + } + + if ( TemporalMutationHelper.isUsingParameters( session ) ) { + jdbcValueBindings.bindValue( + session.getCurrentTransactionIdentifier(), + historyTableMapping.getTableName(), + temporalMapping.getEndingColumnMapping().getSelectionExpression(), + ParameterUsage.SET + ); + } + + mutationExecutor.execute( + entity, + null, + null, + (statementDetails, affectedRowCount, batchPosition) -> + resultCheck( id, statementDetails, affectedRowCount, batchPosition ), + session, + staleStateException -> staleObjectStateException( id, staleStateException ) + ); + } + finally { + mutationExecutor.release(); + } + } + + private MutationOperationGroup buildHistoryEndUpdateGroup() { + final var tableUpdateBuilder = + new TableUpdateBuilderStandard<>( entityPersister(), historyTableMapping, factory() ); + + applyKeyRestriction( null, entityPersister(), tableUpdateBuilder, historyTableMapping ); + applyTemporalEnding( tableUpdateBuilder, temporalMapping ); + if ( entityPersister().optimisticLockStyle().isVersion() ) { + applyVersionOptimisticLocking( tableUpdateBuilder ); + } + + final var tableMutation = tableUpdateBuilder.buildMutation(); + return singleOperation( + new MutationGroupSingle( MutationType.DELETE, entityPersister(), tableMutation ), + tableMutation.createMutationOperation( null, factory() ) + ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/DeleteCoordinatorSoft.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/DeleteCoordinatorSoft.java index 7630096018ea..673d3abbd09b 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/DeleteCoordinatorSoft.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/DeleteCoordinatorSoft.java @@ -4,21 +4,14 @@ */ package org.hibernate.persister.entity.mutation; -import org.hibernate.engine.OptimisticLockStyle; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor; -import org.hibernate.metamodel.mapping.AttributeMapping; import org.hibernate.metamodel.mapping.SoftDeleteMapping; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.sql.ast.tree.expression.ColumnReference; import org.hibernate.sql.model.MutationOperation; import org.hibernate.sql.model.MutationOperationGroup; -import org.hibernate.sql.model.MutationType; -import org.hibernate.sql.model.ast.builder.TableUpdateBuilder; import org.hibernate.sql.model.ast.builder.TableUpdateBuilderStandard; -import org.hibernate.sql.model.internal.MutationGroupSingle; - -import static org.hibernate.sql.model.internal.MutationOperationGroupFactory.singleOperation; /** * DeleteCoordinator for soft-deletes @@ -37,129 +30,29 @@ protected MutationOperationGroup generateOperationGroup( boolean applyVersion, SharedSessionContractImplementor session) { final var rootTableMapping = entityPersister().getIdentifierTableMapping(); - final TableUpdateBuilderStandard tableUpdateBuilder = new TableUpdateBuilderStandard<>( - entityPersister(), - rootTableMapping, - factory() - ); + final var tableUpdateBuilder = new TableUpdateBuilderStandard<>( entityPersister(), rootTableMapping, factory() ); applyKeyRestriction( rowId, entityPersister(), tableUpdateBuilder, rootTableMapping ); applySoftDelete( entityPersister().getSoftDeleteMapping(), tableUpdateBuilder ); - applyPartitionKeyRestriction( tableUpdateBuilder ); - applyOptimisticLocking( tableUpdateBuilder, loadedState, session ); - - final var tableMutation = tableUpdateBuilder.buildMutation(); - final MutationGroupSingle mutationGroup = new MutationGroupSingle( - MutationType.DELETE, - entityPersister(), - tableMutation + applyPartitionKeyRestriction( tableName -> tableUpdateBuilder ); + applyOptimisticLocking( + entityPersister().optimisticLockStyle(), + tableMutationBuilderResolver( tableUpdateBuilder ), + loadedState, + session ); - final var mutationOperation = tableMutation.createMutationOperation( null, factory() ); - return singleOperation( mutationGroup, mutationOperation ); - } - - private void applyPartitionKeyRestriction(TableUpdateBuilder tableUpdateBuilder) { - final var persister = entityPersister(); - if ( persister.hasPartitionedSelectionMapping() ) { - final var attributeMappings = persister.getAttributeMappings(); - for ( int m = 0; m < attributeMappings.size(); m++ ) { - final var attributeMapping = attributeMappings.get( m ); - final int jdbcTypeCount = attributeMapping.getJdbcTypeCount(); - for ( int i = 0; i < jdbcTypeCount; i++ ) { - final var selectableMapping = attributeMapping.getSelectable( i ); - if ( selectableMapping.isPartitioned() ) { - tableUpdateBuilder.addKeyRestrictionLeniently( selectableMapping ); - } - } - } - } + return createMutationOperationGroup( tableUpdateBuilder ); } - private void applySoftDelete( + private static void applySoftDelete( SoftDeleteMapping softDeleteMapping, TableUpdateBuilderStandard tableUpdateBuilder) { - final var softDeleteColumnReference = new ColumnReference( tableUpdateBuilder.getMutatingTable(), softDeleteMapping ); - + final var softDeleteColumnReference = + new ColumnReference( tableUpdateBuilder.getMutatingTable(), softDeleteMapping ); // apply the assignment tableUpdateBuilder.addValueColumn( softDeleteMapping.createDeletedValueBinding( softDeleteColumnReference ) ); // apply the restriction tableUpdateBuilder.addNonKeyRestriction( softDeleteMapping.createNonDeletedValueBinding( softDeleteColumnReference ) ); } - - protected void applyOptimisticLocking( - TableUpdateBuilderStandard tableUpdateBuilder, - Object[] loadedState, - SharedSessionContractImplementor session) { - final var persister = entityPersister(); - final var optimisticLockStyle = persister.optimisticLockStyle(); - if ( optimisticLockStyle.isVersion() && persister.getVersionMapping() != null ) { - applyVersionBasedOptLocking( tableUpdateBuilder ); - } - else if ( loadedState != null && persister.optimisticLockStyle().isAllOrDirty() ) { - applyNonVersionOptLocking( - optimisticLockStyle, - tableUpdateBuilder, - loadedState, - session - ); - } - } - - protected void applyVersionBasedOptLocking(TableUpdateBuilderStandard tableUpdateBuilder) { - assert entityPersister().optimisticLockStyle() == OptimisticLockStyle.VERSION; - assert entityPersister().getVersionMapping() != null; - - tableUpdateBuilder.addOptimisticLockRestriction( entityPersister().getVersionMapping() ); - } - - protected void applyNonVersionOptLocking( - OptimisticLockStyle lockStyle, - TableUpdateBuilderStandard tableUpdateBuilder, - Object[] loadedState, - SharedSessionContractImplementor session) { - final var persister = entityPersister(); - assert loadedState != null; - assert lockStyle.isAllOrDirty(); - assert persister.optimisticLockStyle().isAllOrDirty(); - assert session != null; - - final boolean[] versionability = persister.getPropertyVersionability(); - for ( int attributeIndex = 0; attributeIndex < versionability.length; attributeIndex++ ) { - // only makes sense to lock on singular attributes which are not excluded from optimistic locking - if ( versionability[attributeIndex] ) { - final var attribute = persister.getAttributeMapping( attributeIndex ); - if ( !attribute.isPluralAttributeMapping() ) { - breakDownJdbcValues( tableUpdateBuilder, session, attribute, loadedState[attributeIndex] ); - } - } - } - } - - private void breakDownJdbcValues( - TableUpdateBuilderStandard tableUpdateBuilder, - SharedSessionContractImplementor session, - AttributeMapping attribute, - Object loadedValue) { - if ( tableUpdateBuilder.getMutatingTable().getTableName() - .equals( attribute.getContainingTableExpression() ) ) { - final var optimisticLockBindings = tableUpdateBuilder.getOptimisticLockBindings(); - if ( optimisticLockBindings != null ) { - attribute.breakDownJdbcValues( - loadedValue, - (valueIndex, value, jdbcValueMapping) -> { - if ( !tableUpdateBuilder.getKeyRestrictionBindings() - .containsColumn( - jdbcValueMapping.getSelectableName(), - jdbcValueMapping.getJdbcMapping() - ) ) { - optimisticLockBindings.consume( valueIndex, value, jdbcValueMapping ); - } - }, - session - ); - } - } - // else if it is not on the root table, skip it - } } diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/DeleteCoordinatorStandard.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/DeleteCoordinatorStandard.java index 7eeb0affec71..e455ec60f85b 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/DeleteCoordinatorStandard.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/DeleteCoordinatorStandard.java @@ -4,15 +4,12 @@ */ package org.hibernate.persister.entity.mutation; -import org.hibernate.engine.OptimisticLockStyle; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor; -import org.hibernate.metamodel.mapping.AttributeMapping; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.sql.model.MutationOperationGroup; import org.hibernate.sql.model.MutationType; import org.hibernate.sql.model.ast.builder.MutationGroupBuilder; -import org.hibernate.sql.model.ast.builder.RestrictedTableMutationBuilder; import org.hibernate.sql.model.ast.builder.TableDeleteBuilder; import org.hibernate.sql.model.ast.builder.TableDeleteBuilderSkipped; import org.hibernate.sql.model.ast.builder.TableDeleteBuilderStandard; @@ -36,12 +33,10 @@ protected MutationOperationGroup generateOperationGroup( SharedSessionContractImplementor session) { final var deleteGroupBuilder = new MutationGroupBuilder( MutationType.DELETE, entityPersister() ); - entityPersister().forEachMutableTableReverse( (tableMapping) -> { - final var tableDeleteBuilder = tableMapping.isCascadeDeleteEnabled() - ? new TableDeleteBuilderSkipped( tableMapping ) - : new TableDeleteBuilderStandard( entityPersister(), tableMapping, factory() ); - deleteGroupBuilder.addTableDetailsBuilder( tableDeleteBuilder ); - } ); + entityPersister().forEachMutableTableReverse( tableMapping -> + deleteGroupBuilder.addTableDetailsBuilder( tableMapping.isCascadeDeleteEnabled() + ? new TableDeleteBuilderSkipped( tableMapping ) + : new TableDeleteBuilderStandard( entityPersister(), tableMapping, factory() ) ) ); applyTableDeleteDetails( deleteGroupBuilder, rowId, loadedState, applyVersion, session ); @@ -55,113 +50,19 @@ private void applyTableDeleteDetails( boolean applyVersion, SharedSessionContractImplementor session) { // first, the table key column(s) - deleteGroupBuilder.forEachTableMutationBuilder( (builder) -> { - final var tableMapping = (EntityTableMapping) builder.getMutatingTable().getTableMapping(); - final var tableDeleteBuilder = (TableDeleteBuilder) builder; - applyKeyRestriction( rowId, entityPersister(), tableDeleteBuilder, tableMapping ); - } ); + deleteGroupBuilder.forEachTableMutationBuilder( builder -> + applyKeyRestriction( rowId, entityPersister(), (TableDeleteBuilder) builder, + (EntityTableMapping) builder.getMutatingTable().getTableMapping() ) ); if ( applyVersion ) { // apply any optimistic locking - applyOptimisticLocking( deleteGroupBuilder, loadedState, session ); - final var persister = entityPersister(); - if ( persister.hasPartitionedSelectionMapping() ) { - final var attributeMappings = persister.getAttributeMappings(); - for ( int m = 0; m < attributeMappings.size(); m++ ) { - final var attributeMapping = attributeMappings.get( m ); - final int jdbcTypeCount = attributeMapping.getJdbcTypeCount(); - for ( int i = 0; i < jdbcTypeCount; i++ ) { - final var selectableMapping = attributeMapping.getSelectable( i ); - if ( selectableMapping.isPartitioned() ) { - final String tableNameForMutation = - persister.physicalTableNameForMutation( selectableMapping ); - final RestrictedTableMutationBuilder rootTableMutationBuilder = - deleteGroupBuilder.findTableDetailsBuilder( tableNameForMutation ); - rootTableMutationBuilder.addKeyRestrictionLeniently( selectableMapping ); - } - } - } - } - } - } - - protected void applyOptimisticLocking( - MutationGroupBuilder mutationGroupBuilder, - Object[] loadedState, - SharedSessionContractImplementor session) { - final var optimisticLockStyle = entityPersister().optimisticLockStyle(); - if ( optimisticLockStyle.isVersion() && entityPersister().getVersionMapping() != null ) { - applyVersionBasedOptLocking( mutationGroupBuilder ); - } - else if ( loadedState != null && entityPersister().optimisticLockStyle().isAllOrDirty() ) { - applyNonVersionOptLocking( - optimisticLockStyle, - mutationGroupBuilder, + applyOptimisticLocking( + entityPersister().optimisticLockStyle(), + deleteGroupBuilder::findTableDetailsBuilder, loadedState, session ); - } - } - - protected void applyVersionBasedOptLocking(MutationGroupBuilder mutationGroupBuilder) { - assert entityPersister().optimisticLockStyle() == OptimisticLockStyle.VERSION; - assert entityPersister().getVersionMapping() != null; - - final String tableNameForMutation = - entityPersister().physicalTableNameForMutation( entityPersister().getVersionMapping() ); - final RestrictedTableMutationBuilder rootTableMutationBuilder = - mutationGroupBuilder.findTableDetailsBuilder( tableNameForMutation ); - rootTableMutationBuilder.addOptimisticLockRestriction( entityPersister().getVersionMapping() ); - } - - protected void applyNonVersionOptLocking( - OptimisticLockStyle lockStyle, - MutationGroupBuilder mutationGroupBuilder, - Object[] loadedState, - SharedSessionContractImplementor session) { - final var persister = entityPersister(); - assert loadedState != null; - assert lockStyle.isAllOrDirty(); - assert persister.optimisticLockStyle().isAllOrDirty(); - assert session != null; - - final boolean[] versionability = persister.getPropertyVersionability(); - for ( int attributeIndex = 0; attributeIndex < versionability.length; attributeIndex++ ) { - // only makes sense to lock on singular attributes which are not excluded from optimistic locking - if ( versionability[attributeIndex] ) { - final var attribute = persister.getAttributeMapping( attributeIndex ); - if ( !attribute.isPluralAttributeMapping() ) { - breakDownJdbcValues( mutationGroupBuilder, session, attribute, loadedState[attributeIndex] ); - } - } - } - } - - private void breakDownJdbcValues( - MutationGroupBuilder mutationGroupBuilder, - SharedSessionContractImplementor session, - AttributeMapping attribute, - Object loadedValue) { - final RestrictedTableMutationBuilder tableMutationBuilder = - mutationGroupBuilder.findTableDetailsBuilder( attribute.getContainingTableExpression() ); - if ( tableMutationBuilder != null ) { - final var optimisticLockBindings = tableMutationBuilder.getOptimisticLockBindings(); - if ( optimisticLockBindings != null ) { - attribute.breakDownJdbcValues( - loadedValue, - (valueIndex, value, jdbcValueMapping) -> { - if ( !tableMutationBuilder.getKeyRestrictionBindings() - .containsColumn( - jdbcValueMapping.getSelectableName(), - jdbcValueMapping.getJdbcMapping() - ) ) { - optimisticLockBindings.consume( valueIndex, value, jdbcValueMapping ); - } - } - , - session - ); - } + applyPartitionKeyRestriction( deleteGroupBuilder::findTableDetailsBuilder ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/DeleteCoordinatorTemporal.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/DeleteCoordinatorTemporal.java new file mode 100644 index 000000000000..430c796156cd --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/DeleteCoordinatorTemporal.java @@ -0,0 +1,93 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.persister.entity.mutation; + +import org.hibernate.engine.jdbc.mutation.JdbcValueBindings; +import org.hibernate.engine.jdbc.mutation.MutationExecutor; +import org.hibernate.engine.jdbc.mutation.ParameterUsage; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.metamodel.mapping.TemporalMapping; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.sql.model.MutationOperationGroup; +import org.hibernate.sql.model.ast.builder.TableUpdateBuilderStandard; + +import static org.hibernate.persister.entity.mutation.AbstractTemporalUpdateCoordinator.applyTemporalEnding; + +/** + * Delete coordinator for + * {@link org.hibernate.temporal.TemporalTableStrategy#SINGLE_TABLE} + * temporal strategy. + * + * @author Gavin King + */ +public class DeleteCoordinatorTemporal extends AbstractDeleteCoordinator { + private final TemporalMapping temporalMapping; + + public DeleteCoordinatorTemporal(EntityPersister entityPersister, SessionFactoryImplementor factory) { + super( entityPersister, factory ); + this.temporalMapping = entityPersister.getTemporalMapping(); + } + + @Override + protected void applyStaticDeleteTableDetails( + Object id, + Object rowId, + Object[] loadedState, + Object version, + boolean applyVersion, + MutationExecutor mutationExecutor, + SharedSessionContractImplementor session) { + super.applyStaticDeleteTableDetails( id, rowId, loadedState, version, applyVersion, mutationExecutor, session ); + bindTemporalEndingValue( session, mutationExecutor.getJdbcValueBindings() ); + } + + @Override + protected void applyDynamicDeleteTableDetails( + Object id, + Object rowId, + Object[] loadedState, + MutationExecutor mutationExecutor, + MutationOperationGroup operationGroup, + SharedSessionContractImplementor session) { + super.applyDynamicDeleteTableDetails( id, rowId, loadedState, mutationExecutor, operationGroup, session ); + bindTemporalEndingValue( session, mutationExecutor.getJdbcValueBindings() ); + } + + private void bindTemporalEndingValue( + SharedSessionContractImplementor session, + JdbcValueBindings jdbcValueBindings) { + if ( TemporalMutationHelper.isUsingParameters( session ) ) { + jdbcValueBindings.bindValue( + session.getCurrentTransactionIdentifier(), + entityPersister().physicalTableNameForMutation( temporalMapping.getEndingColumnMapping() ), + temporalMapping.getEndingColumnMapping().getSelectionExpression(), + ParameterUsage.SET + ); + } + } + + @Override + protected MutationOperationGroup generateOperationGroup( + Object rowId, + Object[] loadedState, + boolean applyVersion, + SharedSessionContractImplementor session) { + final var rootTableMapping = entityPersister().getIdentifierTableMapping(); + final var tableUpdateBuilder = new TableUpdateBuilderStandard<>( entityPersister(), rootTableMapping, factory() ); + + applyKeyRestriction( rowId, entityPersister(), tableUpdateBuilder, rootTableMapping ); + applyTemporalEnding( tableUpdateBuilder, entityPersister().getTemporalMapping() ); + applyPartitionKeyRestriction( tableName -> tableUpdateBuilder ); + applyOptimisticLocking( + entityPersister().optimisticLockStyle(), + tableMutationBuilderResolver( tableUpdateBuilder ), + loadedState, + session + ); + + return createMutationOperationGroup( tableUpdateBuilder ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/EntityMutationTarget.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/EntityMutationTarget.java index 04b78e26dc71..568781a9d8da 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/EntityMutationTarget.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/EntityMutationTarget.java @@ -42,7 +42,7 @@ public interface EntityMutationTarget extends MutationTarget void addDiscriminatorToInsertGroup(MutationGroupBuilder insertGroupBuilder); - void addSoftDeleteToInsertGroup(MutationGroupBuilder insertGroupBuilder); + void addAuxiliaryToInsertGroup(MutationGroupBuilder insertGroupBuilder); /** * The name of the table to use when performing mutations (INSERT,UPDATE,DELETE) diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/InsertCoordinatorAudit.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/InsertCoordinatorAudit.java new file mode 100644 index 000000000000..96d0bed60f45 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/InsertCoordinatorAudit.java @@ -0,0 +1,53 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.persister.entity.mutation; + +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.generator.values.GeneratedValues; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.persister.state.internal.AuditStateManagement; +import org.hibernate.sql.model.MutationOperationGroup; + +/** + * Insert coordinator for audited entities. + */ +public class InsertCoordinatorAudit extends AbstractAuditCoordinator implements InsertCoordinator { + private final InsertCoordinator currentInsertCoordinator; + + public InsertCoordinatorAudit( + EntityPersister entityPersister, + SessionFactoryImplementor factory, + InsertCoordinator currentInsertCoordinator) { + super( entityPersister, factory ); + this.currentInsertCoordinator = currentInsertCoordinator; + } + + @Override + public MutationOperationGroup getStaticMutationOperationGroup() { + return currentInsertCoordinator.getStaticMutationOperationGroup(); + } + + @Override + public GeneratedValues insert( + Object entity, + Object[] values, + SharedSessionContractImplementor session) { + final var generatedValues = currentInsertCoordinator.insert( entity, values, session ); + insertAuditRow( entity, null, values, AuditStateManagement.ModificationType.ADD, session ); + return generatedValues; + } + + @Override + public GeneratedValues insert( + Object entity, + Object id, + Object[] values, + SharedSessionContractImplementor session) { + final var generatedValues = currentInsertCoordinator.insert( entity, id, values, session ); + insertAuditRow( entity, id, values, AuditStateManagement.ModificationType.ADD, session ); + return generatedValues; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/InsertCoordinatorHistory.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/InsertCoordinatorHistory.java new file mode 100644 index 000000000000..b4797b55fec0 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/InsertCoordinatorHistory.java @@ -0,0 +1,258 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.persister.entity.mutation; + +import java.sql.SQLException; + +import org.hibernate.engine.jdbc.batch.internal.BasicBatchKey; +import org.hibernate.engine.jdbc.batch.spi.BatchKey; +import org.hibernate.engine.jdbc.mutation.JdbcValueBindings; +import org.hibernate.engine.jdbc.mutation.ParameterUsage; +import org.hibernate.engine.jdbc.mutation.group.PreparedStatementDetails; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.generator.Generator; +import org.hibernate.generator.OnExecutionGenerator; +import org.hibernate.generator.values.GeneratedValues; +import org.hibernate.metamodel.mapping.AttributeMapping; +import org.hibernate.metamodel.mapping.PluralAttributeMapping; +import org.hibernate.metamodel.mapping.TemporalMapping; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.sql.ast.tree.expression.ColumnReference; +import org.hibernate.sql.model.MutationOperationGroup; +import org.hibernate.sql.model.MutationType; +import org.hibernate.sql.model.ast.builder.TableInsertBuilderStandard; +import org.hibernate.sql.model.internal.MutationGroupSingle; + +import static org.hibernate.persister.entity.mutation.InsertCoordinatorStandard.getPropertiesToInsert; +import static org.hibernate.sql.model.internal.MutationOperationGroupFactory.singleOperation; + +/** + * Insert coordinator for + * {@link org.hibernate.temporal.TemporalTableStrategy#HISTORY_TABLE} + * temporal strategy. + * + * @author Gavin King + */ +public class InsertCoordinatorHistory extends AbstractMutationCoordinator implements InsertCoordinator { + private final InsertCoordinator currentInsertCoordinator; + private final EntityTableMapping identifierTableMapping; + private final EntityTableMapping historyTableMapping; + private final TemporalMapping temporalMapping; + private final BasicBatchKey historyBatchKey; + private final MutationOperationGroup staticHistoryInsertGroup; + + public InsertCoordinatorHistory( + EntityPersister entityPersister, + SessionFactoryImplementor factory, + InsertCoordinator currentInsertCoordinator) { + super( entityPersister, factory ); + this.currentInsertCoordinator = currentInsertCoordinator; + identifierTableMapping = entityPersister.getIdentifierTableMapping(); + temporalMapping = entityPersister.getTemporalMapping(); + historyTableMapping = + createAuxiliaryTableMapping( identifierTableMapping, + entityPersister, temporalMapping.getTableName() + ); + historyBatchKey = new BasicBatchKey( entityPersister.getEntityName() + "#HISTORY_INSERT" ); + staticHistoryInsertGroup = entityPersister.isDynamicInsert() + ? null + : buildHistoryInsertGroup( entityPersister.getPropertyInsertability(), null, null ); + } + + @Override + public MutationOperationGroup getStaticMutationOperationGroup() { + return currentInsertCoordinator.getStaticMutationOperationGroup(); + } + + @Override + protected BatchKey getBatchKey() { + return historyBatchKey; + } + + @Override + public GeneratedValues insert(Object entity, Object[] values, SharedSessionContractImplementor session) { + final var generatedValues = currentInsertCoordinator.insert( entity, values, session ); + final Object id = entityPersister().getIdentifier( entity, session ); + insertHistoryRow( entity, id, values, session ); + return generatedValues; + } + + @Override + public GeneratedValues insert( + Object entity, + Object id, + Object[] values, + SharedSessionContractImplementor session) { + final var generatedValues = currentInsertCoordinator.insert( entity, id, values, session ); + final Object resolvedId = id == null ? entityPersister().getIdentifier( entity, session ) : id; + insertHistoryRow( entity, resolvedId, values, session ); + return generatedValues; + } + + private void insertHistoryRow( + Object entity, + Object id, + Object[] values, + SharedSessionContractImplementor session) { + final boolean dynamicInsert = entityPersister().isDynamicInsert(); + final boolean[] propertyInclusions = dynamicInsert + ? getPropertiesToInsert( entityPersister(), values ) + : entityPersister().getPropertyInsertability(); + final var operationGroup = dynamicInsert + ? buildHistoryInsertGroup( propertyInclusions, entity, session ) + : staticHistoryInsertGroup; + + final var mutationExecutor = + mutationExecutorService.createExecutor( resolveBatchKeyAccess( dynamicInsert, session ), + operationGroup, session ); + try { + bindHistoryValues( id, values, propertyInclusions, session, mutationExecutor.getJdbcValueBindings() ); + mutationExecutor.execute( entity, null, null, InsertCoordinatorHistory::verifyOutcome, session ); + } + finally { + mutationExecutor.release(); + } + } + + private MutationOperationGroup buildHistoryInsertGroup( + boolean[] propertyInclusions, + Object entity, + SharedSessionContractImplementor session) { + final var insertBuilder = + new TableInsertBuilderStandard( entityPersister(), historyTableMapping, factory() ); + applyHistoryInsertDetails( insertBuilder, propertyInclusions, entity, session ); + final var tableMutation = insertBuilder.buildMutation(); + return singleOperation( + new MutationGroupSingle( MutationType.INSERT, entityPersister(), tableMutation ), + tableMutation.createMutationOperation( null, factory() ) + ); + } + + private void applyHistoryInsertDetails( + TableInsertBuilderStandard insertBuilder, + boolean[] propertyInclusions, + Object entity, + SharedSessionContractImplementor session) { + final var attributeMappings = entityPersister().getAttributeMappings(); + for ( final int attributeIndex : identifierTableMapping.getAttributeIndexes() ) { + final var attributeMapping = attributeMappings.get( attributeIndex ); + if ( propertyInclusions[attributeIndex] ) { + attributeMapping.forEachInsertable( insertBuilder ); + } + else { + final var generator = attributeMapping.getGenerator(); + if ( isValueGenerated( generator ) ) { + if ( session != null && generator.generatedBeforeExecution( entity, session ) ) { + propertyInclusions[attributeIndex] = true; + attributeMapping.forEachInsertable( insertBuilder ); + } + else if ( isValueGenerationInSql( generator ) ) { + addSqlGeneratedValue( insertBuilder, attributeMapping, (OnExecutionGenerator) generator ); + } + } + } + } + + final var mutatingTable = insertBuilder.getMutatingTable(); + final var startingColumn = new ColumnReference( mutatingTable, temporalMapping.getStartingColumnMapping() ); + insertBuilder.addValueColumn( temporalMapping.createStartingValueBinding( startingColumn ) ); + final var endingColumn = new ColumnReference( mutatingTable, temporalMapping.getEndingColumnMapping() ); + insertBuilder.addValueColumn( temporalMapping.createNullEndingValueBinding( endingColumn ) ); + + identifierTableMapping.getKeyMapping().forEachKeyColumn( insertBuilder::addKeyColumn ); + } + + private void addSqlGeneratedValue( + TableInsertBuilderStandard insertBuilder, + AttributeMapping attributeMapping, + OnExecutionGenerator generator) { + final boolean writePropertyValue = generator.writePropertyValue(); + final var columnValues = + writePropertyValue + ? null + : generator.getReferencedColumnValues( factory.getJdbcServices().getDialect() ); + attributeMapping.forEachSelectable( (j, mapping) -> + insertBuilder.addValueColumn( writePropertyValue ? "?" : columnValues[j], mapping ) ); + } + + private void bindHistoryValues( + Object id, + Object[] values, + boolean[] propertyInclusions, + SharedSessionContractImplementor session, + JdbcValueBindings jdbcValueBindings) { + final String historyTableName = historyTableMapping.getTableName(); + historyTableMapping.getKeyMapping().breakDownKeyJdbcValues( + id, + (jdbcValue, columnMapping) -> jdbcValueBindings.bindValue( + jdbcValue, + historyTableName, + columnMapping.getColumnName(), + ParameterUsage.SET + ), + session + ); + + final var attributeMappings = entityPersister().getAttributeMappings(); + for ( final int attributeIndex : identifierTableMapping.getAttributeIndexes() ) { + if ( propertyInclusions[attributeIndex] ) { + final var attributeMapping = attributeMappings.get( attributeIndex ); + if ( !(attributeMapping instanceof PluralAttributeMapping) ) { + attributeMapping.decompose( + values[attributeIndex], + 0, + jdbcValueBindings, + null, + (valueIndex, bindings, noop, jdbcValue, selectableMapping) -> { + if ( selectableMapping.isInsertable() && !selectableMapping.isFormula() ) { + bindings.bindValue( + jdbcValue, + historyTableName, + selectableMapping.getSelectionExpression(), + ParameterUsage.SET + ); + } + }, + session + ); + } + } + } + + if ( TemporalMutationHelper.isUsingParameters( session ) ) { + jdbcValueBindings.bindValue( + session.getCurrentTransactionIdentifier(), + historyTableName, + temporalMapping.getStartingColumnMapping().getSelectionExpression(), + ParameterUsage.SET + ); + } + } + + private static boolean isValueGenerated(Generator generator) { + return generator != null + && generator.generatesOnInsert() + && generator.generatedOnExecution(); + } + + private boolean isValueGenerationInSql(Generator generator) { + assert isValueGenerated( generator ); + return ( (OnExecutionGenerator) generator ).referenceColumnsInSql( dialect() ); + } + + private static boolean verifyOutcome( + PreparedStatementDetails statementDetails, + int affectedRowCount, + int batchPosition) throws SQLException { + statementDetails.getExpectation().verifyOutcome( + affectedRowCount, + statementDetails.getStatement(), + batchPosition, + statementDetails.getSqlString() + ); + return true; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/InsertCoordinatorStandard.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/InsertCoordinatorStandard.java index 9f2354a8ac3b..45b5497dcb38 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/InsertCoordinatorStandard.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/InsertCoordinatorStandard.java @@ -6,6 +6,7 @@ import java.sql.SQLException; import java.util.ArrayList; +import java.util.IdentityHashMap; import java.util.List; import org.hibernate.Internal; @@ -20,12 +21,14 @@ import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.generator.BeforeExecutionGenerator; +import org.hibernate.generator.EventType; import org.hibernate.generator.Generator; import org.hibernate.generator.OnExecutionGenerator; import org.hibernate.generator.values.GeneratedValues; +import org.hibernate.id.CompositeNestedGeneratedValueGenerator; import org.hibernate.metamodel.mapping.AttributeMapping; -import org.hibernate.metamodel.mapping.AttributeMappingsList; import org.hibernate.metamodel.mapping.PluralAttributeMapping; +import org.hibernate.metamodel.mapping.TableDetails; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.sql.model.MutationOperationGroup; import org.hibernate.sql.model.MutationType; @@ -144,9 +147,8 @@ public static class InsertValuesAnalysis implements ValuesAnalysis { public InsertValuesAnalysis(EntityMutationTarget mutationTarget, Object[] values) { mutationTarget.forEachMutableTable( (tableMapping) -> { - final int[] tableAttributeIndexes = tableMapping.getAttributeIndexes(); - for ( int i = 0; i < tableAttributeIndexes.length; i++ ) { - if ( values[tableAttributeIndexes[i]] != null ) { + for ( int tableAttributeIndex : tableMapping.getAttributeIndexes() ) { + if ( values[tableAttributeIndex] != null ) { tablesWithNonNullValues.add( tableMapping ); break; } @@ -170,6 +172,7 @@ protected GeneratedValues doStaticInserts(Object id, Object[] values, Object obj mutationExecutor, id, values, + object, staticInsertGroup, entityPersister().getPropertyInsertability(), tableInclusionChecker, @@ -194,6 +197,7 @@ protected void decomposeForInsert( MutationExecutor mutationExecutor, Object id, Object[] values, + Object object, MutationOperationGroup mutationGroup, boolean[] propertyInclusions, TableInclusionChecker tableInclusionChecker, @@ -205,12 +209,17 @@ protected void decomposeForInsert( final var operation = mutationGroup.getOperation( position ); final var tableDetails = (EntityTableMapping) operation.getTableDetails(); if ( tableInclusionChecker.include( tableDetails ) ) { - final int[] attributeIndexes = tableDetails.getAttributeIndexes(); - for ( int i = 0; i < attributeIndexes.length; i++ ) { - final int attributeIndex = attributeIndexes[ i ]; + for ( final int attributeIndex : tableDetails.getAttributeIndexes() ) { if ( propertyInclusions[attributeIndex] ) { - decomposeAttribute( values[attributeIndex], session, jdbcValueBindings, - attributeMappings.get( attributeIndex ) ); + final var attributeMapping = attributeMappings.get( attributeIndex ); + decomposeAttribute( + values[attributeIndex], + session, + jdbcValueBindings, + attributeMapping, + attributeMapping.getGenerator(), + object + ); } } } @@ -218,6 +227,7 @@ protected void decomposeForInsert( if ( id == null ) { assert entityPersister().getInsertDelegate() != null; + bindGeneratedIdentifierJdbcValues( object, session, jdbcValueBindings, mutationGroup ); } else { for ( int position = 0; position < mutationGroup.getNumberOfOperations(); position++ ) { @@ -228,6 +238,52 @@ protected void decomposeForInsert( } } + private void bindGeneratedIdentifierJdbcValues( + Object entity, + SharedSessionContractImplementor session, + JdbcValueBindings jdbcValueBindings, + MutationOperationGroup mutationGroup) { + if ( entityPersister().getGenerator() + instanceof CompositeNestedGeneratedValueGenerator compositeGenerator ) { + final boolean[] columnInclusions = + compositeGenerator.getColumnInclusions( dialect(), EventType.INSERT ); + final String[] columnValues = + compositeGenerator.getReferencedColumnValues( dialect(), EventType.INSERT ); + final boolean bindAllIncluded = + columnValues == null && compositeGenerator.writePropertyValue( EventType.INSERT ); + if ( bindAllIncluded || hasParameterMarkers( columnValues, columnInclusions ) ) { + final Object idToBind = entityPersister().getIdentifier( entity, session ); + if ( idToBind != null ) { + for ( int position = 0; position < mutationGroup.getNumberOfOperations(); position++ ) { + breakDownJdbcValue( + idToBind, + session, + jdbcValueBindings, + (EntityTableMapping) + mutationGroup.getOperation( position ) + .getTableDetails(), + columnInclusions, + columnValues, + bindAllIncluded + ); + } + } + } + } + } + + private static boolean hasParameterMarkers(String[] columnValues, boolean[] columnInclusions) { + if ( columnValues != null ) { + for ( int i = 0; i < columnValues.length; i++ ) { + if ( (columnInclusions == null || i >= columnInclusions.length || columnInclusions[i]) + && "?".equals( columnValues[i] ) ) { + return true; + } + } + } + return false; + } + protected void breakDownJdbcValue( Object id, SharedSessionContractImplementor session, @@ -248,19 +304,93 @@ protected void breakDownJdbcValue( ); } + protected void breakDownJdbcValue( + Object id, + SharedSessionContractImplementor session, + JdbcValueBindings jdbcValueBindings, + EntityTableMapping tableDetails, + boolean[] columnInclusions, + String[] columnValues, + boolean bindAllIncluded) { + final String tableName = tableDetails.getTableName(); + final var keyMapping = tableDetails.getKeyMapping(); + final var keyColumns = keyMapping.getKeyColumns(); + final var keyColumnIndex = + new IdentityHashMap( keyColumns.size() ); + for ( int i = 0; i < keyColumns.size(); i++ ) { + keyColumnIndex.put( keyColumns.get( i ), i ); + } + keyMapping.breakDownKeyJdbcValues( + id, + (jdbcValue, columnMapping) -> { + final Integer index = keyColumnIndex.get( columnMapping ); + if ( index != null + && shouldBindKeyColumn( index, columnInclusions, columnValues, bindAllIncluded ) ) { + jdbcValueBindings.bindValue( + jdbcValue, + tableName, + columnMapping.getColumnName(), + ParameterUsage.SET + ); + } + }, + session + ); + } + + private static boolean shouldBindKeyColumn( + int index, + boolean[] columnInclusions, + String[] columnValues, + boolean bindAllIncluded) { + if ( columnInclusions != null + && ( index >= columnInclusions.length || !columnInclusions[index] ) ) { + return false; + } + else if ( columnValues == null ) { + return bindAllIncluded; + } + else { + return index < columnValues.length + && "?".equals( columnValues[index] ); + } + } + protected void decomposeAttribute( Object value, SharedSessionContractImplementor session, JdbcValueBindings jdbcValueBindings, - AttributeMapping mapping) { + AttributeMapping mapping, + Generator generator, + Object entity) { if ( !(mapping instanceof PluralAttributeMapping) ) { + final OnExecutionGenerator onExecutionGenerator; + final String[] columnValues; + final boolean[] columnInclusions; + final boolean bindAllValues; + if ( generator instanceof OnExecutionGenerator executionGenerator + && generator.generatedOnExecution( entity, session ) + && generator.generatesOnInsert() ) { + onExecutionGenerator = executionGenerator; + columnValues = onExecutionGenerator.getReferencedColumnValues( dialect(), EventType.INSERT ); + columnInclusions = onExecutionGenerator.getColumnInclusions( dialect(), EventType.INSERT ); + bindAllValues = onExecutionGenerator.writePropertyValue( EventType.INSERT ) && columnValues == null; + } + else { + onExecutionGenerator = null; + columnValues = null; + columnInclusions = null; + bindAllValues = false; + } + mapping.decompose( value, 0, jdbcValueBindings, null, (valueIndex, bindings, noop, jdbcValue, selectableMapping) -> { - if ( selectableMapping.isInsertable() ) { + if ( selectableMapping.isInsertable() + && shouldBindValue( onExecutionGenerator, columnValues, columnInclusions, bindAllValues, valueIndex ) ) { bindings.bindValue( jdbcValue, entityPersister().physicalTableNameForMutation( selectableMapping ), @@ -274,19 +404,37 @@ protected void decomposeAttribute( } } + private static boolean shouldBindValue( + OnExecutionGenerator onExecutionGenerator, + String[] columnValues, + boolean[] columnInclusions, + boolean bindAllValues, + int valueIndex) { + if ( onExecutionGenerator == null ) { + return true; + } + else if ( columnInclusions != null && !columnInclusions[valueIndex] ) { + return false; + } + else { + return bindAllValues + || columnValues != null && "?".equals( columnValues[valueIndex] ); + } + } + protected GeneratedValues doDynamicInserts( Object id, Object[] values, Object object, SharedSessionContractImplementor session, boolean forceIdentifierBinding) { - final boolean[] propertiesToInsert = getPropertiesToInsert( values ); + final boolean[] propertiesToInsert = getPropertiesToInsert( entityPersister(), values ); final var insertGroup = generateDynamicInsertSqlGroup( propertiesToInsert, object, session, forceIdentifierBinding ); final var mutationExecutor = executor( session, insertGroup, true ); final var insertValuesAnalysis = new InsertValuesAnalysis( entityPersister(), values ); final var tableInclusionChecker = getTableInclusionChecker( insertValuesAnalysis ); - decomposeForInsert( mutationExecutor, id, values, insertGroup, propertiesToInsert, tableInclusionChecker, session ); + decomposeForInsert( mutationExecutor, id, values, object, insertGroup, propertiesToInsert, tableInclusionChecker, session ); try { return mutationExecutor.execute( object, @@ -327,9 +475,9 @@ protected static TableInclusionChecker getTableInclusionChecker(InsertValuesAnal * Transform the array of property indexes to an array of booleans, * true when the property is insertable and non-null */ - public boolean[] getPropertiesToInsert(Object[] fields) { + static boolean[] getPropertiesToInsert(EntityPersister persister, Object[] fields) { final var notNull = new boolean[fields.length]; - final var insertable = entityPersister().getPropertyInsertability(); + final var insertable = persister.getPropertyInsertability(); for ( int i = 0; i < fields.length; i++ ) { notNull[i] = insertable[i] && fields[i] != null; } @@ -375,7 +523,7 @@ private void applyTableInsertDetails( Object object, SharedSessionContractImplementor session, boolean forceIdentifierBinding) { - final AttributeMappingsList attributeMappings = entityPersister().getAttributeMappings(); + final var attributeMappings = entityPersister().getAttributeMappings(); insertGroupBuilder.forEachTableMutationBuilder( (builder) -> { final var tableMapping = (EntityTableMapping) builder.getMutatingTable().getTableMapping(); @@ -383,23 +531,23 @@ private void applyTableInsertDetails( // `attributeIndexes` represents the indexes (relative to `attributeMappings`) of // the attributes mapped to the table - final int[] attributeIndexes = tableMapping.getAttributeIndexes(); - for ( int i = 0; i < attributeIndexes.length; i++ ) { - final int attributeIndex = attributeIndexes[ i ]; + for ( final int attributeIndex : tableMapping.getAttributeIndexes() ) { final var attributeMapping = attributeMappings.get( attributeIndex ); - if ( attributeInclusions[attributeIndex] ) { + final var generator = attributeMapping.getGenerator(); + if ( generator instanceof OnExecutionGenerator onExecutionGenerator + && hasValueGenerationOnExecution( object, session, onExecutionGenerator, EventType.INSERT ) ) { + if ( needsValueBinding( onExecutionGenerator, dialect() ) ) { + attributeInclusions[attributeIndex] = true; + } + handleValueGeneration( attributeMapping, insertGroupBuilder, onExecutionGenerator, EventType.INSERT ); + } + else if ( attributeInclusions[attributeIndex] ) { attributeMapping.forEachInsertable( insertGroupBuilder ); } - else { - final var generator = attributeMapping.getGenerator(); - if ( isValueGenerated( generator ) ) { - if ( session != null && generator.generatedBeforeExecution( object, session ) ) { - attributeInclusions[attributeIndex] = true; - attributeMapping.forEachInsertable( insertGroupBuilder ); - } - else if ( isValueGenerationInSql( generator, factory.getJdbcServices().getDialect() ) ) { - handleValueGeneration( attributeMapping, insertGroupBuilder, (OnExecutionGenerator) generator ); - } + else if ( generator != null && generator.generatesOnInsert() ) { + if ( session != null && generator.generatedBeforeExecution( object, session ) ) { + attributeInclusions[attributeIndex] = true; + attributeMapping.forEachInsertable( insertGroupBuilder ); } } } @@ -407,21 +555,42 @@ else if ( isValueGenerationInSql( generator, factory.getJdbcServices().getDialec // add the discriminator entityPersister().addDiscriminatorToInsertGroup( insertGroupBuilder ); - entityPersister().addSoftDeleteToInsertGroup( insertGroupBuilder ); + entityPersister().addAuxiliaryToInsertGroup( insertGroupBuilder ); // add the keys insertGroupBuilder.forEachTableMutationBuilder( (tableMutationBuilder) -> { final var tableInsertBuilder = (TableInsertBuilder) tableMutationBuilder; final var tableMapping = (EntityTableMapping) tableInsertBuilder.getMutatingTable().getTableMapping(); final var keyMapping = tableMapping.getKeyMapping(); - if ( tableMapping.isIdentifierTable() && entityPersister().isIdentifierAssignedByInsert() && !forceIdentifierBinding ) { + if ( tableMapping.isIdentifierTable() + && entityPersister().isIdentifierAssignedByInsert() + && !forceIdentifierBinding ) { assert entityPersister().getInsertDelegate() != null; final var generator = (OnExecutionGenerator) entityPersister().getGenerator(); - if ( generator.referenceColumnsInSql( dialect ) ) { - final String[] columnValues = generator.getReferencedColumnValues( dialect ); + final boolean[] columnInclusions = generator.getColumnInclusions( dialect, EventType.INSERT ); + final String[] columnValues = generator.getReferencedColumnValues( dialect, EventType.INSERT ); + final int keyColumnCount = keyMapping.getColumnCount(); + if ( columnInclusions != null ) { + if ( columnValues != null && columnValues.length != keyColumnCount ) { + throw new IllegalStateException( + "Mismatch between generated column values and identifier columns for " + + entityPersister().getEntityName() + ); + } + for ( int i = 0; i < keyColumnCount; i++ ) { + if ( columnInclusions[i] ) { + final String valueExpression = + columnValues == null + ? keyMapping.getKeyColumn( i ).getWriteExpression() + : columnValues[i]; + tableInsertBuilder.addKeyColumn( valueExpression, keyMapping.getKeyColumn( i ) ); + } + } + } + else if ( generator.referenceColumnsInSql( dialect, EventType.INSERT ) ) { if ( columnValues != null ) { assert columnValues.length == 1; - assert keyMapping.getColumnCount() == 1; + assert keyColumnCount == 1; tableInsertBuilder.addKeyColumn( columnValues[0], keyMapping.getKeyColumn( 0 ) ); } } @@ -432,15 +601,26 @@ else if ( isValueGenerationInSql( generator, factory.getJdbcServices().getDialec } ); } - private static boolean isValueGenerated(Generator generator) { - return generator != null - && generator.generatesOnInsert() - && generator.generatedOnExecution(); - } - - private static boolean isValueGenerationInSql(Generator generator, Dialect dialect) { - assert isValueGenerated( generator ); - return ( (OnExecutionGenerator) generator ).referenceColumnsInSql( dialect ); + private static boolean needsValueBinding(OnExecutionGenerator generator, Dialect dialect) { + if ( generator.generatesOnInsert() ) { + final boolean[] columnInclusions = generator.getColumnInclusions( dialect, EventType.INSERT ); + final String[] columnValues = generator.getReferencedColumnValues( dialect, EventType.INSERT ); + if ( columnValues != null ) { + for ( int i = 0; i < columnValues.length; i++ ) { + if ( (columnInclusions == null || columnInclusions[i]) + && "?".equals( columnValues[i] ) ) { + return true; + } + } + return false; + } + else { + return generator.writePropertyValue( EventType.INSERT ); + } + } + else { + return false; + } } /** diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/InsertCoordinatorTemporal.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/InsertCoordinatorTemporal.java new file mode 100644 index 000000000000..3e5c3b4e9680 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/InsertCoordinatorTemporal.java @@ -0,0 +1,56 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.persister.entity.mutation; + +import org.hibernate.engine.jdbc.mutation.MutationExecutor; +import org.hibernate.engine.jdbc.mutation.ParameterUsage; +import org.hibernate.engine.jdbc.mutation.TableInclusionChecker; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.metamodel.mapping.TemporalMapping; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.sql.model.MutationOperationGroup; + +/** + * @author Gavin King + */ +public class InsertCoordinatorTemporal extends InsertCoordinatorStandard { + private final TemporalMapping temporalMapping; + + public InsertCoordinatorTemporal(EntityPersister entityPersister, SessionFactoryImplementor factory) { + super( entityPersister, factory ); + this.temporalMapping = entityPersister.getTemporalMapping(); + } + + @Override + protected void decomposeForInsert( + MutationExecutor mutationExecutor, + Object id, + Object[] values, + Object object, + MutationOperationGroup mutationGroup, + boolean[] propertyInclusions, + TableInclusionChecker tableInclusionChecker, + SharedSessionContractImplementor session) { + super.decomposeForInsert( + mutationExecutor, + id, values, + object, + mutationGroup, + propertyInclusions, + tableInclusionChecker, + session + ); + + if ( TemporalMutationHelper.isUsingParameters( session ) ) { + mutationExecutor.getJdbcValueBindings().bindValue( + session.getCurrentTransactionIdentifier(), + entityPersister().physicalTableNameForMutation( temporalMapping.getStartingColumnMapping() ), + temporalMapping.getStartingColumnMapping().getSelectionExpression(), + ParameterUsage.SET + ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/MergeCoordinator.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/MergeCoordinator.java deleted file mode 100644 index 1b6f78497dc9..000000000000 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/MergeCoordinator.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * Copyright Red Hat Inc. and Hibernate Authors - */ -package org.hibernate.persister.entity.mutation; - -import org.hibernate.engine.spi.SessionFactoryImplementor; -import org.hibernate.persister.entity.EntityPersister; -import org.hibernate.sql.model.MutationOperation; -import org.hibernate.sql.model.ast.builder.AbstractTableUpdateBuilder; -import org.hibernate.sql.model.ast.builder.TableMergeBuilder; - -/** - * Specialized {@link UpdateCoordinator} for {@code merge into}. - * - * @author Gavin King - */ -public class MergeCoordinator extends UpdateCoordinatorStandard { - - public MergeCoordinator(EntityPersister entityPersister, SessionFactoryImplementor factory) { - super(entityPersister, factory); - } - - @Override - protected AbstractTableUpdateBuilder newTableUpdateBuilder(EntityTableMapping tableMapping) { - return new TableMergeBuilder<>( entityPersister(), tableMapping, factory() ); - } - -} diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/MergeCoordinatorAudit.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/MergeCoordinatorAudit.java new file mode 100644 index 000000000000..ae9666f3f0c8 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/MergeCoordinatorAudit.java @@ -0,0 +1,20 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.persister.entity.mutation; + +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.persister.entity.EntityPersister; + +/** + * Merge coordinator for audited entities. + */ +public class MergeCoordinatorAudit extends UpdateCoordinatorAudit { + public MergeCoordinatorAudit( + EntityPersister entityPersister, + SessionFactoryImplementor factory, + UpdateCoordinator currentUpdateCoordinator) { + super( entityPersister, factory, currentUpdateCoordinator ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/MergeCoordinatorHistory.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/MergeCoordinatorHistory.java new file mode 100644 index 000000000000..a5591ea3d3d5 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/MergeCoordinatorHistory.java @@ -0,0 +1,31 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.persister.entity.mutation; + +import org.hibernate.engine.jdbc.mutation.group.PreparedStatementDetails; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.persister.entity.EntityPersister; + +/** + * Merge coordinator for + * {@link org.hibernate.temporal.TemporalTableStrategy#HISTORY_TABLE} + * temporal strategy. + * + * @author Gavin King + */ +public class MergeCoordinatorHistory extends UpdateCoordinatorHistory { + public MergeCoordinatorHistory( + EntityPersister entityPersister, + SessionFactoryImplementor factory, + UpdateCoordinator currentMergeCoordinator) { + super( entityPersister, factory, currentMergeCoordinator ); + } + + @Override + boolean resultCheck(Object id, PreparedStatementDetails statementDetails, int affectedRowCount, int batchPosition) { + return affectedRowCount != 0 + && super.resultCheck( id, statementDetails, affectedRowCount, batchPosition ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/MergeCoordinatorStandard.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/MergeCoordinatorStandard.java new file mode 100644 index 000000000000..ea9da62b4d37 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/MergeCoordinatorStandard.java @@ -0,0 +1,163 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.persister.entity.mutation; + +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.metamodel.mapping.AttributeMapping; +import org.hibernate.metamodel.mapping.DiscriminatorValue; +import org.hibernate.metamodel.mapping.SelectableMapping; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.sql.model.MutationOperation; +import org.hibernate.sql.model.ast.builder.AbstractTableUpdateBuilder; +import org.hibernate.sql.model.ast.builder.TableMergeBuilder; +import org.hibernate.sql.model.ast.builder.TableUpdateBuilder; + +/** + * Specialized {@link UpdateCoordinator} for {@code merge into}. + * + * @author Gavin King + */ +public class MergeCoordinatorStandard extends UpdateCoordinatorStandard { + + public MergeCoordinatorStandard(EntityPersister entityPersister, SessionFactoryImplementor factory) { + super( entityPersister, factory ); + } + + @Override + protected AbstractTableUpdateBuilder newTableUpdateBuilder(EntityTableMapping tableMapping) { + final TableMergeBuilder tableUpdateBuilder = + new TableMergeBuilder<>( entityPersister(), tableMapping, factory() ); + addDiscriminatorValueIfNeeded( tableUpdateBuilder, tableMapping ); + return tableUpdateBuilder; + } + + private void addDiscriminatorValueIfNeeded( + AbstractTableUpdateBuilder tableUpdateBuilder, + EntityTableMapping tableMapping) { + final var discriminatorMapping = entityPersister().getDiscriminatorMapping(); + if ( discriminatorMapping != null + && discriminatorMapping.hasPhysicalColumn() + && tableMapping.getTableName().equals( discriminatorMapping.getContainingTableExpression() ) ) { + final DiscriminatorValue discriminatorValue = entityPersister().getDiscriminatorValue(); + if ( discriminatorValue != DiscriminatorValue.Special.NULL + && discriminatorValue != DiscriminatorValue.Special.NOT_NULL ) { + tableUpdateBuilder.addValueColumn( + entityPersister().getDiscriminatorSQLValue(), + discriminatorMapping + ); + } + } + } + + @Override + protected boolean isColumnIncludedInSet(SelectableMapping selectable) { + return selectable.isUpdateable() || selectable.isInsertable(); + } + + private static boolean isInsertableOrUpdatable(AttributeMapping attribute) { + final var attributeMetadata = attribute.getAttributeMetadata(); + return attributeMetadata.isUpdatable() + || attributeMetadata.isInsertable(); + } + + @Override + protected InclusionChecker createInclusionChecker(boolean[] attributeUpdateability) { + return (position, attribute) -> isInsertableOrUpdatable( attribute ); + } + + @Override + protected boolean includeInStaticUpdate( + int index, + AttributeMapping attribute, + boolean[] propertyUpdateability) { + return isInsertableOrUpdatable( attribute ) + || super.includeInStaticUpdate( index, attribute, propertyUpdateability ); + } + + @Override + protected boolean includeProperty(boolean[] insertability, boolean[] updateability, int property) { + return insertability[property] || updateability[property]; + } + + @Override + public boolean[] getPropertyUpdateability(Object entity) { + final boolean[] updateability = super.getPropertyUpdateability( entity ); + final boolean[] insertability = entityPersister().getPropertyInsertability(); + final var result = new boolean[updateability.length]; + for ( int i = 0; i < updateability.length; i++ ) { + result[i] = updateability[i] || insertability[i]; + } + return result; + } + + @Override + public boolean[] getPropertyUpdateability() { + final boolean[] updateability = entityPersister().getPropertyUpdateability(); + final boolean[] insertability = entityPersister().getPropertyInsertability(); + final var result = new boolean[updateability.length]; + for ( int i = 0; i < updateability.length; i++ ) { + result[i] = updateability[i] || insertability[i]; + } + return result; + } + @Override + protected void forEachUpdatable(AttributeMapping attributeMapping, TableUpdateBuilder tableUpdateBuilder) { + attributeMapping.forEachSelectable( tableUpdateBuilder ); + } + + @Override + protected UpdateValuesAnalysisImpl analyzeUpdateValues( + Object entity, + Object[] values, + Object oldVersion, + Object[] oldValues, + int[] dirtyAttributeIndexes, + InclusionChecker inclusionChecker, + InclusionChecker lockingChecker, + InclusionChecker dirtinessChecker, + boolean restrictToTemporalExcluded, + Object rowId, + boolean forceDynamicUpdate, + SharedSessionContractImplementor session) { + final var updateValuesAnalysis = super.analyzeUpdateValues( + entity, + values, + oldVersion, + oldValues, + dirtyAttributeIndexes, + inclusionChecker, + lockingChecker, + dirtinessChecker, + restrictToTemporalExcluded, + rowId, + forceDynamicUpdate, + session + ); + if ( oldValues == null ) { + final TableSet tablesNeedingUpdate = updateValuesAnalysis.getTablesNeedingUpdate(); + final TableSet tablesWithNonNullValues = updateValuesAnalysis.getTablesWithNonNullValues(); + final TableSet tablesWithPreviousNonNullValues = updateValuesAnalysis.getTablesWithPreviousNonNullValues(); + for ( var tableMapping : entityPersister().getTableMappings() ) { + // Need to upsert into all non-optional table mappings + if ( !tableMapping.isOptional() ) { + // If the table was previously not needing an update, remove it from tablesWithPreviousNonNullValues + // to avoid triggering a delete-statement for this operation + if ( !tablesNeedingUpdate.contains( tableMapping ) ) { + tablesWithPreviousNonNullValues.remove( tableMapping ); + } + tablesNeedingUpdate.add( tableMapping ); + tablesWithNonNullValues.add( tableMapping ); + } + } + } + return updateValuesAnalysis; + } + + @Override + public String toString() { + return "MergeCoordinator(" + entityPersister().getEntityName() + ")"; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/MergeCoordinatorTemporal.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/MergeCoordinatorTemporal.java new file mode 100644 index 000000000000..891658f6ed1d --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/MergeCoordinatorTemporal.java @@ -0,0 +1,135 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.persister.entity.mutation; + +import org.hibernate.StaleObjectStateException; +import org.hibernate.engine.jdbc.batch.internal.BasicBatchKey; +import org.hibernate.engine.jdbc.mutation.JdbcValueBindings; +import org.hibernate.engine.jdbc.mutation.OperationResultChecker; +import org.hibernate.engine.jdbc.mutation.ParameterUsage; +import org.hibernate.engine.jdbc.mutation.group.PreparedStatementDetails; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.generator.values.GeneratedValues; +import org.hibernate.metamodel.mapping.TemporalMapping; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.sql.model.MutationOperationGroup; + +/** + * Merge coordinator for + * {@link org.hibernate.temporal.TemporalTableStrategy#SINGLE_TABLE} + * temporal strategy. + * + * @author Gavin King + */ +public class MergeCoordinatorTemporal extends AbstractTemporalUpdateCoordinator { + private final TemporalMapping temporalMapping; + private final MutationOperationGroup endingUpdateGroup; + private final BasicBatchKey batchKey; + private final UpdateCoordinator versionUpdateDelegate; + + public MergeCoordinatorTemporal(EntityPersister entityPersister, SessionFactoryImplementor factory) { + super( entityPersister, factory ); + this.temporalMapping = entityPersister.getTemporalMapping(); + this.endingUpdateGroup = buildEndingUpdateGroup( entityPersister.getIdentifierTableMapping(), temporalMapping ); + this.batchKey = new BasicBatchKey( entityPersister.getEntityName() + "#TEMPORAL_MERGE" ); + this.versionUpdateDelegate = new MergeCoordinatorStandard( entityPersister, factory ); + } + + @Override + public MutationOperationGroup getStaticMutationOperationGroup() { + return endingUpdateGroup; + } + + @Override + protected BasicBatchKey getBatchKey() { + return batchKey; + } + + @Override + public GeneratedValues update( + Object entity, + Object id, + Object rowId, + Object[] values, + Object oldVersion, + Object[] incomingOldValues, + int[] dirtyAttributeIndexes, + boolean hasDirtyCollection, + SharedSessionContractImplementor session) { + if ( entityPersister() + .excludedFromTemporalVersioning( dirtyAttributeIndexes, hasDirtyCollection ) ) { + return versionUpdateDelegate.update( + entity, + id, + rowId, + values, + oldVersion, + incomingOldValues, + dirtyAttributeIndexes, + hasDirtyCollection, + session + ); + } + else { + final boolean rowEnded = performRowEndUpdate( entity, id, rowId, oldVersion, session ); + if ( !rowEnded && currentRowExists( id, session ) ) { + throw new StaleObjectStateException( entityPersister().getEntityName(), id ); + } + return entityPersister().getInsertCoordinator().insert( entity, id, values, session ); + } + } + + boolean performRowEndUpdate( + Object entity, + Object id, + Object rowId, + Object oldVersion, + SharedSessionContractImplementor session) { + class Result implements OperationResultChecker { + private boolean updated; + @Override + public boolean checkResult(PreparedStatementDetails statementDetails, int affectedRowCount, int batchPosition) { + updated = affectedRowCount > 0; + return !updated + || resultCheck( id, statementDetails, affectedRowCount, batchPosition ); + } + } + final var resultChecker = new Result(); + performRowEndUpdate( + entity, + id, + rowId, + oldVersion, + session, + temporalMapping, + endingUpdateGroup, + entityPersister().physicalTableNameForMutation( temporalMapping.getEndingColumnMapping() ), + resultChecker + ); + return resultChecker.updated; + } + + private boolean currentRowExists(Object id, SharedSessionContractImplementor session) { + return entityPersister().getDatabaseSnapshot( id, session ) != null; + } + + @Override + void bindVersionRestriction(Object oldVersion, JdbcValueBindings jdbcValueBindings, String temporalTableName) { + final var versionMapping = entityPersister().getVersionMapping(); + if ( versionMapping != null && entityPersister().optimisticLockStyle().isVersion() ) { + jdbcValueBindings.bindValue( oldVersion, versionMapping, ParameterUsage.RESTRICT ); + } + } + + @Override + public void forceVersionIncrement( + Object id, + Object currentVersion, + Object nextVersion, + SharedSessionContractImplementor session) { + versionUpdateDelegate.forceVersionIncrement( id, currentVersion, nextVersion, session ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/MutationCoordinator.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/MutationCoordinator.java index 594c8d071e05..58b821da8d8f 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/MutationCoordinator.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/MutationCoordinator.java @@ -12,7 +12,7 @@ * @see InsertCoordinator * @see DeleteCoordinator * @see UpdateCoordinator - * @see MergeCoordinator + * @see MergeCoordinatorStandard * * @author Marco Belladelli */ diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/TableSet.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/TableSet.java index 4d2de0f42b09..e8fb90cf818c 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/TableSet.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/TableSet.java @@ -34,6 +34,13 @@ public void add(final TableMapping tableMapping) { bits.set( tableMapping.getRelativePosition() ); } + public void remove(final TableMapping tableMapping) { + if ( bits != null ) { + assert addForChecks( tableMapping ); + bits.set( tableMapping.getRelativePosition(), false ); + } + } + public boolean isEmpty() { return bits == null; } diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/TemporalMutationHelper.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/TemporalMutationHelper.java new file mode 100644 index 000000000000..9eb8aebb7b4f --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/TemporalMutationHelper.java @@ -0,0 +1,17 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.persister.entity.mutation; + +import org.hibernate.engine.spi.SharedSessionContractImplementor; + +import static org.hibernate.temporal.TemporalTableStrategy.NATIVE; + +public class TemporalMutationHelper { + public static boolean isUsingParameters(SharedSessionContractImplementor session) { + final var factory = session.getFactory(); + return factory.getSessionFactoryOptions().getTemporalTableStrategy() != NATIVE + && !factory.getTransactionIdentifierService().useServerTimestamp( session.getDialect() ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/UpdateCoordinatorAudit.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/UpdateCoordinatorAudit.java new file mode 100644 index 000000000000..41f0d5e0a3b4 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/UpdateCoordinatorAudit.java @@ -0,0 +1,86 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.persister.entity.mutation; + +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.generator.values.GeneratedValues; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.persister.state.internal.AuditStateManagement; +import org.hibernate.sql.model.MutationOperationGroup; + +/** + * Update coordinator for audited entities. + */ +public class UpdateCoordinatorAudit extends AbstractAuditCoordinator implements UpdateCoordinator { + private final UpdateCoordinator currentUpdateCoordinator; + + public UpdateCoordinatorAudit( + EntityPersister entityPersister, + SessionFactoryImplementor factory, + UpdateCoordinator currentUpdateCoordinator) { + super( entityPersister, factory ); + this.currentUpdateCoordinator = currentUpdateCoordinator; + } + + @Override + public MutationOperationGroup getStaticMutationOperationGroup() { + return currentUpdateCoordinator.getStaticMutationOperationGroup(); + } + + @Override + public GeneratedValues update( + Object entity, + Object id, + Object rowId, + Object[] values, + Object oldVersion, + Object[] incomingOldValues, + int[] dirtyAttributeIndexes, + boolean hasDirtyCollection, + SharedSessionContractImplementor session) { + final var generatedValues = currentUpdateCoordinator.update( + entity, + id, + rowId, + values, + oldVersion, + incomingOldValues, + dirtyAttributeIndexes, + hasDirtyCollection, + session + ); + if ( shouldAuditUpdate( dirtyAttributeIndexes, hasDirtyCollection ) ) { + insertAuditRow( entity, id, values, AuditStateManagement.ModificationType.MOD, session ); + } + return generatedValues; + } + + @Override + public void forceVersionIncrement( + Object id, + Object currentVersion, + Object nextVersion, + SharedSessionContractImplementor session) { + currentUpdateCoordinator.forceVersionIncrement( id, currentVersion, nextVersion, session ); + } + + private boolean shouldAuditUpdate(int[] dirtyAttributeIndexes, boolean hasDirtyCollection) { + if ( dirtyAttributeIndexes == null || dirtyAttributeIndexes.length == 0 ) { + return true; + } + else if ( hasDirtyCollection ) { + return true; + } + else { + for ( int dirtyIndex : dirtyAttributeIndexes ) { + if ( auditedPropertyMask[dirtyIndex] ) { + return true; + } + } + return false; + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/UpdateCoordinatorHistory.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/UpdateCoordinatorHistory.java new file mode 100644 index 000000000000..df599f8d7552 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/UpdateCoordinatorHistory.java @@ -0,0 +1,518 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.persister.entity.mutation; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +import org.hibernate.engine.jdbc.batch.internal.BasicBatchKey; +import org.hibernate.engine.jdbc.mutation.JdbcValueBindings; +import org.hibernate.engine.jdbc.mutation.ParameterUsage; +import org.hibernate.engine.jdbc.mutation.group.PreparedStatementDetails; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.generator.Generator; +import org.hibernate.generator.OnExecutionGenerator; +import org.hibernate.generator.values.GeneratedValues; +import org.hibernate.metamodel.mapping.AttributeMapping; +import org.hibernate.metamodel.mapping.PluralAttributeMapping; +import org.hibernate.metamodel.mapping.TemporalMapping; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.sql.ast.tree.expression.ColumnReference; +import org.hibernate.sql.model.MutationOperation; +import org.hibernate.sql.model.MutationOperationGroup; +import org.hibernate.sql.model.MutationType; +import org.hibernate.sql.model.ast.builder.ColumnValuesTableMutationBuilder; +import org.hibernate.sql.model.ast.builder.TableInsertBuilderStandard; +import org.hibernate.sql.model.ast.builder.TableUpdateBuilderStandard; +import org.hibernate.sql.model.internal.MutationGroupSingle; + +import static org.hibernate.sql.model.internal.MutationOperationGroupFactory.singleOperation; + +/** + * Update coordinator for + * {@link org.hibernate.temporal.TemporalTableStrategy#HISTORY_TABLE} + * temporal strategy. + * + * @author Gavin King + */ +public class UpdateCoordinatorHistory extends AbstractTemporalUpdateCoordinator { + private final UpdateCoordinator currentUpdateCoordinator; + private final EntityTableMapping identifierTableMapping; + private final EntityTableMapping historyTableMapping; + private final TemporalMapping temporalMapping; + private final BasicBatchKey historyUpdateBatchKey; + private final BasicBatchKey historyInsertBatchKey; + private final MutationOperationGroup historyEndUpdateGroup; + private final MutationOperationGroup historyInsertGroup; + + public UpdateCoordinatorHistory( + EntityPersister entityPersister, + SessionFactoryImplementor factory, + UpdateCoordinator currentUpdateCoordinator) { + super( entityPersister, factory ); + this.currentUpdateCoordinator = currentUpdateCoordinator; + this.identifierTableMapping = entityPersister.getIdentifierTableMapping(); + this.temporalMapping = entityPersister.getTemporalMapping(); + this.historyTableMapping = + createAuxiliaryTableMapping( identifierTableMapping, entityPersister, + temporalMapping.getTableName() ); + final String entityName = entityPersister.getEntityName(); + this.historyUpdateBatchKey = new BasicBatchKey( entityName + "#HISTORY_UPDATE" ); + this.historyInsertBatchKey = new BasicBatchKey( entityName + "#HISTORY_INSERT" ); + this.historyEndUpdateGroup = buildEndingUpdateGroup( historyTableMapping, temporalMapping ); + this.historyInsertGroup = buildHistoryInsertGroup( entityPersister.getPropertyInsertability() ); + } + + @Override + public MutationOperationGroup getStaticMutationOperationGroup() { + return currentUpdateCoordinator.getStaticMutationOperationGroup(); + } + + @Override + protected BasicBatchKey getBatchKey() { + return historyUpdateBatchKey; + } + + @Override + public GeneratedValues update( + Object entity, + Object id, + Object rowId, + Object[] values, + Object oldVersion, + Object[] incomingOldValues, + int[] dirtyAttributeIndexes, + boolean hasDirtyCollection, + SharedSessionContractImplementor session) { + final var generatedValues = currentUpdateCoordinator.update( + entity, + id, + rowId, + values, + oldVersion, + incomingOldValues, + dirtyAttributeIndexes, + hasDirtyCollection, + session + ); + if ( entityPersister() + .excludedFromTemporalVersioning( dirtyAttributeIndexes, hasDirtyCollection ) ) { + performHistoryExcludedUpdate( + entity, + id, + rowId, + values, + oldVersion, + incomingOldValues, + dirtyAttributeIndexes, + session + ); + } + else { + performRowEndUpdate( + entity, + id, + rowId, + oldVersion, + session, + temporalMapping, + historyEndUpdateGroup, + historyTableMapping.getTableName(), + (statementDetails, affectedRowCount, batchPosition) -> + resultCheck( id, statementDetails, affectedRowCount, batchPosition ) + + ); + insertHistoryRow( id, values, session ); + } + return generatedValues; + } + + private void performHistoryExcludedUpdate( + Object entity, + Object id, + Object rowId, + Object[] values, + Object oldVersion, + Object[] incomingOldValues, + int[] dirtyAttributeIndexes, + SharedSessionContractImplementor session) { + final var updateDetails = + buildHistoryExcludedUpdateDetails( entity, rowId, dirtyAttributeIndexes, session ); + if ( updateDetails != null ) { + final var mutationExecutor = + mutationExecutorService.createExecutor( resolveBatchKeyAccess( true, session ), + updateDetails.operationGroup, session ); + try { + final var jdbcValueBindings = mutationExecutor.getJdbcValueBindings(); + breakDownKeyJdbcValues( id, rowId, session, jdbcValueBindings, historyTableMapping ); + + if ( updateDetails.applyVersionRestriction ) { + final var versionMapping = entityPersister().getVersionMapping(); + jdbcValueBindings.bindValue( + oldVersion, + historyTableMapping.getTableName(), + versionMapping.getSelectionExpression(), + ParameterUsage.RESTRICT + ); + } + + final var loadedState = incomingOldValues != null ? incomingOldValues : values; + bindPartitionColumnValueBindings( loadedState, session, jdbcValueBindings ); + bindHistoryExcludedUpdateValues( values, updateDetails, session, jdbcValueBindings ); + + mutationExecutor.execute( + entity, + null, + null, + (statementDetails, affectedRowCount, batchPosition) -> + resultCheck( id, statementDetails, affectedRowCount, batchPosition ), + session, + staleStateException -> staleObjectStateException( id, staleStateException ) + ); + } + finally { + mutationExecutor.release(); + } + } + } + + private HistoryExcludedUpdateDetails buildHistoryExcludedUpdateDetails( + Object entity, + Object rowId, + int[] dirtyAttributeIndexes, + SharedSessionContractImplementor session) { + if ( dirtyAttributeIndexes == null || dirtyAttributeIndexes.length == 0 ) { + return null; + } + + final var attributeMappings = entityPersister().getAttributeMappings(); + final int attributeCount = attributeMappings.size(); + final boolean[] dirtyFlags = new boolean[attributeCount]; + for ( int dirtyAttributeIndex : dirtyAttributeIndexes ) { + dirtyFlags[dirtyAttributeIndex] = true; + } + + final var versionMapping = entityPersister().getVersionMapping(); + final var updateability = + entityPersister().hasUninitializedLazyProperties( entity ) + ? entityPersister().getNonLazyPropertyUpdateability() + : entityPersister().getPropertyUpdateability(); + + final var tableUpdateBuilder = + new TableUpdateBuilderStandard<>( entityPersister(), historyTableMapping, factory() ); + final List bindableAttributeIndexes = new ArrayList<>(); + boolean hasValues = false; + + for ( final int attributeIndex : identifierTableMapping.getAttributeIndexes() ) { + final var attributeMapping = attributeMappings.get( attributeIndex ); + if ( !(attributeMapping instanceof PluralAttributeMapping) ) { + if ( entityPersister().isPropertyTemporalExcluded( attributeIndex ) ) { + final var generator = attributeMapping.getGenerator(); + final boolean generatedInSql = needsUpdateValueGeneration( entity, session, generator ); + final boolean include = + generatedInSql + || isGeneratedBeforeExecution( entity, session, generator ) + || dirtyFlags[attributeIndex] && updateability[attributeIndex]; + + if ( include ) { + if ( generatedInSql ) { + final var onExecutionGenerator = (OnExecutionGenerator) generator; + addSqlGeneratedValue( tableUpdateBuilder, attributeMapping, onExecutionGenerator ); + hasValues = true; + if ( onExecutionGenerator.writePropertyValue() ) { + bindableAttributeIndexes.add( attributeIndex ); + } + } + else { + attributeMapping.forEachUpdatable( tableUpdateBuilder ); + hasValues = true; + bindableAttributeIndexes.add( attributeIndex ); + } + } + } + } + } + + if ( hasValues ) { + applyKeyRestriction( rowId, entityPersister(), tableUpdateBuilder, historyTableMapping ); + applyCurrentRowRestriction( tableUpdateBuilder ); + applyPartitionKeyRestriction( tableUpdateBuilder ); + applyOptimisticLocking( tableUpdateBuilder ); + + return new HistoryExcludedUpdateDetails( + createMutationOperationGroup( tableUpdateBuilder ), + toIntArray( bindableAttributeIndexes ), + entityPersister().optimisticLockStyle().isVersion() + && versionMapping != null + ); + } + else { + return null; + } + + } + + private static boolean isGeneratedBeforeExecution( + Object entity, SharedSessionContractImplementor session, Generator generator) { + return generator != null + && generator.generatesOnUpdate() + && generator.generatedBeforeExecution( entity, session ); + } + + private void bindHistoryExcludedUpdateValues( + Object[] values, + HistoryExcludedUpdateDetails updateDetails, + SharedSessionContractImplementor session, + JdbcValueBindings jdbcValueBindings) { + final var attributeMappings = entityPersister().getAttributeMappings(); + for ( final int attributeIndex : updateDetails.attributeIndexes ) { + final var attributeMapping = attributeMappings.get( attributeIndex ); + if ( !(attributeMapping instanceof PluralAttributeMapping) ) { + attributeMapping.decompose( + values[attributeIndex], + 0, + jdbcValueBindings, + historyTableMapping, + (valueIndex, bindings, table, jdbcValue, selectableMapping) -> { + if ( selectableMapping.isUpdateable() && !selectableMapping.isFormula() ) { + bindings.bindValue( + jdbcValue, + table.getTableName(), + selectableMapping.getSelectionExpression(), + ParameterUsage.SET + ); + } + }, + session + ); + } + } + } + + private void applyCurrentRowRestriction(TableUpdateBuilderStandard tableUpdateBuilder) { + final var endingColumnReference = + new ColumnReference( tableUpdateBuilder.getMutatingTable(), temporalMapping.getEndingColumnMapping() ); + tableUpdateBuilder.addNonKeyRestriction( temporalMapping.createNullEndingValueBinding( endingColumnReference ) ); + } + + @Override + void bindVersionRestriction(Object oldVersion, JdbcValueBindings jdbcValueBindings, String temporalTableName) { + final var versionMapping = entityPersister().getVersionMapping(); + if ( versionMapping != null && entityPersister().optimisticLockStyle().isVersion() ) { + jdbcValueBindings.bindValue( + oldVersion, + temporalTableName, + versionMapping.getSelectionExpression(), + ParameterUsage.RESTRICT + ); + } + } + + private void insertHistoryRow( + Object id, + Object[] values, + SharedSessionContractImplementor session) { + final var mutationExecutor = + mutationExecutorService.createExecutor( () -> historyInsertBatchKey, historyInsertGroup, session ); + try { + bindHistoryInsertValues( id, values, entityPersister().getPropertyInsertability(), session, + mutationExecutor.getJdbcValueBindings() ); + mutationExecutor.execute( id, null, null, + UpdateCoordinatorHistory::verifyOutcome, session ); + } + finally { + mutationExecutor.release(); + } + } + + private MutationOperationGroup buildHistoryInsertGroup(boolean[] propertyInclusions) { + final var insertBuilder = + new TableInsertBuilderStandard( entityPersister(), historyTableMapping, factory() ); + applyHistoryInsertDetails( insertBuilder, propertyInclusions ); + final var tableMutation = insertBuilder.buildMutation(); + return singleOperation( + new MutationGroupSingle( MutationType.INSERT, entityPersister(), tableMutation ), + tableMutation.createMutationOperation( null, factory() ) + ); + } + + private void applyHistoryInsertDetails( + TableInsertBuilderStandard insertBuilder, + boolean[] propertyInclusions) { + final var attributeMappings = entityPersister().getAttributeMappings(); + for ( final int attributeIndex : identifierTableMapping.getAttributeIndexes() ) { + final var attributeMapping = attributeMappings.get( attributeIndex ); + if ( propertyInclusions[attributeIndex] ) { + attributeMapping.forEachInsertable( insertBuilder ); + } + else { + final var generator = attributeMapping.getGenerator(); + if ( isValueGeneratedOnInsert( generator ) ) { +// if ( session != null && generator.generatedBeforeExecution( entity, session ) ) { +// propertyInclusions[attributeIndex] = true; +// attributeMapping.forEachInsertable( insertBuilder ); +// } +// else + if ( isValueGenerationInSql( generator ) ) { + addSqlGeneratedValue( insertBuilder, attributeMapping, (OnExecutionGenerator) generator ); + } + } + } + } + + final var mutatingTable = insertBuilder.getMutatingTable(); + final var startingColumn = new ColumnReference( mutatingTable, temporalMapping.getStartingColumnMapping() ); + insertBuilder.addValueColumn( temporalMapping.createStartingValueBinding( startingColumn ) ); + final var endingColumn = new ColumnReference( mutatingTable, temporalMapping.getEndingColumnMapping() ); + insertBuilder.addValueColumn( temporalMapping.createNullEndingValueBinding( endingColumn ) ); + + identifierTableMapping.getKeyMapping().forEachKeyColumn( insertBuilder::addKeyColumn ); + } + + private void addSqlGeneratedValue( + ColumnValuesTableMutationBuilder updateBuilder, + AttributeMapping attributeMapping, + OnExecutionGenerator generator) { + final boolean writePropertyValue = generator.writePropertyValue(); + final var columnValues = + writePropertyValue + ? null + : generator.getReferencedColumnValues( factory.getJdbcServices().getDialect() ); + attributeMapping.forEachSelectable( (j, mapping) -> + updateBuilder.addValueColumn( writePropertyValue ? "?" : columnValues[j], mapping ) ); + } + + private void bindHistoryInsertValues( + Object id, + Object[] values, + boolean[] propertyInclusions, + SharedSessionContractImplementor session, + JdbcValueBindings jdbcValueBindings) { + final String historyTableName = historyTableMapping.getTableName(); + historyTableMapping.getKeyMapping().breakDownKeyJdbcValues( + id, + (jdbcValue, columnMapping) -> jdbcValueBindings.bindValue( + jdbcValue, + historyTableName, + columnMapping.getColumnName(), + ParameterUsage.SET + ), + session + ); + + final var attributeMappings = entityPersister().getAttributeMappings(); + for ( final int attributeIndex : identifierTableMapping.getAttributeIndexes() ) { + if ( propertyInclusions[attributeIndex] ) { + final var attributeMapping = attributeMappings.get( attributeIndex ); + if ( !(attributeMapping instanceof PluralAttributeMapping) ) { + attributeMapping.decompose( + values[attributeIndex], + 0, + jdbcValueBindings, + null, + (valueIndex, bindings, noop, jdbcValue, selectableMapping) -> { + if ( selectableMapping.isInsertable() && !selectableMapping.isFormula() ) { + bindings.bindValue( + jdbcValue, + historyTableName, + selectableMapping.getSelectionExpression(), + ParameterUsage.SET + ); + } + }, + session + ); + } + } + } + + if ( TemporalMutationHelper.isUsingParameters( session ) ) { + jdbcValueBindings.bindValue( + session.getCurrentTransactionIdentifier(), + historyTableName, + temporalMapping.getStartingColumnMapping().getSelectionExpression(), + ParameterUsage.SET + ); + } + } + + private static boolean isValueGeneratedOnInsert(Generator generator) { + return generator != null + && generator.generatesOnInsert() + && generator.generatedOnExecution(); + } + + private static boolean isValueGeneratedOnUpdate(Generator generator) { + return generator != null + && generator.generatesOnUpdate() + && generator.generatedOnExecution(); + } + + private boolean isValueGenerationInSql(Generator generator) { + assert isValueGeneratedOnInsert( generator ); + return ( (OnExecutionGenerator) generator ).referenceColumnsInSql( dialect() ); + } + + private boolean isUpdateValueGenerationInSql(Generator generator) { + assert isValueGeneratedOnUpdate( generator ); + return ( (OnExecutionGenerator) generator ).referenceColumnsInSql( dialect() ); + } + + private boolean needsUpdateValueGeneration( + Object entity, + SharedSessionContractImplementor session, + Generator generator) { + return isValueGeneratedOnUpdate( generator ) + && (session == null && generator.generatedOnExecution() || generator.generatedOnExecution( entity, session ) ) + && isUpdateValueGenerationInSql( generator ); + } + + private static int[] toIntArray(List values) { + final int[] result = new int[values.size()]; + for ( int i = 0; i < values.size(); i++ ) { + result[i] = values.get( i ); + } + return result; + } + + private static final class HistoryExcludedUpdateDetails { + private final MutationOperationGroup operationGroup; + private final int[] attributeIndexes; + private final boolean applyVersionRestriction; + + private HistoryExcludedUpdateDetails( + MutationOperationGroup operationGroup, + int[] attributeIndexes, + boolean applyVersionRestriction) { + this.operationGroup = operationGroup; + this.attributeIndexes = attributeIndexes; + this.applyVersionRestriction = applyVersionRestriction; + } + } + + private static boolean verifyOutcome( + PreparedStatementDetails statementDetails, + int affectedRowCount, + int batchPosition) throws SQLException { + statementDetails.getExpectation().verifyOutcome( + affectedRowCount, + statementDetails.getStatement(), + batchPosition, + statementDetails.getSqlString() + ); + return true; + } + + @Override + public void forceVersionIncrement( + Object id, + Object currentVersion, + Object nextVersion, + SharedSessionContractImplementor session) { + currentUpdateCoordinator.forceVersionIncrement( id, currentVersion, nextVersion, session ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/UpdateCoordinatorStandard.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/UpdateCoordinatorStandard.java index f2c9fc3f2117..c13c5a07fa49 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/UpdateCoordinatorStandard.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/UpdateCoordinatorStandard.java @@ -9,10 +9,9 @@ import java.util.Locale; import java.util.function.Supplier; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.HibernateException; import org.hibernate.Internal; -import org.hibernate.StaleObjectStateException; -import org.hibernate.StaleStateException; import org.hibernate.dialect.Dialect; import org.hibernate.engine.OptimisticLockStyle; import org.hibernate.engine.jdbc.batch.internal.BasicBatchKey; @@ -20,7 +19,6 @@ import org.hibernate.engine.jdbc.mutation.JdbcValueBindings; import org.hibernate.engine.jdbc.mutation.MutationExecutor; import org.hibernate.engine.jdbc.mutation.ParameterUsage; -import org.hibernate.engine.jdbc.mutation.group.PreparedStatementDetails; import org.hibernate.engine.jdbc.mutation.internal.MutationQueryOptions; import org.hibernate.engine.jdbc.mutation.internal.NoBatchKeyAccess; import org.hibernate.engine.jdbc.mutation.spi.BatchKeyAccess; @@ -28,6 +26,7 @@ import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.generator.BeforeExecutionGenerator; +import org.hibernate.generator.EventType; import org.hibernate.generator.Generator; import org.hibernate.generator.OnExecutionGenerator; import org.hibernate.generator.values.GeneratedValues; @@ -51,7 +50,6 @@ import static org.hibernate.engine.OptimisticLockStyle.DIRTY; import static org.hibernate.engine.internal.Versioning.isVersionIncrementRequired; -import static org.hibernate.engine.jdbc.mutation.internal.ModelMutationHelper.identifiedResultsCheck; import static org.hibernate.generator.EventType.UPDATE; import static org.hibernate.internal.CoreMessageLogger.CORE_LOGGER; import static org.hibernate.internal.util.collections.ArrayHelper.EMPTY_INT_ARRAY; @@ -135,7 +133,14 @@ public void forceVersionIncrement( if ( versionUpdateGroup == null ) { throw new HibernateException( "Cannot force version increment relative to subtype; use the root type" ); } - doVersionUpdate( null, id, nextVersion, currentVersion, session ); + doVersionUpdate( null, id, nextVersion, currentVersion, getLoadedState( id, session ), session ); + } + + private Object @Nullable [] getLoadedState(Object id, SharedSessionContractImplementor session) { + return entityPersister.hasPartitionedSelectionMapping() + ? session.getPersistenceContextInternal() + .getEntityHolder( session.generateEntityKey( id, entityPersister ) ).getEntityEntry().getLoadedState() + : null; } @Override @@ -148,7 +153,7 @@ public void forceVersionIncrement( if ( versionUpdateGroup == null ) { throw new HibernateException( "Cannot force version increment relative to subtype; use the root type" ); } - doVersionUpdate( null, id, nextVersion, currentVersion, batching, session ); + doVersionUpdate( null, id, nextVersion, currentVersion, batching, getLoadedState( id, session ), session ); } @Override @@ -169,6 +174,7 @@ public GeneratedValues update( entity, id, values, + incomingOldValues, oldVersion, incomingDirtyAttributeIndexes, session, @@ -192,9 +198,21 @@ public GeneratedValues update( final int[] dirtyAttributeIndexes = dirtyAttributeIndexes( incomingDirtyAttributeIndexes, preUpdateGeneratedAttributeIndexes ); + final boolean temporalExcludedUpdate = + entityPersister().excludedFromTemporalVersioning( dirtyAttributeIndexes, hasDirtyCollection ); + final boolean[] attributeUpdateability; - boolean forceDynamicUpdate; - if ( entityPersister().isDynamicUpdate() && dirtyAttributeIndexes != null ) { + final boolean forceDynamicUpdate; + if ( temporalExcludedUpdate ) { + attributeUpdateability = getPropertiesToUpdate( dirtyAttributeIndexes, hasDirtyCollection ); + for ( int i = 0; i < attributeUpdateability.length; i++ ) { + if ( attributeUpdateability[i] && !entityPersister().isPropertyTemporalExcluded( i ) ) { + attributeUpdateability[i] = false; + } + } + forceDynamicUpdate = true; + } + else if ( entityPersister().isDynamicUpdate() && dirtyAttributeIndexes != null ) { attributeUpdateability = getPropertiesToUpdate( dirtyAttributeIndexes, hasDirtyCollection ); forceDynamicUpdate = true; } @@ -227,7 +245,6 @@ && hasLazyDirtyFields( entityPersister(), dirtyAttributeIndexes ) ) { forceDynamicUpdate = entityPersister().hasUninitializedLazyProperties( entity ); } - return performUpdate( entity, id, @@ -240,7 +257,8 @@ && hasLazyDirtyFields( entityPersister(), dirtyAttributeIndexes ) ) { versionMapping, dirtyAttributeIndexes, attributeUpdateability, - forceDynamicUpdate + forceDynamicUpdate, + temporalExcludedUpdate ); } @@ -253,9 +271,11 @@ protected GeneratedValues performUpdate( Object[] incomingOldValues, boolean hasDirtyCollection, SharedSessionContractImplementor session, - EntityVersionMapping versionMapping, int[] dirtyAttributeIndexes, + EntityVersionMapping versionMapping, + int[] dirtyAttributeIndexes, boolean[] attributeUpdateability, - boolean forceDynamicUpdate) { + boolean forceDynamicUpdate, + boolean temporalExcludedUpdate) { final InclusionChecker dirtinessChecker = (position, attribute) -> isDirty( @@ -277,23 +297,23 @@ protected GeneratedValues performUpdate( entityPersister() ); - final InclusionChecker inclusionChecker = (position, attribute) -> attributeUpdateability[position]; - final var valuesAnalysis = analyzeUpdateValues( entity, values, oldVersion, incomingOldValues, dirtyAttributeIndexes, - inclusionChecker, + createInclusionChecker( attributeUpdateability ), lockingChecker, dirtinessChecker, + temporalExcludedUpdate, rowId, forceDynamicUpdate, session ); - if ( valuesAnalysis.tablesNeedingUpdate.isEmpty() && valuesAnalysis.tablesNeedingDynamicUpdate.isEmpty() ) { + if ( valuesAnalysis.tablesNeedingUpdate.isEmpty() + && valuesAnalysis.tablesNeedingDynamicUpdate.isEmpty() ) { // nothing to do return null; } @@ -382,6 +402,7 @@ protected Supplier handlePotentialImplicitForcedVersionIncremen Object entity, Object id, Object[] values, + Object[] oldValues, Object oldVersion, int[] incomingDirtyAttributeIndexes, SharedSessionContractImplementor session, @@ -432,7 +453,14 @@ else if ( incomingDirtyAttributeIndexes != null ) { // we have just the version being updated - use the special handling assert newVersion != null; - final GeneratedValues generatedValues = doVersionUpdate( entity, id, newVersion, oldVersion, session ); + final var generatedValues = doVersionUpdate( + entity, + id, + newVersion, + oldVersion, + oldValues == null ? values : oldValues, + session + ); return () -> generatedValues; } @@ -442,15 +470,11 @@ private boolean hasUpdateGeneratedValues() { || entityMetamodel.hasPreUpdateGeneratedProperties(); } - private static boolean isValueGenerated(Generator generator) { + private static boolean isValueGenerationOnUpdateInSql(Generator generator, Dialect dialect) { return generator != null + && generator.generatedOnExecution() && generator.generatesOnUpdate() - && generator.generatedOnExecution(); - } - - private static boolean isValueGenerationInSql(Generator generator, Dialect dialect) { - assert isValueGenerated( generator ); - return ( (OnExecutionGenerator) generator ).referenceColumnsInSql( dialect ); + && ( (OnExecutionGenerator) generator ).referenceColumnsInSql( dialect, EventType.UPDATE ); } /** @@ -468,8 +492,17 @@ protected GeneratedValues doVersionUpdate( Object id, Object version, Object oldVersion, + Object[] loadedState, SharedSessionContractImplementor session) { - return doVersionUpdate( entity, id, version, oldVersion, true, session ); + return doVersionUpdate( + entity, + id, + version, + oldVersion, + true, + loadedState, + session + ); } protected GeneratedValues doVersionUpdate( @@ -478,6 +511,7 @@ protected GeneratedValues doVersionUpdate( Object version, Object oldVersion, boolean batching, + Object[] loadedState, SharedSessionContractImplementor session) { assert versionUpdateGroup != null; @@ -497,6 +531,8 @@ protected GeneratedValues doVersionUpdate( ParameterUsage.SET ); + bindPartitionColumnValueBindings( loadedState, session, mutationExecutor.getJdbcValueBindings() ); + // restrict the key mutatingTableDetails.getKeyMapping().breakDownKeyJdbcValues( id, @@ -548,7 +584,7 @@ private int[] preUpdateInMemoryValueGeneration( final int[] fieldsPreUpdateNeeded = new int[generators.length]; int count = 0; for ( int i = 0; i < generators.length; i++ ) { - final Generator generator = generators[i]; + final var generator = generators[i]; if ( generator != null && generator.generatesOnUpdate() && generator.generatedBeforeExecution( object, session ) ) { @@ -566,22 +602,25 @@ private int[] preUpdateInMemoryValueGeneration( return EMPTY_INT_ARRAY; } + public boolean[] getPropertyUpdateability() { + return entityPersister().getPropertyUpdateability(); + } + /** * Transform the array of property indexes to an array of booleans for each attribute, * true when the property is dirty */ protected boolean[] getPropertiesToUpdate(final int[] dirtyProperties, final boolean hasDirtyCollection) { final var persister = entityPersister(); - final var updateability = persister.getPropertyUpdateability(); if ( dirtyProperties == null ) { - return updateability; + return getPropertyUpdateability(); } else { + final var updateability = persister.getPropertyUpdateability(); + final var insertability = persister.getPropertyInsertability(); final var propsToUpdate = new boolean[persister.getNumberOfAttributeMappings()]; for ( int property: dirtyProperties ) { - if ( updateability[property] ) { - propsToUpdate[property] = true; - } + propsToUpdate[property] = includeProperty( insertability, updateability, property ); } if ( persister.isVersioned() ) { final var versionAttribute = persister.getVersionMapping().getVersionAttribute(); @@ -600,7 +639,11 @@ protected boolean[] getPropertiesToUpdate(final int[] dirtyProperties, final boo } } - private UpdateValuesAnalysisImpl analyzeUpdateValues( + protected boolean includeProperty(boolean[] insertability, boolean[] updateability, int property) { + return updateability[property]; + } + + protected UpdateValuesAnalysisImpl analyzeUpdateValues( Object entity, Object[] values, Object oldVersion, @@ -609,6 +652,7 @@ private UpdateValuesAnalysisImpl analyzeUpdateValues( InclusionChecker inclusionChecker, InclusionChecker lockingChecker, InclusionChecker dirtinessChecker, + boolean restrictToTemporalExcluded, Object rowId, boolean forceDynamicUpdate, SharedSessionContractImplementor session) { @@ -630,30 +674,29 @@ private UpdateValuesAnalysisImpl analyzeUpdateValues( forceDynamicUpdate ); - final var propertyUpdateability = persister.getPropertyUpdateability(); - for ( int attributeIndex = 0; attributeIndex < attributeMappings.size(); attributeIndex++ ) { final var attributeMapping = attributeMappings.get( attributeIndex ); analysis.startingAttribute( attributeMapping ); try { if ( attributeMapping.getJdbcTypeCount() > 0 - && attributeMapping instanceof SingularAttributeMapping asSingularAttributeMapping ) { + && attributeMapping instanceof SingularAttributeMapping singularAttributeMapping ) { processAttribute( entity, analysis, attributeIndex, - asSingularAttributeMapping, + singularAttributeMapping, oldVersion, oldValues, inclusionChecker, lockingChecker, + restrictToTemporalExcluded, session ); // In this case we check for exactly DirtynessStatus.DIRTY so to not log warnings when the user didn't get it wrong: if ( analysis.currentAttributeAnalysis.getDirtynessStatus() == AttributeAnalysis.DirtynessStatus.DIRTY ) { - if ( !propertyUpdateability[attributeIndex] ) { + if ( !includeProperty( persister.getPropertyInsertability(), persister.getPropertyUpdateability(), attributeIndex ) ) { CORE_LOGGER.ignoreImmutablePropertyModification( attributeMapping.getAttributeName(), persister.getEntityName() ); } } @@ -676,14 +719,36 @@ private void processAttribute( Object[] oldValues, InclusionChecker inclusionChecker, InclusionChecker lockingChecker, + boolean restrictToTemporalExcluded, SharedSessionContractImplementor session) { - final var generator = attributeMapping.getGenerator(); - final boolean generated = isValueGenerated( generator ); + final var generator = + restrictToTemporalExcluded + && !entityPersister().isPropertyTemporalExcluded( attributeIndex ) + ? null + : attributeMapping.getGenerator(); + final boolean generatesOnUpdate = + generator != null + && generator.generatesOnUpdate(); final boolean needsDynamicUpdate = - generated && session != null && generator.generatedBeforeExecution( entity, session ); - final boolean generatedInSql = generated && isValueGenerationInSql( generator, dialect ); - if ( generatedInSql && !needsDynamicUpdate && !( (OnExecutionGenerator) generator ).writePropertyValue() ) { + generatesOnUpdate + && session != null + && generator.generatedBeforeExecution( entity, session ) + // Only force dynamic update when the generator can switch to on-execution mode. + && generator.generatedOnExecution(); + final boolean generatedOnExecution = + generatesOnUpdate + && ( session == null + ? generator.generatedOnExecution() + : generator.generatedOnExecution( entity, session ) + ); + final boolean generatedInSql = + generatedOnExecution + && generator instanceof OnExecutionGenerator onExecutionGenerator + && hasValueGenerationOnExecution( onExecutionGenerator, dialect, EventType.UPDATE ); + if ( generatedInSql + && !needsDynamicUpdate + && !( (OnExecutionGenerator) generator ).writePropertyValue( EventType.UPDATE ) ) { analysis.registerValueGeneratedInSqlNoWrite(); } @@ -716,7 +781,7 @@ private Object attributeLockValue( } private void processSet(UpdateValuesAnalysisImpl analysis, SelectableMapping selectable, boolean needsDynamicUpdate) { - if ( selectable != null && !selectable.isFormula() && selectable.isUpdateable() ) { + if ( selectable != null && !selectable.isFormula() && isColumnIncludedInSet( selectable ) ) { final var tableMapping = physicalTableMappingForMutation( entityPersister(), selectable ); analysis.registerColumnSet( tableMapping, selectable.getSelectionExpression(), selectable.getWriteExpression() ); if ( needsDynamicUpdate ) { @@ -725,6 +790,14 @@ private void processSet(UpdateValuesAnalysisImpl analysis, SelectableMapping sel } } + protected boolean isColumnIncludedInSet(SelectableMapping selectable) { + return selectable.isUpdateable(); + } + + protected InclusionChecker createInclusionChecker(boolean[] attributeUpdateability) { + return (position, attribute) -> attributeUpdateability[position]; + } + private void processLock( UpdateValuesAnalysisImpl analysis, SingularAttributeMapping attributeMapping, @@ -760,6 +833,7 @@ protected GeneratedValues doStaticUpdate( final var mutationExecutor = executor( session, staticUpdateGroup, false ); decomposeForUpdate( + entity, id, rowId, values, @@ -770,7 +844,9 @@ protected GeneratedValues doStaticUpdate( (position, attribute) -> AttributeAnalysis.DirtynessStatus.CONSIDER_LIKE_DIRTY, session ); - bindPartitionColumnValueBindings( oldValues, session, mutationExecutor.getJdbcValueBindings() ); + // no snapshot when called from StatelessSession.update() + bindPartitionColumnValueBindings( oldValues == null ? values : oldValues, + session, mutationExecutor.getJdbcValueBindings() ); try { return mutationExecutor.execute( @@ -789,6 +865,7 @@ protected GeneratedValues doStaticUpdate( } protected void decomposeForUpdate( + Object entity, Object id, Object rowId, Object[] values, @@ -797,23 +874,23 @@ protected void decomposeForUpdate( MutationOperationGroup jdbcOperationGroup, DirtinessChecker dirtinessChecker, SharedSessionContractImplementor session) { - final JdbcValueBindings jdbcValueBindings = mutationExecutor.getJdbcValueBindings(); + final var jdbcValueBindings = mutationExecutor.getJdbcValueBindings(); // apply values for ( int position = 0; position < jdbcOperationGroup.getNumberOfOperations(); position++ ) { final var operation = jdbcOperationGroup.getOperation( position ); final var tableMapping = (EntityTableMapping) operation.getTableDetails(); if ( valuesAnalysis.tablesNeedingUpdate.contains( tableMapping ) ) { - final int[] attributeIndexes = tableMapping.getAttributeIndexes(); - for ( int i = 0; i < attributeIndexes.length; i++ ) { + for ( int attributeIndex : tableMapping.getAttributeIndexes() ) { decomposeAttributeForUpdate( + entity, values, valuesAnalysis, dirtinessChecker, session, jdbcValueBindings, tableMapping, - attributeIndexes[ i ] + attributeIndex ); } } @@ -828,6 +905,7 @@ protected void decomposeForUpdate( } private void decomposeAttributeForUpdate( + Object entity, Object[] values, UpdateValuesAnalysisImpl valuesAnalysis, DirtinessChecker dirtinessChecker, @@ -844,7 +922,14 @@ private void decomposeAttributeForUpdate( if ( attributeAnalysis.includeInSet() ) { // apply the new values if ( includeInSet( dirtinessChecker, attributeIndex, attributeMapping, attributeAnalysis ) ) { - decomposeAttributeMapping( session, jdbcValueBindings, tableMapping, attributeMapping, values[attributeIndex] ); + decomposeAttributeMapping( + session, + jdbcValueBindings, + tableMapping, + attributeMapping, + values[attributeIndex], + entity + ); } } @@ -873,19 +958,42 @@ private static void optimisticLock( } ); } - private static void decomposeAttributeMapping( + private void decomposeAttributeMapping( SharedSessionContractImplementor session, JdbcValueBindings jdbcValueBindings, EntityTableMapping tableMapping, AttributeMapping attributeMapping, - Object values) { + Object values, + Object entity) { + final var generator = attributeMapping.getGenerator(); + final OnExecutionGenerator onExecutionGenerator; + final String[] columnValues; + final boolean[] columnInclusions; + final boolean bindAllValues; + if ( generator instanceof OnExecutionGenerator executionGenerator + && generator.generatedOnExecution( entity, session ) + && generator.generatesOnUpdate() ) { + onExecutionGenerator = executionGenerator; + columnValues = onExecutionGenerator.getReferencedColumnValues( dialect(), EventType.UPDATE ); + columnInclusions = onExecutionGenerator.getColumnInclusions( dialect(), EventType.UPDATE ); + bindAllValues = onExecutionGenerator.writePropertyValue( EventType.UPDATE ) && columnValues == null; + } + else { + onExecutionGenerator = null; + columnValues = null; + columnInclusions = null; + bindAllValues = false; + } + attributeMapping.decompose( values, 0, jdbcValueBindings, tableMapping, (valueIndex, bindings, table, jdbcValue, jdbcMapping) -> { - if ( !jdbcMapping.isFormula() && jdbcMapping.isUpdateable() ) { + if ( !jdbcMapping.isFormula() + && isColumnIncludedInSet( jdbcMapping ) + && shouldBindValue( onExecutionGenerator, columnValues, columnInclusions, bindAllValues, valueIndex ) ) { bindings.bindValue( jdbcValue, table.getTableName(), @@ -898,6 +1006,24 @@ private static void decomposeAttributeMapping( ); } + private static boolean shouldBindValue( + OnExecutionGenerator onExecutionGenerator, + String[] columnValues, + boolean[] columnInclusions, + boolean bindAllValues, + int valueIndex) { + if ( onExecutionGenerator == null ) { + return true; + } + else if ( columnInclusions != null && !columnInclusions[valueIndex] ) { + return false; + } + else { + return bindAllValues + || columnValues != null && "?".equals( columnValues[valueIndex] ); + } + } + private boolean includeInSet( DirtinessChecker dirtinessChecker, int attributeIndex, @@ -944,6 +1070,7 @@ protected GeneratedValues doDynamicUpdate( final var mutationExecutor = executor( session, dynamicUpdateGroup, true ); decomposeForUpdate( + entity, id, rowId, values, @@ -956,7 +1083,9 @@ protected GeneratedValues doDynamicUpdate( : AttributeAnalysis.DirtynessStatus.NOT_DIRTY, session ); - bindPartitionColumnValueBindings( oldValues, session, mutationExecutor.getJdbcValueBindings() ); + // no snapshot when called from StatelessSession.update() + bindPartitionColumnValueBindings( oldValues == null ? values : oldValues, + session, mutationExecutor.getJdbcValueBindings() ); try { return mutationExecutor.execute( @@ -1017,22 +1146,6 @@ protected BatchKey getVersionUpdateBatchkey(){ return versionUpdateBatchkey; } - private boolean resultCheck( - Object id, PreparedStatementDetails statementDetails, int affectedRowCount, int batchPosition) { - return identifiedResultsCheck( - statementDetails, - affectedRowCount, - batchPosition, - entityPersister, - id, - factory - ); - } - - private StaleObjectStateException staleObjectStateException(Object id, StaleStateException staleStateException) { - return new StaleObjectStateException( entityPersister.getEntityName(), id, staleStateException ); - } - protected MutationOperationGroup generateDynamicUpdateGroup( Object entity, Object id, @@ -1044,14 +1157,11 @@ protected MutationOperationGroup generateDynamicUpdateGroup( entityPersister().forEachMutableTable( (tableMapping) -> { final var tableReference = new MutatingTableReference( tableMapping ); - final TableMutationBuilder tableUpdateBuilder; - if ( ! valuesAnalysis.tablesNeedingUpdate.contains( tableReference.getTableMapping() ) ) { - // this table does not need updating - tableUpdateBuilder = new TableUpdateBuilderSkipped( tableReference ); - } - else { - tableUpdateBuilder = createTableUpdateBuilder( tableMapping ); - } + final var tableUpdateBuilder = + valuesAnalysis.tablesNeedingUpdate.contains( tableReference.getTableMapping() ) + ? createTableUpdateBuilder( tableMapping ) + // this table does not need updating + : new TableUpdateBuilderSkipped( tableReference ); updateGroupBuilder.addTableDetailsBuilder( tableUpdateBuilder ); } ); @@ -1099,16 +1209,13 @@ private void applyTableUpdateDetails( updateGroupBuilder.forEachTableMutationBuilder( (builder) -> { final var tableMapping = (EntityTableMapping) builder.getMutatingTable().getTableMapping(); - final int[] attributeIndexes = tableMapping.getAttributeIndexes(); - for ( int i = 0; i < attributeIndexes.length; i++ ) { - final int attributeIndex = attributeIndexes[i]; - + for ( final int attributeIndex : tableMapping.getAttributeIndexes() ) { final var attributeMapping = attributeMappings.get( attributeIndex ); final var attributeAnalysis = updateValuesAnalysis.attributeAnalyses.get( attributeIndex ); if ( attributeAnalysis.includeInSet() ) { assert updateValuesAnalysis.tablesNeedingUpdate.contains( tableMapping ) - || updateValuesAnalysis.tablesNeedingDynamicUpdate.contains( tableMapping ); + || updateValuesAnalysis.tablesNeedingDynamicUpdate.contains( tableMapping ); applyAttributeUpdateDetails( entity, updateGroupBuilder, @@ -1154,23 +1261,6 @@ private void applyTableUpdateDetails( } ); } - private void applyPartitionKeyRestriction(TableUpdateBuilder tableUpdateBuilder) { - final var persister = entityPersister(); - if ( persister.hasPartitionedSelectionMapping() ) { - final var attributeMappings = persister.getAttributeMappings(); - for ( int m = 0; m < attributeMappings.size(); m++ ) { - final var attributeMapping = attributeMappings.get( m ); - final int jdbcTypeCount = attributeMapping.getJdbcTypeCount(); - for ( int i = 0; i < jdbcTypeCount; i++ ) { - final var selectableMapping = attributeMapping.getSelectable( i ); - if ( selectableMapping.isPartitioned() ) { - tableUpdateBuilder.addKeyRestrictionLeniently( selectableMapping ); - } - } - } - } - } - private static void applyAttributeLockingDetails( Object[] oldValues, SharedSessionContractImplementor session, @@ -1236,8 +1326,9 @@ private void applyAttributeUpdateDetails( TableUpdateBuilder tableUpdateBuilder, SharedSessionContractImplementor session) { final var generator = attributeMapping.getGenerator(); - if ( needsValueGeneration( entity, session, generator ) ) { - handleValueGeneration( attributeMapping, updateGroupBuilder, (OnExecutionGenerator) generator ); + if ( generator instanceof OnExecutionGenerator onExecutionGenerator + && hasValueGenerationOnExecution( entity, session, onExecutionGenerator, EventType.UPDATE ) ) { + handleValueGeneration( attributeMapping, updateGroupBuilder, onExecutionGenerator, EventType.UPDATE ); } else if ( versionMapping != null && versionMapping.getVersionAttribute() == attributeMapping) { @@ -1248,15 +1339,13 @@ else if ( versionMapping != null || dirtinessChecker == null || dirtinessChecker.isDirty( attributeIndex, attributeMapping ).isDirty(); if ( includeInSet ) { - attributeMapping.forEachUpdatable( tableUpdateBuilder ); + forEachUpdatable( attributeMapping, tableUpdateBuilder ); } } } - private boolean needsValueGeneration(Object entity, SharedSessionContractImplementor session, Generator generator) { - return isValueGenerated( generator ) - && (session == null && generator.generatedOnExecution() || generator.generatedOnExecution( entity, session ) ) - && isValueGenerationInSql( generator, dialect ); + protected void forEachUpdatable(AttributeMapping attributeMapping, TableUpdateBuilder tableUpdateBuilder) { + attributeMapping.forEachUpdatable( tableUpdateBuilder ); } /** @@ -1297,9 +1386,8 @@ public UpdateValuesAnalysisImpl( tablesWithNonNullValues.add( tableMapping ); } else { - for ( int i = 0; i < tableMapping.getAttributeIndexes().length; i++ ) { - final int attributeIndex = tableMapping.getAttributeIndexes()[ i ]; - if ( values[ attributeIndex ] != null ) { + for ( final int attributeIndex : tableMapping.getAttributeIndexes() ) { + if ( values[attributeIndex] != null ) { tablesWithNonNullValues.add( tableMapping ); break; } @@ -1314,9 +1402,8 @@ public UpdateValuesAnalysisImpl( tablesWithPreviousNonNullValues.add( tableMapping ); } else { - for ( int i = 0; i < tableMapping.getAttributeIndexes().length; i++ ) { - final int attributeIndex = tableMapping.getAttributeIndexes()[ i ]; - if ( oldValues[ attributeIndex ] != null ) { + for ( final int attributeIndex : tableMapping.getAttributeIndexes() ) { + if ( oldValues[attributeIndex] != null ) { tablesWithPreviousNonNullValues.add( tableMapping ); break; } @@ -1614,7 +1701,7 @@ public Object getLockValue() { } } - private MutationOperationGroup buildStaticUpdateGroup() { + protected MutationOperationGroup buildStaticUpdateGroup() { final var persister = entityPersister(); final var valuesAnalysis = analyzeUpdateValues( null, @@ -1622,10 +1709,7 @@ private MutationOperationGroup buildStaticUpdateGroup() { null, null, null, - (index,attribute) -> - isValueGenerated( attribute.getGenerator() ) - && isValueGenerationInSql( attribute.getGenerator(), dialect() ) - || persister.getPropertyUpdateability()[index], + (index, attribute) -> includeInStaticUpdate( index, attribute, persister.getPropertyUpdateability() ), (index,attribute) -> switch ( persister.optimisticLockStyle() ) { case ALL -> true; @@ -1636,6 +1720,7 @@ && isValueGenerationInSql( attribute.getGenerator(), dialect() ) default -> false; }, (index,attribute) -> true, + false, "", // pass anything here to generate the row id restriction if possible false, null @@ -1667,6 +1752,14 @@ && isValueGenerationInSql( attribute.getGenerator(), dialect() ) return createOperationGroup( valuesAnalysis, updateGroupBuilder.buildMutationGroup() ); } + protected boolean includeInStaticUpdate( + int index, + AttributeMapping attribute, + boolean[] propertyUpdateability) { + return isValueGenerationOnUpdateInSql( attribute.getGenerator(), dialect() ) + || propertyUpdateability[index]; + } + private MutationOperationGroup buildVersionUpdateGroup() { final var versionMapping = entityPersister().getVersionMapping(); if ( versionMapping == null ) { @@ -1683,7 +1776,7 @@ private MutationOperationGroup buildVersionUpdateGroup() { updateBuilder.addKeyRestrictionsLeniently( identifierTableMapping.getKeyMapping() ); - updateBuilder.addOptimisticLockRestriction( versionMapping ); + applyVersionOptimisticLocking( updateBuilder ); applyPartitionKeyRestriction( updateBuilder ); //noinspection resource @@ -1710,8 +1803,8 @@ protected interface DirtinessChecker { public boolean hasLazyDirtyFields(EntityPersister persister, int[] dirtyFields) { final var propertyLaziness = persister.getPropertyLaziness(); - for ( int i = 0; i < dirtyFields.length; i++ ) { - if ( propertyLaziness[dirtyFields[i]] ) { + for ( int dirtyField : dirtyFields ) { + if ( propertyLaziness[dirtyField] ) { return true; } } @@ -1721,10 +1814,9 @@ public boolean hasLazyDirtyFields(EntityPersister persister, int[] dirtyFields) public EntityTableMapping physicalTableMappingForMutation( EntityPersister persister, SelectableMapping selectableMapping) { final String tableNameForMutation = persister.physicalTableNameForMutation( selectableMapping ); - final var tableMappings = persister.getTableMappings(); - for ( int i = 0; i < tableMappings.length; i++ ) { - if ( tableNameForMutation.equals( tableMappings[i].getTableName() ) ) { - return tableMappings[i]; + for ( var tableMapping : persister.getTableMappings() ) { + if ( tableNameForMutation.equals( tableMapping.getTableName() ) ) { + return tableMapping; } } diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/UpdateCoordinatorTemporal.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/UpdateCoordinatorTemporal.java new file mode 100644 index 000000000000..3877859421bd --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/UpdateCoordinatorTemporal.java @@ -0,0 +1,111 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.persister.entity.mutation; + +import org.hibernate.engine.jdbc.batch.internal.BasicBatchKey; +import org.hibernate.engine.jdbc.mutation.JdbcValueBindings; +import org.hibernate.engine.jdbc.mutation.ParameterUsage; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.generator.values.GeneratedValues; +import org.hibernate.metamodel.mapping.TemporalMapping; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.sql.model.MutationOperationGroup; + + +/** + * Update coordinator for + * {@link org.hibernate.temporal.TemporalTableStrategy#SINGLE_TABLE} + * temporal strategy. + * + * @author Gavin King + */ +public class UpdateCoordinatorTemporal extends AbstractTemporalUpdateCoordinator { + private final TemporalMapping temporalMapping; + private final MutationOperationGroup endingUpdateGroup; + private final BasicBatchKey batchKey; + private final UpdateCoordinator versionUpdateDelegate; + + public UpdateCoordinatorTemporal( + EntityPersister entityPersister, + SessionFactoryImplementor factory) { + super( entityPersister, factory ); + this.temporalMapping = entityPersister.getTemporalMapping(); + this.endingUpdateGroup = buildEndingUpdateGroup( entityPersister.getIdentifierTableMapping(), temporalMapping ); + this.batchKey = new BasicBatchKey( entityPersister.getEntityName() + "#TEMPORAL_UPDATE" ); + this.versionUpdateDelegate = new UpdateCoordinatorStandard( entityPersister, factory ); + } + + @Override + public MutationOperationGroup getStaticMutationOperationGroup() { + return endingUpdateGroup; + } + + @Override + protected BasicBatchKey getBatchKey() { + return batchKey; + } + + @Override + public GeneratedValues update( + Object entity, + Object id, + Object rowId, + Object[] values, + Object oldVersion, + Object[] incomingOldValues, + int[] dirtyAttributeIndexes, + boolean hasDirtyCollection, + SharedSessionContractImplementor session) { + if ( entityPersister() + .excludedFromTemporalVersioning( dirtyAttributeIndexes, hasDirtyCollection ) ) { + return versionUpdateDelegate.update( + entity, + id, + rowId, + values, + oldVersion, + incomingOldValues, + dirtyAttributeIndexes, + hasDirtyCollection, + session + ); + } + else { + performRowEndUpdate( + entity, + id, + rowId, + oldVersion, + session, + temporalMapping, + endingUpdateGroup, + entityPersister() + .physicalTableNameForMutation( temporalMapping.getEndingColumnMapping() ), + (statementDetails, affectedRowCount, batchPosition) -> + resultCheck( id, statementDetails, affectedRowCount, batchPosition ) + + ); + return entityPersister().getInsertCoordinator().insert( entity, id, values, session ); + } + } + + @Override + void bindVersionRestriction(Object oldVersion, JdbcValueBindings jdbcValueBindings, String temporalTableName) { + final var versionMapping = entityPersister().getVersionMapping(); + if ( versionMapping != null && entityPersister().optimisticLockStyle().isVersion() ) { + jdbcValueBindings.bindValue( oldVersion, versionMapping, ParameterUsage.RESTRICT ); + } + } + + @Override + public void forceVersionIncrement( + Object id, + Object currentVersion, + Object nextVersion, + SharedSessionContractImplementor session) { + versionUpdateDelegate.forceVersionIncrement( id, currentVersion, nextVersion, session ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/persister/state/internal/AbstractStateManagement.java b/hibernate-core/src/main/java/org/hibernate/persister/state/internal/AbstractStateManagement.java new file mode 100644 index 000000000000..dc4a73c9c4ae --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/persister/state/internal/AbstractStateManagement.java @@ -0,0 +1,225 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.persister.state.internal; + +import org.hibernate.mapping.Collection; +import org.hibernate.mapping.RootClass; +import org.hibernate.metamodel.mapping.AuxiliaryMapping; +import org.hibernate.metamodel.mapping.PluralAttributeMapping; +import org.hibernate.metamodel.mapping.SingularAttributeMapping; +import org.hibernate.metamodel.mapping.internal.MappingModelCreationProcess; +import org.hibernate.persister.collection.CollectionPersister; +import org.hibernate.persister.collection.OneToManyPersister; +import org.hibernate.persister.collection.mutation.CollectionMutationTarget; +import org.hibernate.persister.collection.mutation.DeleteRowsCoordinator; +import org.hibernate.persister.collection.mutation.DeleteRowsCoordinatorNoOp; +import org.hibernate.persister.collection.mutation.DeleteRowsCoordinatorStandard; +import org.hibernate.persister.collection.mutation.DeleteRowsCoordinatorTablePerSubclass; +import org.hibernate.persister.collection.mutation.InsertRowsCoordinator; +import org.hibernate.persister.collection.mutation.InsertRowsCoordinatorNoOp; +import org.hibernate.persister.collection.mutation.InsertRowsCoordinatorStandard; +import org.hibernate.persister.collection.mutation.InsertRowsCoordinatorTablePerSubclass; +import org.hibernate.persister.collection.mutation.RemoveCoordinator; +import org.hibernate.persister.collection.mutation.RemoveCoordinatorNoOp; +import org.hibernate.persister.collection.mutation.RemoveCoordinatorStandard; +import org.hibernate.persister.collection.mutation.RemoveCoordinatorTablePerSubclass; +import org.hibernate.persister.collection.mutation.UpdateRowsCoordinator; +import org.hibernate.persister.collection.mutation.UpdateRowsCoordinatorNoOp; +import org.hibernate.persister.collection.mutation.UpdateRowsCoordinatorOneToMany; +import org.hibernate.persister.collection.mutation.UpdateRowsCoordinatorStandard; +import org.hibernate.persister.collection.mutation.UpdateRowsCoordinatorTablePerSubclass; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.persister.entity.UnionSubclassEntityPersister; +import org.hibernate.persister.entity.mutation.DeleteCoordinator; +import org.hibernate.persister.entity.mutation.DeleteCoordinatorStandard; +import org.hibernate.persister.entity.mutation.InsertCoordinator; +import org.hibernate.persister.entity.mutation.InsertCoordinatorStandard; +import org.hibernate.persister.entity.mutation.MergeCoordinatorStandard; +import org.hibernate.persister.entity.mutation.UpdateCoordinator; +import org.hibernate.persister.entity.mutation.UpdateCoordinatorNoOp; +import org.hibernate.persister.entity.mutation.UpdateCoordinatorStandard; +import org.hibernate.persister.state.spi.StateManagement; + +import static org.hibernate.internal.util.collections.ArrayHelper.isAnyTrue; + +/** + * @author Gavin King + * + * @since 7.4 + */ +public abstract class AbstractStateManagement implements StateManagement { + @Override + public InsertCoordinator createInsertCoordinator(EntityPersister persister) { + return new InsertCoordinatorStandard( persister, persister.getFactory() ); + } + + @Override + public UpdateCoordinator createUpdateCoordinator(EntityPersister persister) { + final var attributeMappings = persister.getAttributeMappings(); + for ( int i = 0; i < attributeMappings.size(); i++ ) { + if ( attributeMappings.get( i ) instanceof SingularAttributeMapping ) { + return new UpdateCoordinatorStandard( persister, persister.getFactory() ); + } + } + return new UpdateCoordinatorNoOp( persister ); + } + + @Override + public UpdateCoordinator createMergeCoordinator(EntityPersister persister) { + return new MergeCoordinatorStandard( persister, persister.getFactory() ); + } + + @Override + public DeleteCoordinator createDeleteCoordinator(EntityPersister persister) { + return new DeleteCoordinatorStandard( persister, persister.getFactory() ); + } + + @Override + public InsertRowsCoordinator createInsertRowsCoordinator(CollectionPersister persister) { + final var mutationTarget = resolveMutationTarget( persister ); + if ( !isInsertAllowed( persister ) ) { + return new InsertRowsCoordinatorNoOp( mutationTarget ); + } + else if ( persister.isOneToMany() && isTablePerSubclass( persister ) ) { + return new InsertRowsCoordinatorTablePerSubclass( + (OneToManyPersister) mutationTarget, + persister.getRowMutationOperations(), + persister.getFactory().getServiceRegistry() + ); + } + else { + return new InsertRowsCoordinatorStandard( + mutationTarget, + persister.getRowMutationOperations(), + persister.getFactory().getServiceRegistry() + ); + } + } + + @Override + public UpdateRowsCoordinator createUpdateRowsCoordinator(CollectionPersister persister) { + final var mutationTarget = resolveMutationTarget( persister ); + if ( !isUpdatePossible( persister ) ) { + return new UpdateRowsCoordinatorNoOp( mutationTarget ); + } + else if ( persister.isOneToMany() ) { + if ( isTablePerSubclass( persister ) ) { + return new UpdateRowsCoordinatorTablePerSubclass( + (OneToManyPersister) mutationTarget, + persister.getRowMutationOperations(), + persister.getFactory() + ); + } + else { + return new UpdateRowsCoordinatorOneToMany( + mutationTarget, + persister.getRowMutationOperations(), + persister.getFactory() + ); + } + } + else { + return new UpdateRowsCoordinatorStandard( + mutationTarget, + persister.getRowMutationOperations(), + persister.getFactory() + ); + } + } + + @Override + public DeleteRowsCoordinator createDeleteRowsCoordinator(CollectionPersister persister) { + final var mutationTarget = resolveMutationTarget( persister ); + if ( !persister.needsRemove() ) { + return new DeleteRowsCoordinatorNoOp( mutationTarget ); + } + else if ( persister.isOneToMany() && isTablePerSubclass( persister ) ) { + return new DeleteRowsCoordinatorTablePerSubclass( + (OneToManyPersister) mutationTarget, + persister.getRowMutationOperations(), + false, + persister.getFactory().getServiceRegistry() + ); + } + else { + return new DeleteRowsCoordinatorStandard( + mutationTarget, + persister.getRowMutationOperations(), + !persister.isOneToMany() + && mutationTarget.hasPhysicalIndexColumn(), + persister.getFactory().getServiceRegistry() + ); + } + } + + @Override + public RemoveCoordinator createRemoveCoordinator(CollectionPersister persister) { + final var mutationTarget = resolveMutationTarget( persister ); + if ( !persister.needsRemove() ) { + return new RemoveCoordinatorNoOp( mutationTarget ); + } + else if ( persister.isOneToMany() && isTablePerSubclass( persister ) ) { + return new RemoveCoordinatorTablePerSubclass( + (OneToManyPersister) mutationTarget, + persister.getRowMutationOperations(), + persister.getFactory().getServiceRegistry() + ); + } + else { + return new RemoveCoordinatorStandard( + mutationTarget, + persister.getRowMutationOperations(), + persister.getFactory().getServiceRegistry() + ); + } + } + + protected static boolean isUpdatePossible(CollectionPersister persister) { + if ( persister.isOneToMany() ) { + return persister.isRowDeleteEnabled() + || persister.isRowInsertEnabled(); + } + else { + return !persister.isInverse() + && persister.getCollectionSemantics().getCollectionClassification().isRowUpdatePossible() + && isAnyTrue( persister.getElementColumnIsSettable() ); + } + } + + protected static boolean isInsertAllowed(CollectionPersister persister) { + return !persister.isInverse() && persister.isRowInsertEnabled(); + } + + protected boolean isTablePerSubclass(CollectionPersister persister) { + final var elementPersister = persister.getElementPersister(); + return elementPersister != null + && elementPersister.hasSubclasses() + && elementPersister instanceof UnionSubclassEntityPersister; + } + + protected static CollectionMutationTarget resolveMutationTarget(CollectionPersister persister) { + if ( persister instanceof CollectionMutationTarget collectionMutationTarget ) { + return collectionMutationTarget; + } + throw new IllegalArgumentException( "CollectionPersister does not implement CollectionMutationTarget" ); + } + + + @Override + public AuxiliaryMapping createAuxiliaryMapping( + EntityPersister persister, + RootClass rootClass, + MappingModelCreationProcess creationProcess) { + return null; + } + + @Override + public AuxiliaryMapping createAuxiliaryMapping( + PluralAttributeMapping pluralAttributeMapping, + Collection bootDescriptor, + MappingModelCreationProcess creationProcess) { + return null; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/persister/state/internal/AuditStateManagement.java b/hibernate-core/src/main/java/org/hibernate/persister/state/internal/AuditStateManagement.java new file mode 100644 index 000000000000..a196b3375adf --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/persister/state/internal/AuditStateManagement.java @@ -0,0 +1,194 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.persister.state.internal; + +import org.hibernate.mapping.Collection; +import org.hibernate.mapping.RootClass; +import org.hibernate.metamodel.mapping.AuditMapping; +import org.hibernate.metamodel.mapping.PluralAttributeMapping; +import org.hibernate.metamodel.mapping.internal.AuditMappingImpl; +import org.hibernate.metamodel.mapping.internal.MappingModelCreationProcess; +import org.hibernate.persister.collection.CollectionPersister; +import org.hibernate.persister.collection.mutation.DeleteRowsCoordinator; +import org.hibernate.persister.collection.mutation.DeleteRowsCoordinatorNoOp; +import org.hibernate.persister.collection.mutation.DeleteRowsCoordinatorAudit; +import org.hibernate.persister.collection.mutation.InsertRowsCoordinator; +import org.hibernate.persister.collection.mutation.InsertRowsCoordinatorAudit; +import org.hibernate.persister.collection.mutation.InsertRowsCoordinatorNoOp; +import org.hibernate.persister.collection.mutation.RemoveCoordinator; +import org.hibernate.persister.collection.mutation.RemoveCoordinatorNoOp; +import org.hibernate.persister.collection.mutation.UpdateRowsCoordinator; +import org.hibernate.persister.collection.mutation.UpdateRowsCoordinatorAudit; +import org.hibernate.persister.collection.mutation.UpdateRowsCoordinatorNoOp; +import org.hibernate.persister.entity.AbstractEntityPersister; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.persister.entity.mutation.DeleteCoordinator; +import org.hibernate.persister.entity.mutation.DeleteCoordinatorAudit; +import org.hibernate.persister.entity.mutation.InsertCoordinator; +import org.hibernate.persister.entity.mutation.InsertCoordinatorAudit; +import org.hibernate.persister.entity.mutation.MergeCoordinatorAudit; +import org.hibernate.persister.entity.mutation.UpdateCoordinator; +import org.hibernate.persister.entity.mutation.UpdateCoordinatorAudit; +import org.hibernate.persister.state.spi.StateManagement; + +import static org.hibernate.metamodel.mapping.internal.MappingModelCreationHelper.getTableIdentifierExpression; +import static org.hibernate.persister.state.internal.AbstractStateManagement.resolveMutationTarget; + +/** + * State management for {@linkplain org.hibernate.annotations.Audited audited} + * entities and collections. + * + * @author Gavin King + * + * @since 7.4 + */ +public class AuditStateManagement implements StateManagement { + public static final AuditStateManagement INSTANCE = new AuditStateManagement(); + + private AuditStateManagement() { + } + + /** + * The modification type stored in the + * {@linkplain org.hibernate.annotations.Audited#modificationType + * modification type column}. + */ + public enum ModificationType { + /** Creation, encoded as 0 */ + ADD, + /** Modification, encoded as 1 */ + MOD, + /** Deletion, encoded as 2 */ + DEL + } + + @Override + public InsertCoordinator createInsertCoordinator(EntityPersister persister) { + return new InsertCoordinatorAudit( persister, persister.getFactory(), + StandardStateManagement.INSTANCE.createInsertCoordinator( persister ) ); + } + + @Override + public UpdateCoordinator createUpdateCoordinator(EntityPersister persister) { + return new UpdateCoordinatorAudit( persister, persister.getFactory(), + StandardStateManagement.INSTANCE.createUpdateCoordinator( persister ) ); + } + + @Override + public UpdateCoordinator createMergeCoordinator(EntityPersister persister) { + return new MergeCoordinatorAudit( persister, persister.getFactory(), + StandardStateManagement.INSTANCE.createMergeCoordinator( persister ) ); + } + + @Override + public DeleteCoordinator createDeleteCoordinator(EntityPersister persister) { + return new DeleteCoordinatorAudit( persister, persister.getFactory(), + StandardStateManagement.INSTANCE.createDeleteCoordinator( persister ) ); + } + + @Override + public InsertRowsCoordinator createInsertRowsCoordinator(CollectionPersister persister) { + final var mutationTarget = resolveMutationTarget( persister ); + if ( !AbstractStateManagement.isInsertAllowed( persister ) ) { + return new InsertRowsCoordinatorNoOp( mutationTarget ); + } + else if ( persister.isOneToMany() ) { + throw new UnsupportedOperationException(); + } + else { + return new InsertRowsCoordinatorAudit( + mutationTarget, + persister.getRowMutationOperations(), + StandardStateManagement.INSTANCE.createInsertRowsCoordinator( persister ), + persister.getIndexColumnIsSettable(), + persister.getElementColumnIsSettable(), + persister.getIndexIncrementer(), + persister.getFactory() + ); + } + } + + @Override + public UpdateRowsCoordinator createUpdateRowsCoordinator(CollectionPersister persister) { + final var mutationTarget = resolveMutationTarget( persister ); + if ( !AbstractStateManagement.isUpdatePossible( persister ) ) { + return new UpdateRowsCoordinatorNoOp( mutationTarget ); + } + else if ( persister.isOneToMany() ) { + throw new UnsupportedOperationException(); + } + else { + return new UpdateRowsCoordinatorAudit( + mutationTarget, + StandardStateManagement.INSTANCE.createUpdateRowsCoordinator( persister ), + persister.getIndexColumnIsSettable(), + persister.getElementColumnIsSettable(), + persister.getIndexIncrementer(), + persister.getFactory() + ); + } + } + + @Override + public DeleteRowsCoordinator createDeleteRowsCoordinator(CollectionPersister persister) { + final var mutationTarget = resolveMutationTarget( persister ); + if ( !persister.needsRemove() ) { + return new DeleteRowsCoordinatorNoOp( mutationTarget ); + } + else if ( persister.isOneToMany() ) { + throw new UnsupportedOperationException(); + } + else { + return new DeleteRowsCoordinatorAudit( + mutationTarget, + StandardStateManagement.INSTANCE.createDeleteRowsCoordinator( persister ), + mutationTarget.hasPhysicalIndexColumn(), + persister.getIndexColumnIsSettable(), + persister.getElementColumnIsSettable(), + persister.getIndexIncrementer(), + persister.getFactory() + ); + } + } + + @Override + public RemoveCoordinator createRemoveCoordinator(CollectionPersister persister) { + final var mutationTarget = resolveMutationTarget( persister ); + if ( !persister.needsRemove() ) { + return new RemoveCoordinatorNoOp( mutationTarget ); + } + else if ( persister.isOneToMany() ) { + throw new UnsupportedOperationException(); + } + else { + return StandardStateManagement.INSTANCE.createRemoveCoordinator( persister ); + } + } + + @Override + public AuditMapping createAuxiliaryMapping( + EntityPersister persister, + RootClass rootClass, + MappingModelCreationProcess creationProcess) { + final var auditTable = rootClass.getAuxiliaryTable(); + final String tableName = auditTable == null + ? persister.getIdentifierTableName() + : ( (AbstractEntityPersister) persister ) + .determineTableName( auditTable ); + return new AuditMappingImpl( rootClass, tableName, creationProcess ); + } + + @Override + public AuditMapping createAuxiliaryMapping( + PluralAttributeMapping pluralAttributeMapping, + Collection bootDescriptor, + MappingModelCreationProcess creationProcess) { + final var auditTable = bootDescriptor.getAuxiliaryTable(); + final String tableName = auditTable == null ? null + : getTableIdentifierExpression( auditTable, creationProcess ); + return new AuditMappingImpl( bootDescriptor, tableName, creationProcess ); + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/persister/state/internal/HistoryStateManagement.java b/hibernate-core/src/main/java/org/hibernate/persister/state/internal/HistoryStateManagement.java new file mode 100644 index 000000000000..8c8101d663f7 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/persister/state/internal/HistoryStateManagement.java @@ -0,0 +1,191 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.persister.state.internal; + +import org.hibernate.mapping.Collection; +import org.hibernate.mapping.RootClass; +import org.hibernate.metamodel.mapping.AuxiliaryMapping; +import org.hibernate.metamodel.mapping.PluralAttributeMapping; +import org.hibernate.metamodel.mapping.internal.MappingModelCreationProcess; +import org.hibernate.metamodel.mapping.internal.TemporalMappingImpl; +import org.hibernate.persister.collection.CollectionPersister; +import org.hibernate.persister.collection.mutation.DeleteRowsCoordinator; +import org.hibernate.persister.collection.mutation.DeleteRowsCoordinatorHistory; +import org.hibernate.persister.collection.mutation.DeleteRowsCoordinatorNoOp; +import org.hibernate.persister.collection.mutation.InsertRowsCoordinator; +import org.hibernate.persister.collection.mutation.InsertRowsCoordinatorHistory; +import org.hibernate.persister.collection.mutation.InsertRowsCoordinatorNoOp; +import org.hibernate.persister.collection.mutation.RemoveCoordinator; +import org.hibernate.persister.collection.mutation.RemoveCoordinatorHistory; +import org.hibernate.persister.collection.mutation.RemoveCoordinatorNoOp; +import org.hibernate.persister.collection.mutation.UpdateRowsCoordinator; +import org.hibernate.persister.collection.mutation.UpdateRowsCoordinatorHistory; +import org.hibernate.persister.collection.mutation.UpdateRowsCoordinatorNoOp; +import org.hibernate.persister.entity.AbstractEntityPersister; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.persister.entity.mutation.DeleteCoordinator; +import org.hibernate.persister.entity.mutation.DeleteCoordinatorHistory; +import org.hibernate.persister.entity.mutation.InsertCoordinator; +import org.hibernate.persister.entity.mutation.InsertCoordinatorHistory; +import org.hibernate.persister.entity.mutation.MergeCoordinatorHistory; +import org.hibernate.persister.entity.mutation.UpdateCoordinator; +import org.hibernate.persister.entity.mutation.UpdateCoordinatorHistory; +import org.hibernate.persister.state.spi.StateManagement; + +import static org.hibernate.metamodel.mapping.internal.MappingModelCreationHelper.getTableIdentifierExpression; +import static org.hibernate.persister.state.internal.AbstractStateManagement.isInsertAllowed; +import static org.hibernate.persister.state.internal.AbstractStateManagement.isUpdatePossible; +import static org.hibernate.persister.state.internal.AbstractStateManagement.resolveMutationTarget; + +/** + * State management for temporal entities and collections with + * {@linkplain org.hibernate.annotations.Temporal.HistoryTable + * history tables}. + * + * @author Gavin King + * + * @since 7.4 + */ +public final class HistoryStateManagement implements StateManagement { + public static final HistoryStateManagement INSTANCE = new HistoryStateManagement(); + + private HistoryStateManagement() { + } + + @Override + public UpdateCoordinator createMergeCoordinator(EntityPersister persister) { + return new MergeCoordinatorHistory( persister, persister.getFactory(), + StandardStateManagement.INSTANCE.createMergeCoordinator( persister ) ); + } + + @Override + public InsertCoordinator createInsertCoordinator(EntityPersister persister) { + return new InsertCoordinatorHistory( persister, persister.getFactory(), + StandardStateManagement.INSTANCE.createInsertCoordinator( persister ) ); + } + + @Override + public UpdateCoordinator createUpdateCoordinator(EntityPersister persister) { + return new UpdateCoordinatorHistory( persister, persister.getFactory(), + StandardStateManagement.INSTANCE.createUpdateCoordinator( persister ) ); + } + + @Override + public DeleteCoordinator createDeleteCoordinator(EntityPersister persister) { + return new DeleteCoordinatorHistory( persister, persister.getFactory(), + StandardStateManagement.INSTANCE.createDeleteCoordinator( persister ) ); + } + + @Override + public InsertRowsCoordinator createInsertRowsCoordinator(CollectionPersister persister) { + final var mutationTarget = resolveMutationTarget( persister ); + if ( !isInsertAllowed( persister ) ) { + return new InsertRowsCoordinatorNoOp( mutationTarget ); + } + else if ( persister.isOneToMany() ) { + throw new UnsupportedOperationException(); + } + else { + return new InsertRowsCoordinatorHistory( + mutationTarget, + persister.getRowMutationOperations(), + StandardStateManagement.INSTANCE.createInsertRowsCoordinator( persister ), + persister.getIndexColumnIsSettable(), + persister.getElementColumnIsSettable(), + persister.getIndexIncrementer(), + persister.getFactory().getServiceRegistry() + ); + } + } + + @Override + public UpdateRowsCoordinator createUpdateRowsCoordinator(CollectionPersister persister) { + final var mutationTarget = resolveMutationTarget( persister ); + if ( !isUpdatePossible( persister ) ) { + return new UpdateRowsCoordinatorNoOp( mutationTarget ); + } + else if ( persister.isOneToMany() ) { + throw new UnsupportedOperationException(); + } + else { + return new UpdateRowsCoordinatorHistory( + mutationTarget, + persister.getRowMutationOperations(), + persister.getFactory(), + persister.getIndexColumnIsSettable(), + persister.getElementColumnIsSettable(), + persister.getIndexIncrementer() + ); + } + } + + @Override + public DeleteRowsCoordinator createDeleteRowsCoordinator(CollectionPersister persister) { + final var mutationTarget = resolveMutationTarget( persister ); + if ( !persister.needsRemove() ) { + return new DeleteRowsCoordinatorNoOp( mutationTarget ); + } + else if ( persister.isOneToMany() ) { + throw new UnsupportedOperationException(); + } + else { + return new DeleteRowsCoordinatorHistory( + mutationTarget, + persister.getRowMutationOperations(), + mutationTarget.hasPhysicalIndexColumn(), + persister.getIndexColumnIsSettable(), + persister.getElementColumnIsSettable(), + persister.getIndexIncrementer(), + persister.getFactory().getServiceRegistry() + ); + } + } + + @Override + public RemoveCoordinator createRemoveCoordinator(CollectionPersister persister) { + final var mutationTarget = resolveMutationTarget( persister ); + if ( !persister.needsRemove() ) { + return new RemoveCoordinatorNoOp( mutationTarget ); + } + else if ( persister.isOneToMany() ) { + throw new UnsupportedOperationException(); + } + else { + return new RemoveCoordinatorHistory( + mutationTarget, + persister.getRowMutationOperations(), + persister.getIndexColumnIsSettable(), + persister.getElementColumnIsSettable(), + persister.getIndexIncrementer(), + persister.getFactory().getServiceRegistry() + ); + } + } + + @Override + public AuxiliaryMapping createAuxiliaryMapping( + EntityPersister persister, + RootClass rootClass, + MappingModelCreationProcess creationProcess) { + final var temporalTable = rootClass.getAuxiliaryTable(); + String tableName = temporalTable == null + ? persister.getIdentifierTableName() + : ( (AbstractEntityPersister) persister ) + .determineTableName( temporalTable ); + return new TemporalMappingImpl( rootClass, tableName, creationProcess ); + } + + @Override + public AuxiliaryMapping createAuxiliaryMapping( + PluralAttributeMapping pluralAttributeMapping, + Collection bootDescriptor, + MappingModelCreationProcess creationProcess) { + final var temporalTable = bootDescriptor.getAuxiliaryTable(); + String tableName = temporalTable == null + ? pluralAttributeMapping.getSeparateCollectionTable() + : getTableIdentifierExpression( temporalTable, creationProcess ); + return new TemporalMappingImpl( bootDescriptor, tableName, creationProcess ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/persister/state/internal/NativeTemporalStateManagement.java b/hibernate-core/src/main/java/org/hibernate/persister/state/internal/NativeTemporalStateManagement.java new file mode 100644 index 000000000000..23b24af67bdd --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/persister/state/internal/NativeTemporalStateManagement.java @@ -0,0 +1,45 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.persister.state.internal; + +import org.hibernate.mapping.Collection; +import org.hibernate.mapping.RootClass; +import org.hibernate.metamodel.mapping.AuxiliaryMapping; +import org.hibernate.metamodel.mapping.PluralAttributeMapping; +import org.hibernate.metamodel.mapping.internal.MappingModelCreationProcess; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.temporal.TemporalTableStrategy; + +/** + * State management for temporal entities and collections with + * {@linkplain TemporalTableStrategy#NATIVE + * dialect-native temporary tables}. + * + * @author Gavin King + * + * @since 7.4 + */ +public final class NativeTemporalStateManagement extends AbstractStateManagement { + public static final NativeTemporalStateManagement INSTANCE = new NativeTemporalStateManagement(); + + private NativeTemporalStateManagement() { + } + + @Override + public AuxiliaryMapping createAuxiliaryMapping( + EntityPersister persister, + RootClass rootClass, + MappingModelCreationProcess creationProcess) { + return TemporalStateManagement.INSTANCE.createAuxiliaryMapping( persister, rootClass, creationProcess); + } + + @Override + public AuxiliaryMapping createAuxiliaryMapping( + PluralAttributeMapping pluralAttributeMapping, + Collection bootDescriptor, + MappingModelCreationProcess creationProcess) { + return TemporalStateManagement.INSTANCE.createAuxiliaryMapping( pluralAttributeMapping, bootDescriptor, creationProcess); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/persister/state/internal/SoftDeleteStateManagement.java b/hibernate-core/src/main/java/org/hibernate/persister/state/internal/SoftDeleteStateManagement.java new file mode 100644 index 000000000000..94a1c27cbd64 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/persister/state/internal/SoftDeleteStateManagement.java @@ -0,0 +1,50 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.persister.state.internal; + +import org.hibernate.mapping.Collection; +import org.hibernate.mapping.RootClass; +import org.hibernate.metamodel.mapping.AuxiliaryMapping; +import org.hibernate.metamodel.mapping.PluralAttributeMapping; +import org.hibernate.metamodel.mapping.internal.MappingModelCreationProcess; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.persister.entity.mutation.DeleteCoordinator; +import org.hibernate.persister.entity.mutation.DeleteCoordinatorSoft; + +import static org.hibernate.boot.model.internal.SoftDeleteHelper.resolveSoftDeleteMapping; + +/** + * @author Gavin King + * + * @since 7.4 + */ +public final class SoftDeleteStateManagement extends AbstractStateManagement { + public static final SoftDeleteStateManagement INSTANCE = new SoftDeleteStateManagement(); + + private SoftDeleteStateManagement() { + } + + @Override + public DeleteCoordinator createDeleteCoordinator(EntityPersister persister) { + return new DeleteCoordinatorSoft( persister, persister.getFactory() ); + } + + @Override + public AuxiliaryMapping createAuxiliaryMapping( + EntityPersister persister, + RootClass rootClass, + MappingModelCreationProcess creationProcess) { + return resolveSoftDeleteMapping( persister, rootClass, persister.getIdentifierTableName(), creationProcess ); + } + + @Override + public AuxiliaryMapping createAuxiliaryMapping( + PluralAttributeMapping pluralAttributeMapping, + Collection bootDescriptor, + MappingModelCreationProcess creationProcess) { + return resolveSoftDeleteMapping( pluralAttributeMapping, bootDescriptor, + pluralAttributeMapping.getSeparateCollectionTable(), creationProcess ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/persister/state/internal/StandardStateManagement.java b/hibernate-core/src/main/java/org/hibernate/persister/state/internal/StandardStateManagement.java new file mode 100644 index 000000000000..b6d4752b61b9 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/persister/state/internal/StandardStateManagement.java @@ -0,0 +1,17 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.persister.state.internal; + +/** + * @author Gavin King + * + * @since 7.4 + */ +public final class StandardStateManagement extends AbstractStateManagement { + public static final StandardStateManagement INSTANCE = new StandardStateManagement(); + + private StandardStateManagement() { + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/persister/state/internal/TemporalStateManagement.java b/hibernate-core/src/main/java/org/hibernate/persister/state/internal/TemporalStateManagement.java new file mode 100644 index 000000000000..4721285d93df --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/persister/state/internal/TemporalStateManagement.java @@ -0,0 +1,106 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.persister.state.internal; + +import org.hibernate.mapping.Collection; +import org.hibernate.mapping.RootClass; +import org.hibernate.metamodel.mapping.PluralAttributeMapping; +import org.hibernate.metamodel.mapping.TemporalMapping; +import org.hibernate.metamodel.mapping.internal.MappingModelCreationProcess; +import org.hibernate.metamodel.mapping.internal.TemporalMappingImpl; +import org.hibernate.persister.collection.CollectionPersister; +import org.hibernate.persister.collection.mutation.UpdateRowsCoordinator; +import org.hibernate.persister.collection.mutation.UpdateRowsCoordinatorNoOp; +import org.hibernate.persister.collection.mutation.UpdateRowsCoordinatorTemporal; +import org.hibernate.persister.entity.AbstractEntityPersister; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.persister.entity.mutation.DeleteCoordinator; +import org.hibernate.persister.entity.mutation.DeleteCoordinatorTemporal; +import org.hibernate.persister.entity.mutation.InsertCoordinator; +import org.hibernate.persister.entity.mutation.InsertCoordinatorTemporal; +import org.hibernate.persister.entity.mutation.MergeCoordinatorTemporal; +import org.hibernate.persister.entity.mutation.UpdateCoordinator; +import org.hibernate.persister.entity.mutation.UpdateCoordinatorTemporal; +import org.hibernate.temporal.TemporalTableStrategy; + +import static org.hibernate.metamodel.mapping.internal.MappingModelCreationHelper.getTableIdentifierExpression; + +/** + * State management for temporal entities and collections in the + * {@linkplain TemporalTableStrategy#SINGLE_TABLE + * single table strategy}. + * + * @author Gavin King + * + * @since 7.4 + */ +public final class TemporalStateManagement extends AbstractStateManagement { + public static final TemporalStateManagement INSTANCE = new TemporalStateManagement(); + + private TemporalStateManagement() { + } + + @Override + public InsertCoordinator createInsertCoordinator(EntityPersister persister) { + return new InsertCoordinatorTemporal( persister, persister.getFactory() ); + } + + @Override + public UpdateCoordinator createUpdateCoordinator(EntityPersister persister) { + return new UpdateCoordinatorTemporal( persister, persister.getFactory() ); + } + + @Override + public UpdateCoordinator createMergeCoordinator(EntityPersister persister) { + return new MergeCoordinatorTemporal( persister, persister.getFactory() ); + } + + @Override + public DeleteCoordinator createDeleteCoordinator(EntityPersister persister) { + return new DeleteCoordinatorTemporal( persister, persister.getFactory() ); + } + + @Override + public UpdateRowsCoordinator createUpdateRowsCoordinator(CollectionPersister persister) { + if ( !isUpdatePossible( persister ) ) { + return new UpdateRowsCoordinatorNoOp( resolveMutationTarget( persister ) ); + } + else if ( persister.isOneToMany() ) { + throw new UnsupportedOperationException(); + } + else { + return new UpdateRowsCoordinatorTemporal( + resolveMutationTarget( persister ), + persister.getRowMutationOperations(), + persister.getFactory() + ); + } + } + + @Override + public TemporalMapping createAuxiliaryMapping( + EntityPersister persister, + RootClass rootClass, + MappingModelCreationProcess creationProcess) { + final var temporalTable = rootClass.getAuxiliaryTable(); + final String tableName = temporalTable == null + ? persister.getIdentifierTableName() + : ( (AbstractEntityPersister) persister ) + .determineTableName( temporalTable ); + return new TemporalMappingImpl( rootClass, tableName, creationProcess ); + } + + @Override + public TemporalMapping createAuxiliaryMapping( + PluralAttributeMapping pluralAttributeMapping, + Collection bootDescriptor, + MappingModelCreationProcess creationProcess) { + final var temporalTable = bootDescriptor.getAuxiliaryTable(); + final String tableName = temporalTable == null + ? pluralAttributeMapping.getSeparateCollectionTable() + : getTableIdentifierExpression( temporalTable, creationProcess ); + return new TemporalMappingImpl( bootDescriptor, tableName, creationProcess ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/persister/state/spi/StateManagement.java b/hibernate-core/src/main/java/org/hibernate/persister/state/spi/StateManagement.java new file mode 100644 index 000000000000..3654a2fe17f1 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/persister/state/spi/StateManagement.java @@ -0,0 +1,72 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.persister.state.spi; + +import org.hibernate.Incubating; +import org.hibernate.mapping.Collection; +import org.hibernate.mapping.RootClass; +import org.hibernate.metamodel.mapping.AuxiliaryMapping; +import org.hibernate.metamodel.mapping.PluralAttributeMapping; +import org.hibernate.metamodel.mapping.internal.MappingModelCreationProcess; +import org.hibernate.persister.collection.CollectionPersister; +import org.hibernate.persister.collection.mutation.DeleteRowsCoordinator; +import org.hibernate.persister.collection.mutation.InsertRowsCoordinator; +import org.hibernate.persister.collection.mutation.RemoveCoordinator; +import org.hibernate.persister.collection.mutation.UpdateRowsCoordinator; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.persister.entity.mutation.DeleteCoordinator; +import org.hibernate.persister.entity.mutation.InsertCoordinator; +import org.hibernate.persister.entity.mutation.UpdateCoordinator; +import org.hibernate.persister.state.internal.AuditStateManagement; +import org.hibernate.persister.state.internal.HistoryStateManagement; +import org.hibernate.persister.state.internal.SoftDeleteStateManagement; +import org.hibernate.persister.state.internal.StandardStateManagement; +import org.hibernate.persister.state.internal.TemporalStateManagement; + +/** + * Aggregates the coordinators for a given state management strategy. + *

        + * Every concrete implementation of this interface should declare a + * field {@code public static final StateManagement INSTANCE}. + * + * @author Gavin King + * + * @see StandardStateManagement + * @see SoftDeleteStateManagement + * @see TemporalStateManagement + * @see HistoryStateManagement + * @see AuditStateManagement + * + * @since 7.4 + */ +@Incubating +public interface StateManagement { + + InsertCoordinator createInsertCoordinator(EntityPersister persister); + + UpdateCoordinator createUpdateCoordinator(EntityPersister persister); + + UpdateCoordinator createMergeCoordinator(EntityPersister persister); + + DeleteCoordinator createDeleteCoordinator(EntityPersister persister); + + InsertRowsCoordinator createInsertRowsCoordinator(CollectionPersister persister); + + UpdateRowsCoordinator createUpdateRowsCoordinator(CollectionPersister persister); + + DeleteRowsCoordinator createDeleteRowsCoordinator(CollectionPersister persister); + + RemoveCoordinator createRemoveCoordinator(CollectionPersister persister); + + AuxiliaryMapping createAuxiliaryMapping( + EntityPersister persister, + RootClass rootClass, + MappingModelCreationProcess creationProcess); + + AuxiliaryMapping createAuxiliaryMapping( + PluralAttributeMapping pluralAttributeMapping, + Collection bootDescriptor, + MappingModelCreationProcess creationProcess); +} diff --git a/hibernate-core/src/main/java/org/hibernate/persister/state/spi/package-info.java b/hibernate-core/src/main/java/org/hibernate/persister/state/spi/package-info.java new file mode 100644 index 000000000000..450452190e40 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/persister/state/spi/package-info.java @@ -0,0 +1,8 @@ +/** + * Extension point for customized state management, + * enabling functionality such as soft delete, temporal + * data, and audit logging. + * + * @see org.hibernate.persister.state.spi.StateManagement + */ +package org.hibernate.persister.state.spi; diff --git a/hibernate-core/src/main/java/org/hibernate/procedure/internal/EntityDomainResultBuilder.java b/hibernate-core/src/main/java/org/hibernate/procedure/internal/EntityDomainResultBuilder.java index 11a3f45b0955..f5b77b6f44b2 100644 --- a/hibernate-core/src/main/java/org/hibernate/procedure/internal/EntityDomainResultBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/procedure/internal/EntityDomainResultBuilder.java @@ -19,25 +19,20 @@ /** * @author Christian Beikov */ -public class EntityDomainResultBuilder implements ResultBuilder { +class EntityDomainResultBuilder implements ResultBuilder { private final NavigablePath navigablePath; private final EntityMappingType entityDescriptor; private final FetchBuilderBasicValued discriminatorFetchBuilder; - public EntityDomainResultBuilder(EntityMappingType entityDescriptor) { + EntityDomainResultBuilder(EntityMappingType entityDescriptor) { this.entityDescriptor = entityDescriptor; this.navigablePath = new NavigablePath( entityDescriptor.getEntityName() ); - final EntityDiscriminatorMapping discriminatorMapping = entityDescriptor.getDiscriminatorMapping(); - if ( discriminatorMapping == null ) { - this.discriminatorFetchBuilder = null; - } - else { - this.discriminatorFetchBuilder = new ImplicitFetchBuilderBasic( - navigablePath, - discriminatorMapping - ); - } + final var discriminatorMapping = entityDescriptor.getDiscriminatorMapping(); + this.discriminatorFetchBuilder = + discriminatorMapping == null + ? null + : new ImplicitFetchBuilderBasic( navigablePath, discriminatorMapping ); } @Override @@ -51,12 +46,12 @@ public ResultBuilder cacheKeyInstance() { } @Override - public EntityResult buildResult( + public EntityResult buildResult( JdbcValuesMetadata jdbcResultsMetadata, int resultPosition, DomainResultCreationState domainResultCreationState) { - return new EntityResultImpl( + return new EntityResultImpl<>( navigablePath, entityDescriptor, null, @@ -77,16 +72,16 @@ public EntityResult buildResult( } @Override - public boolean equals(Object o) { - if ( this == o ) { + public boolean equals(Object object) { + if ( this == object ) { return true; } - if ( o == null || getClass() != o.getClass() ) { + else if ( !( object instanceof EntityDomainResultBuilder that ) ) { return false; } - - final EntityDomainResultBuilder that = (EntityDomainResultBuilder) o; - return entityDescriptor.equals( that.entityDescriptor ); + else { + return entityDescriptor.equals( that.entityDescriptor ); + } } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/procedure/internal/FunctionReturnImpl.java b/hibernate-core/src/main/java/org/hibernate/procedure/internal/FunctionReturnImpl.java index fce45e336280..2de0be02215b 100644 --- a/hibernate-core/src/main/java/org/hibernate/procedure/internal/FunctionReturnImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/procedure/internal/FunctionReturnImpl.java @@ -17,23 +17,20 @@ import org.hibernate.sql.exec.internal.JdbcCallParameterExtractorImpl; import org.hibernate.sql.exec.internal.JdbcCallRefCursorExtractorImpl; import org.hibernate.sql.exec.spi.JdbcCallFunctionReturn; -import org.hibernate.type.BasicType; -import org.hibernate.type.descriptor.java.JavaType; -import org.hibernate.type.spi.TypeConfiguration; import jakarta.persistence.ParameterMode; /** * @author Steve Ebersole */ -public class FunctionReturnImpl implements FunctionReturnImplementor { +class FunctionReturnImpl implements FunctionReturnImplementor { private final ProcedureCallImplementor procedureCall; private final int sqlTypeCode; private OutputableType ormType; - public FunctionReturnImpl(ProcedureCallImplementor procedureCall, int sqlTypeCode) { + FunctionReturnImpl(ProcedureCallImplementor procedureCall, int sqlTypeCode) { this.procedureCall = procedureCall; this.sqlTypeCode = sqlTypeCode; } @@ -67,11 +64,11 @@ private OutputableType getOrmType(SharedSessionContractImplementor persistenc return ormType; } else { - final TypeConfiguration typeConfiguration = persistenceContext.getFactory().getTypeConfiguration(); - final JavaType javaType = + final var typeConfiguration = persistenceContext.getFactory().getTypeConfiguration(); + final var javaType = typeConfiguration.getJdbcTypeRegistry().getDescriptor( getJdbcTypeCode() ) - .getJdbcRecommendedJavaTypeMapping( null, null, typeConfiguration ); - final BasicType basicType = + .getRecommendedJavaType( null, null, typeConfiguration ); + final var basicType = typeConfiguration.standardBasicTypeForJavaType( javaType.getJavaTypeClass() ); //noinspection unchecked return (OutputableType) basicType; 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 798d3f729b57..17f2f6dd7c0c 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 @@ -16,6 +16,7 @@ import java.util.Set; import java.util.stream.Stream; +import jakarta.persistence.AttributeConverter; import org.hibernate.HibernateException; import org.hibernate.LockOptions; import org.hibernate.ScrollMode; @@ -31,11 +32,13 @@ import org.hibernate.procedure.ProcedureOutputs; import org.hibernate.procedure.spi.FunctionReturnImplementor; import org.hibernate.procedure.spi.NamedCallableQueryMemento; +import org.hibernate.procedure.spi.NamedCallableQueryMemento.ParameterMemento; import org.hibernate.procedure.spi.ParameterStrategy; import org.hibernate.procedure.spi.ProcedureCallImplementor; import org.hibernate.procedure.spi.ProcedureParameterImplementor; import org.hibernate.query.KeyedPage; import org.hibernate.query.KeyedResultList; +import org.hibernate.query.spi.ProcedureParameterMetadataImplementor; import org.hibernate.query.sqm.NodeBuilder; import org.hibernate.query.sqm.SqmBindableType; import org.hibernate.sql.exec.spi.JdbcOperationQueryCall; @@ -50,8 +53,6 @@ import org.hibernate.query.spi.QueryImplementor; import org.hibernate.query.spi.QueryParameterBindings; import org.hibernate.query.spi.ScrollableResultsImplementor; -import org.hibernate.query.sqm.SqmExpressible; -import org.hibernate.result.Output; import org.hibernate.result.ResultSetOutput; import org.hibernate.result.UpdateCountOutput; import org.hibernate.result.internal.OutputsExecutionContext; @@ -78,6 +79,7 @@ import jakarta.persistence.metamodel.Type; import static java.lang.Boolean.parseBoolean; +import static java.lang.Integer.parseInt; import static java.util.Arrays.asList; import static java.util.Collections.emptyList; import static java.util.Collections.emptySet; @@ -89,6 +91,7 @@ import static org.hibernate.jpa.HibernateHints.HINT_CALLABLE_FUNCTION_RETURN_TYPE; import static org.hibernate.procedure.internal.NamedCallableQueryMementoImpl.ParameterMementoImpl.fromRegistration; import static org.hibernate.procedure.internal.Util.resolveResultSetMappingClasses; +import static org.hibernate.procedure.internal.Util.resolveResultSetMappingNames; import static org.hibernate.procedure.internal.Util.resolveResultSetMappings; import static org.hibernate.query.results.ResultSetMapping.resolveResultSetMapping; @@ -105,7 +108,7 @@ public class ProcedureCallImpl private FunctionReturnImpl functionReturn; - private final ProcedureParameterMetadataImpl parameterMetadata; + private final ProcedureParameterMetadataImplementor parameterMetadata; private final ProcedureParamBindings parameterBindings; private final ResultSetMapping resultSetMapping; @@ -117,29 +120,46 @@ public class ProcedureCallImpl private ProcedureOutputsImpl outputs; private static String mappingId(String procedureName, Class[] resultClasses) { + assert resultClasses != null && resultClasses.length > 0; return procedureName + ":" + join( ",", resultClasses ); } private static String mappingId(String procedureName, String[] resultSetMappingNames) { + assert resultSetMappingNames != null && resultSetMappingNames.length > 0; return procedureName + ":" + join( ",", resultSetMappingNames ); } + private void registerParameters(SharedSessionContractImplementor session, NamedCallableQueryMemento memento) { + for ( var parameterMemento : memento.getParameterMementos() ) { + registerParameter( parameterMemento.resolve( session ) ); + } + } + + private ProcedureCallImpl( + SharedSessionContractImplementor session, + String procedureName, + String resultSetMappingName, + boolean resultSetMappingDynamic, + Set synchronizedQuerySpaces) { + super( session ); + this.procedureName = procedureName; + this.synchronizedQuerySpaces = synchronizedQuerySpaces; + final var factory = session.getSessionFactory(); + resultSetMapping = resolveResultSetMapping( resultSetMappingName, resultSetMappingDynamic, factory ); + parameterMetadata = new ProcedureParameterMetadataImpl(); + parameterBindings = new ProcedureParamBindings( parameterMetadata, factory ); + } + /** * The no-returns form. * * @param session The session * @param procedureName The name of the procedure to call */ - public ProcedureCallImpl(SharedSessionContractImplementor session, String procedureName) { - super( session ); - this.procedureName = procedureName; - - parameterMetadata = new ProcedureParameterMetadataImpl(); - parameterBindings = new ProcedureParamBindings( parameterMetadata, getSessionFactory() ); - - resultSetMapping = resolveResultSetMapping( procedureName, true, session.getSessionFactory() ); - - synchronizedQuerySpaces = null; + public ProcedureCallImpl( + SharedSessionContractImplementor session, + String procedureName) { + this( session, procedureName, procedureName, true, null ); } /** @@ -149,22 +169,14 @@ public ProcedureCallImpl(SharedSessionContractImplementor session, String proced * @param procedureName The name of the procedure to call * @param resultClasses The classes making up the result */ - public ProcedureCallImpl(SharedSessionContractImplementor session, String procedureName, Class... resultClasses) { - super( session ); - - assert resultClasses != null && resultClasses.length > 0; - - this.procedureName = procedureName; - - final var factory = session.getSessionFactory(); - - parameterMetadata = new ProcedureParameterMetadataImpl(); - parameterBindings = new ProcedureParamBindings( parameterMetadata, factory ); - - synchronizedQuerySpaces = new HashSet<>(); - - resultSetMapping = resolveResultSetMapping( mappingId( procedureName, resultClasses ), factory ); - + public ProcedureCallImpl( + SharedSessionContractImplementor session, + String procedureName, + Class... resultClasses) { + this( session, procedureName, + mappingId( procedureName, resultClasses ), + false, + new HashSet<>() ); resolveResultSetMappingClasses( resultClasses, resultSetMapping, @@ -184,22 +196,11 @@ public ProcedureCallImpl( final SharedSessionContractImplementor session, String procedureName, String... resultSetMappingNames) { - super( session ); - - assert resultSetMappingNames != null && resultSetMappingNames.length > 0; - - this.procedureName = procedureName; - - final var factory = session.getSessionFactory(); - - parameterMetadata = new ProcedureParameterMetadataImpl(); - parameterBindings = new ProcedureParamBindings( parameterMetadata, factory ); - - synchronizedQuerySpaces = new HashSet<>(); - - resultSetMapping = resolveResultSetMapping( mappingId( procedureName, resultSetMappingNames ), factory ); - - Util.resolveResultSetMappingNames( + this( session, procedureName, + mappingId( procedureName, resultSetMappingNames ), + false, + new HashSet<>() ); + resolveResultSetMappingNames( resultSetMappingNames, resultSetMapping, synchronizedQuerySpaces::add, @@ -213,20 +214,13 @@ public ProcedureCallImpl( * @param session The session * @param memento The named/stored memento */ - ProcedureCallImpl(SharedSessionContractImplementor session, NamedCallableQueryMemento memento) { - super( session ); - - procedureName = memento.getCallableName(); - - final var factory = session.getSessionFactory(); - - parameterMetadata = new ProcedureParameterMetadataImpl( memento, session ); - parameterBindings = new ProcedureParamBindings( parameterMetadata, factory ); - - synchronizedQuerySpaces = makeCopy( memento.getQuerySpaces() ); - - resultSetMapping = resolveResultSetMapping( memento.getRegistrationName(), factory ); - + ProcedureCallImpl( + SharedSessionContractImplementor session, + NamedCallableQueryMemento memento) { + this( session, memento.getCallableName(), + memento.getRegistrationName(), + false, + makeCopy( memento.getQuerySpaces() ) ); resolveResultSetMappings( memento.getResultSetMappingNames(), memento.getResultSetMappingClasses(), @@ -234,7 +228,7 @@ public ProcedureCallImpl( synchronizedQuerySpaces::add, this::getSessionFactory ); - + registerParameters( session, memento ); applyOptions( memento ); } @@ -248,19 +242,10 @@ public ProcedureCallImpl( SharedSessionContractImplementor session, NamedCallableQueryMemento memento, Class... resultTypes) { - super( session ); - - procedureName = memento.getCallableName(); - - final var factory = session.getSessionFactory(); - - parameterMetadata = new ProcedureParameterMetadataImpl( memento, session ); - parameterBindings = new ProcedureParamBindings( parameterMetadata, factory ); - - synchronizedQuerySpaces = makeCopy( memento.getQuerySpaces() ); - - resultSetMapping = resolveResultSetMapping( mappingId( procedureName, resultTypes ), factory ); - + this( session, memento.getCallableName(), + mappingId( memento.getCallableName(), resultTypes ), + false, + makeCopy( memento.getQuerySpaces() ) ); resolveResultSetMappings( null, resultTypes, @@ -268,7 +253,7 @@ public ProcedureCallImpl( synchronizedQuerySpaces::add, this::getSessionFactory ); - + registerParameters( session, memento ); applyOptions( memento ); } @@ -276,19 +261,10 @@ public ProcedureCallImpl( SharedSessionContractImplementor session, NamedCallableQueryMementoImpl memento, String... resultSetMappingNames) { - super( session ); - - procedureName = memento.getCallableName(); - - final var factory = session.getSessionFactory(); - - parameterMetadata = new ProcedureParameterMetadataImpl( memento, session ); - parameterBindings = new ProcedureParamBindings( parameterMetadata, factory ); - - synchronizedQuerySpaces = makeCopy( memento.getQuerySpaces() ); - - resultSetMapping = resolveResultSetMapping( mappingId( procedureName, resultSetMappingNames ), factory ); - + this( session, memento.getCallableName(), + mappingId( memento.getCallableName(), resultSetMappingNames ), + false, + makeCopy( memento.getQuerySpaces() ) ); resolveResultSetMappings( resultSetMappingNames, null, @@ -296,7 +272,7 @@ public ProcedureCallImpl( synchronizedQuerySpaces::add, this::getSessionFactory ); - + registerParameters( session, memento ); applyOptions( memento ); } @@ -332,7 +308,7 @@ public MutableQueryOptions getQueryOptions() { } @Override - public ProcedureParameterMetadataImpl getParameterMetadata() { + public ProcedureParameterMetadataImplementor getParameterMetadata() { return parameterMetadata; } @@ -369,7 +345,7 @@ private void markAsFunctionCallRefRefCursor() { public ProcedureCallImpl markAsFunctionCall(Class resultType) { final var basicType = getTypeConfiguration().getBasicTypeForJavaType( resultType ); if ( basicType == null ) { - throw new IllegalArgumentException( "Could not resolve a BasicType for the java type: " + resultType.getName() ); + throw new IllegalArgumentException( "Could not resolve a BasicType for the Java type: " + resultType.getName() ); } markAsFunctionCall( basicType ); return this; @@ -381,7 +357,7 @@ public ProcedureCall markAsFunctionCall(Type typeReference) { throw new IllegalArgumentException( "Given type is not an OutputableType: " + typeReference ); } if ( resultSetMapping.getNumberOfResultBuilders() == 0 ) { - final SqmExpressible expressible = resolveExpressible( typeReference ); + final var expressible = resolveExpressible( typeReference ); // Function returns might not be represented as callable parameters, // but we still want to convert the result to the requested java type if possible resultSetMapping.addResultBuilder( new ScalarDomainResultBuilder<>( expressible.getExpressibleJavaType() ) ); @@ -414,7 +390,13 @@ public QueryParameterBindings getParameterBindings() { public ProcedureCallImplementor registerStoredProcedureParameter(int position, Class type, ParameterMode mode) { getSession().checkOpen( true ); try { - registerParameter( position, type, mode ); + if ( AttributeConverter.class.isAssignableFrom( type ) ) { + // Hack to deal with HHH-12661 + registerParameterWithConverter( position, getConvertedType( type ), mode ); + } + else { + registerParameter( position, type, mode ); + } } catch (HibernateException he) { throw getExceptionConverter().convert( he ); @@ -433,7 +415,13 @@ public ProcedureCallImplementor registerStoredProcedureParameter( ParameterMode mode) { getSession().checkOpen( true ); try { - registerParameter( parameterName, type, mode ); + if ( AttributeConverter.class.isAssignableFrom( type ) ) { + // Hack to deal with HHH-12661 + registerParameterWithConverter( parameterName, getConvertedType( type ), mode ); + } + else { + registerParameter( parameterName, type, mode ); + } } catch (HibernateException he) { throw getExceptionConverter().convert( he ); @@ -445,6 +433,34 @@ public ProcedureCallImplementor registerStoredProcedureParameter( return this; } + private BasicType getConvertedType(Class type) { + final var convertedType = + getNodeBuilder().getTypeConfiguration().getBasicTypeRegistry() + .getRegisteredType( type.getTypeName() ); + if ( convertedType == null ) { + throw new IllegalArgumentException( "Unregistered converter type: " + type.getName() ); + } + return convertedType; + } + + private void registerParameterWithConverter(int position, BasicType convertedType, ParameterMode mode) { + registerParameter( new ProcedureParameterImpl<>( + position, + mode, + getExpressibleJavaType( convertedType ), + convertedType + ) ); + } + + private void registerParameterWithConverter(String name, BasicType convertedType, ParameterMode mode) { + registerParameter( new ProcedureParameterImpl<>( + name, + mode, + getExpressibleJavaType( convertedType ), + convertedType + ) ); + } + @Override public ProcedureCallImplementor registerStoredProcedureParameter( int position, @@ -510,12 +526,12 @@ private SqmBindableType resolveExpressible(Type typeReference) { } private void registerParameter(ProcedureParameterImplementor parameter) { - getParameterMetadata().registerParameter( parameter ); + parameterMetadata.registerParameter( parameter ); } @Override public ProcedureParameterImplementor getParameterRegistration(int position) { - return getParameterMetadata().getQueryParameter( position ); + return parameterMetadata.getQueryParameter( position ); } @Override @@ -556,12 +572,12 @@ public ProcedureParameterImplementor registerParameter( @Override public ProcedureParameterImplementor getParameterRegistration(String name) { - return getParameterMetadata().getQueryParameter( name ); + return parameterMetadata.getQueryParameter( name ); } @Override public List> getRegisteredParameters() { - return unmodifiableList( getParameterMetadata().getRegistrationsAsList() ); + return unmodifiableList( parameterMetadata.getRegistrationsAsList() ); } @Override @@ -651,8 +667,9 @@ private ProcedureOutputsImpl buildOutputs() { private Map, JdbcCallParameterRegistration> collectParameterRegistrations(JdbcOperationQueryCall call) { final Map, JdbcCallParameterRegistration> parameterRegistrations = new IdentityHashMap<>(); - if ( call.getFunctionReturn() != null ) { - parameterRegistrations.put( functionReturn, call.getFunctionReturn() ); + final var funReturn = call.getFunctionReturn(); + if ( funReturn != null ) { + parameterRegistrations.put( functionReturn, funReturn ); } final var registrations = getParameterMetadata().getRegistrationsAsList(); final var jdbcParameters = call.getParameterRegistrations(); @@ -665,8 +682,9 @@ private Map, JdbcCallParameterRegistration> collectParamet private List collectRefCursorExtractors(JdbcOperationQueryCall call) { final List refCursorExtractors = new ArrayList<>(); - if ( call.getFunctionReturn() != null ) { - final var refCursorExtractor = call.getFunctionReturn().getRefCursorExtractor(); + final var funReturn = call.getFunctionReturn(); + if ( funReturn != null ) { + final var refCursorExtractor = funReturn.getRefCursorExtractor(); if ( refCursorExtractor != null ) { refCursorExtractors.add( refCursorExtractor ); } @@ -674,8 +692,7 @@ private List collectRefCursorExtractors(JdbcOperatio final var registrations = getParameterMetadata().getRegistrationsAsList(); final var jdbcParameters = call.getParameterRegistrations(); for ( int i = 0; i < registrations.size(); i++ ) { - final var jdbcCallParameterRegistration = jdbcParameters.get( i ); - final var refCursorExtractor = jdbcCallParameterRegistration.getRefCursorExtractor(); + final var refCursorExtractor = jdbcParameters.get( i ).getRefCursorExtractor(); if ( refCursorExtractor != null ) { refCursorExtractors.add( refCursorExtractor ); } @@ -685,7 +702,7 @@ private List collectRefCursorExtractors(JdbcOperatio private JdbcParameterBindings parameterBindings( Map, JdbcCallParameterRegistration> parameterRegistrations) { - final JdbcParameterBindings jdbcParameterBindings = + final var jdbcParameterBindings = new JdbcParameterBindingsImpl( parameterRegistrations.size() ); for ( var entry : parameterRegistrations.entrySet() ) { final var registration = entry.getValue(); @@ -694,20 +711,18 @@ private JdbcParameterBindings parameterBindings( final var parameter = entry.getKey(); final var binding = getParameterBindings().getBinding( parameter ); if ( !binding.isBound() ) { - if ( parameter.getPosition() == null ) { - throw new IllegalArgumentException( "The parameter named [" + parameter + "] was not set! You need to call the setParameter method." ); + if ( parameter.isNamed() ) { + throw new IllegalArgumentException( "The parameter named '" + parameter + "' was not set" ); } else { - throw new IllegalArgumentException( "The parameter at position [" + parameter + "] was not set! You need to call the setParameter method." ); + throw new IllegalArgumentException( "The parameter at position " + parameter + " was not set" ); } } final var parameterType = (JdbcMapping) registration.getParameterType(); jdbcParameterBindings.addBinding( (JdbcParameter) parameterBinder, - new JdbcParameterBindingImpl( - parameterType, - parameterType.convertToRelationalValue( binding.getBindValue() ) - ) + new JdbcParameterBindingImpl( parameterType, + parameterType.convertToRelationalValue( binding.getBindValue() ) ) ); } } @@ -785,14 +800,13 @@ public NamedCallableQueryMemento toMemento(String name) { ); } - private static List toParameterMementos( - ProcedureParameterMetadataImpl parameterMetadata) { + private static List toParameterMementos( + ProcedureParameterMetadataImplementor parameterMetadata) { if ( parameterMetadata.getParameterStrategy() == ParameterStrategy.UNKNOWN ) { - // none... return emptyList(); } else { - final List mementos = new ArrayList<>(); + final List mementos = new ArrayList<>(); parameterMetadata.visitRegistrations( queryParameter -> mementos.add( fromRegistration( (ProcedureParameterImplementor) queryParameter ) ) @@ -815,12 +829,10 @@ public Query applyGraph(@SuppressWarnings("rawtypes") RootGraph graph, GraphS // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // outputs - private ProcedureOutputs procedureResult; - @Override public boolean execute() { try { - return outputs().getCurrent() instanceof ResultSetOutput; + return getOutputs().getCurrent() instanceof ResultSetOutput; } catch (NoMoreOutputsException e) { return false; @@ -834,30 +846,23 @@ public boolean execute() { } } - protected ProcedureOutputs outputs() { - if ( procedureResult == null ) { - procedureResult = getOutputs(); - } - return procedureResult; - } - @Override protected int doExecuteUpdate() { - // the expectation is that there is just one Output, of type UpdateCountOutput + // The expectation is that there is just one Output, of type UpdateCountOutput try { execute(); return getUpdateCount(); } finally { - outputs().release(); + getOutputs().release(); } } @Override public Object getOutputParameterValue(int position) { - // NOTE : according to spec (specifically), an exception thrown from this method should not mark for rollback. + // According to spec, an exception thrown from this method should not mark for rollback. try { - return outputs().getOutputParameterValue( position ); + return getOutputs().getOutputParameterValue( position ); } catch (ParameterStrategyException e) { throw new IllegalArgumentException( "Invalid mix of named and positional parameters", e ); @@ -869,9 +874,9 @@ public Object getOutputParameterValue(int position) { @Override public Object getOutputParameterValue(String parameterName) { - // NOTE : according to spec (specifically), an exception thrown from this method should not mark for rollback. + // According to spec, an exception thrown from this method should not mark for rollback. try { - return outputs().getOutputParameterValue( parameterName ); + return getOutputs().getOutputParameterValue( parameterName ); } catch (ParameterStrategyException e) { throw new IllegalArgumentException( "Invalid mix of named and positional parameters", e ); @@ -883,17 +888,19 @@ public Object getOutputParameterValue(String parameterName) { @Override public boolean hasMoreResults() { - return outputs().goToNext() && outputs().getCurrent() instanceof ResultSetOutput; + final var outputs = getOutputs(); + return outputs.goToNext() + && outputs.getCurrent() instanceof ResultSetOutput; } @Override public int getUpdateCount() { try { - final Output rtn = outputs().getCurrent(); - if ( rtn == null ) { + final var output = getOutputs().getCurrent(); + if ( output == null ) { return -1; } - else if ( rtn instanceof UpdateCountOutput updateCount ) { + else if ( output instanceof UpdateCountOutput updateCount ) { return updateCount.getUpdateCount(); } else { @@ -919,7 +926,7 @@ protected List doList() { } else { try { - if ( outputs().getCurrent() instanceof ResultSetOutput resultSetOutput ) { + if ( getOutputs().getCurrent() instanceof ResultSetOutput resultSetOutput ) { return ((ResultSetOutput) resultSetOutput).getResultList(); } else { @@ -928,9 +935,9 @@ protected List doList() { } } catch (NoMoreOutputsException e) { - // todo : the spec is completely silent on these type of edge-case scenarios. - // Essentially here we'd have a case where there are no more results (ResultSets nor updateCount) but - // getResultList was called. + // TODO: the spec is completely silent on these type of edge-case scenarios. + // Essentially here we'd have a case where there are no more results + // (ResultSets nor updateCount) but getResultList() was called. return null; } catch (HibernateException he) { @@ -972,22 +979,13 @@ public List getResultList() { public R getSingleResult() { final var resultList = getResultList(); if ( resultList == null || resultList.isEmpty() ) { - throw new NoResultException( - String.format( - "Call to stored procedure [%s] returned no results", - getProcedureName() - ) - ); + throw new NoResultException( "Call to stored procedure '" + getProcedureName() + + "' returned no results" ); } else if ( resultList.size() > 1 ) { - throw new NonUniqueResultException( - String.format( - "Call to stored procedure [%s] returned multiple results", - getProcedureName() - ) - ); + throw new NonUniqueResultException( "Call to stored procedure '" + getProcedureName() + + "' returned multiple results" ); } - return resultList.get( 0 ); } @@ -998,14 +996,9 @@ public R getSingleResultOrNull() { return null; } else if ( resultList.size() > 1 ) { - throw new NonUniqueResultException( - String.format( - "Call to stored procedure [%s] returned multiple results", - getProcedureName() - ) - ); + throw new NonUniqueResultException( "Call to stored procedure '" + getProcedureName() + + "' returned multiple results" ); } - return resultList.get( 0 ); } @@ -1046,7 +1039,7 @@ public T unwrap(Class type) { public ProcedureCallImplementor setLockMode(LockModeType lockMode) { // the JPA spec requires IllegalStateException here, even // though it's logically an UnsupportedOperationException - throw new IllegalStateException( "Illegal attempt to set lock mode for a procedure calls" ); + throw new IllegalStateException( "Illegal attempt to set lock mode for a procedure call" ); } @Override @@ -1062,7 +1055,7 @@ public ProcedureCallImplementor setTimeout(Integer timeout) { public LockModeType getLockMode() { // the JPA spec requires IllegalStateException here, even // though it's logically an UnsupportedOperationException - throw new IllegalStateException( "Illegal attempt to get lock mode on a native-query" ); + throw new IllegalStateException( "Illegal attempt to get lock mode for a procedure call" ); } @Override @Deprecated @@ -1070,39 +1063,33 @@ public QueryImplementor setLockOptions(LockOptions lockOptions) { throw new UnsupportedOperationException( "setLockOptions does not apply to procedure calls" ); } - @Override + @Override @SuppressWarnings("resource") public ProcedureCallImplementor setHint(String hintName, Object value) { switch ( hintName ) { case HINT_CALLABLE_FUNCTION: - if ( value != null ) { - if ( value instanceof Boolean bool ) { - if ( bool ) { - applyCallableFunctionHint(); - } - } - else if ( parseBoolean( value.toString() ) ) { - applyCallableFunctionHint(); - } + if ( value instanceof Boolean bool && bool + || value instanceof String string && parseBoolean( string ) ) { + applyCallableFunctionHint(); + } + else { + throw new IllegalArgumentException( "Illegal value for hint '" + hintName + "'" ); } break; case HINT_CALLABLE_FUNCTION_RETURN_TYPE: - if ( value != null ) { - if ( value instanceof Integer code ) { - //noinspection resource - markAsFunctionCall( code ); - } - else if ( value instanceof Type type ) { - //noinspection resource - markAsFunctionCall( type ); - } - else if ( value instanceof Class type ) { - //noinspection resource - markAsFunctionCall( type ); - } - else { - //noinspection resource - markAsFunctionCall( Integer.parseInt( value.toString() ) ); - } + if ( value instanceof Integer code ) { + markAsFunctionCall( code ); + } + else if ( value instanceof Type type ) { + markAsFunctionCall( type ); + } + else if ( value instanceof Class type ) { + markAsFunctionCall( type ); + } + else if ( value instanceof String string ) { + markAsFunctionCall( parseInt( string ) ); + } + else { + throw new IllegalArgumentException( "Illegal value for hint '" + hintName + "'" ); } break; default: 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 d4686e03774f..5e7942736e49 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 @@ -5,7 +5,6 @@ package org.hibernate.procedure.internal; import java.sql.CallableStatement; -import java.sql.ResultSet; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -26,7 +25,7 @@ * * @author Steve Ebersole */ -public class ProcedureOutputsImpl extends OutputsImpl implements ProcedureOutputs { +class ProcedureOutputsImpl extends OutputsImpl implements ProcedureOutputs { private final ProcedureCallImpl procedureCall; private final CallableStatement callableStatement; @@ -53,7 +52,7 @@ public T getOutputParameterValue(ProcedureParameter parameter) { if ( parameter.getMode() == ParameterMode.IN ) { throw new ParameterMisuseException( "IN parameter not valid for output extraction" ); } - final JdbcCallParameterRegistration registration = parameterRegistrations.get( parameter ); + final var registration = parameterRegistrations.get( parameter ); if ( registration == null ) { throw new IllegalArgumentException( "Parameter [" + parameter + "] is not registered with this procedure call" ); } @@ -108,7 +107,7 @@ private ProcedureCurrentReturnState(boolean isResultSet, int updateCount, int re @Override public boolean indicatesMoreOutputs() { return super.indicatesMoreOutputs() - || ProcedureOutputsImpl.this.refCursorParamIndex < refCursorParameters.length; + || ProcedureOutputsImpl.this.refCursorParamIndex < refCursorParameters.length; } @Override @@ -118,8 +117,8 @@ protected boolean hasExtendedReturns() { @Override protected Output buildExtendedReturn() { - final JdbcCallRefCursorExtractor refCursorParam = refCursorParameters[ProcedureOutputsImpl.this.refCursorParamIndex++]; - final ResultSet resultSet = refCursorParam.extractResultSet( + final var refCursorParam = refCursorParameters[ProcedureOutputsImpl.this.refCursorParamIndex++]; + final var resultSet = refCursorParam.extractResultSet( callableStatement, procedureCall.getSession() ); @@ -133,13 +132,14 @@ protected boolean hasFunctionReturns() { @Override protected Output buildFunctionReturn() { - final Object result = parameterRegistrations.get( procedureCall.getFunctionReturn() ) - .getParameterExtractor() - .extractValue( - callableStatement, - false, - procedureCall.getSession() - ); + final Object result = + parameterRegistrations.get( procedureCall.getFunctionReturn() ) + .getParameterExtractor() + .extractValue( + callableStatement, + false, + procedureCall.getSession() + ); final List results = new ArrayList<>( 1 ); results.add( result ); return buildResultSetOutput( () -> results ); diff --git a/hibernate-core/src/main/java/org/hibernate/procedure/internal/ProcedureParamBindings.java b/hibernate-core/src/main/java/org/hibernate/procedure/internal/ProcedureParamBindings.java index f53fb220006a..6c2deaf960c6 100644 --- a/hibernate-core/src/main/java/org/hibernate/procedure/internal/ProcedureParamBindings.java +++ b/hibernate-core/src/main/java/org/hibernate/procedure/internal/ProcedureParamBindings.java @@ -14,6 +14,7 @@ import org.hibernate.procedure.spi.ProcedureParameterBinding; import org.hibernate.procedure.spi.ProcedureParameterImplementor; import org.hibernate.query.QueryParameter; +import org.hibernate.query.spi.ProcedureParameterMetadataImplementor; import org.hibernate.query.spi.QueryParameterBinding; import org.hibernate.query.spi.QueryParameterBindings; import org.hibernate.query.spi.QueryParameterImplementor; @@ -29,87 +30,87 @@ public class ProcedureParamBindings implements QueryParameterBindings { private static final Logger LOG = Logger.getLogger( QueryParameterBindings.class ); - private final ProcedureParameterMetadataImpl parameterMetadata; + private final ProcedureParameterMetadataImplementor parameterMetadata; private final SessionFactoryImplementor sessionFactory; private final Map, ProcedureParameterBinding> bindingMap = new HashMap<>(); public ProcedureParamBindings( - ProcedureParameterMetadataImpl parameterMetadata, + ProcedureParameterMetadataImplementor parameterMetadata, SessionFactoryImplementor sessionFactory) { this.parameterMetadata = parameterMetadata; this.sessionFactory = sessionFactory; } - public ProcedureParameterMetadataImpl getParameterMetadata() { + public ProcedureParameterMetadataImplementor getParameterMetadata() { return parameterMetadata; } @Override public boolean isBound(QueryParameterImplementor parameter) { - //noinspection SuspiciousMethodCalls - return bindingMap.containsKey( parameter ); + return parameter instanceof ProcedureParameterImplementor + && bindingMap.containsKey( parameter ); } @Override public

        ProcedureParameterBinding

        getBinding(QueryParameterImplementor

        parameter) { - return getQueryParamerBinding( (ProcedureParameterImplementor

        ) parameter ); + return getQueryParameterBinding( (ProcedureParameterImplementor

        ) parameter ); } - public

        ProcedureParameterBinding

        getQueryParamerBinding(ProcedureParameterImplementor

        parameter) { + public

        ProcedureParameterBinding

        getQueryParameterBinding(ProcedureParameterImplementor

        parameter) { final var procParam = parameterMetadata.resolve( parameter ); - var binding = bindingMap.get( procParam ); + final var binding = bindingMap.get( procParam ); if ( binding == null ) { if ( !parameterMetadata.containsReference( parameter ) ) { throw new IllegalArgumentException( "Passed parameter is not registered with this query" ); } - binding = new ProcedureParameterBindingImpl<>( procParam, sessionFactory ); - bindingMap.put( procParam, binding ); + final var parameterBinding = new ProcedureParameterBindingImpl<>( procParam, sessionFactory ); + bindingMap.put( procParam, parameterBinding ); + return parameterBinding; + } + else { + //noinspection unchecked + return (ProcedureParameterBinding

        ) binding; } - //noinspection unchecked - return (ProcedureParameterBinding

        ) binding; } @Override - public

        ProcedureParameterBinding

        getBinding(String name) { - //noinspection unchecked - final var parameter = - (ProcedureParameterImplementor

        ) - parameterMetadata.getQueryParameter( name ); + public ProcedureParameterBinding getBinding(String name) { + final var parameter = parameterMetadata.getQueryParameter( name ); if ( parameter == null ) { - throw new IllegalArgumentException( "Parameter does not exist: " + name ); + throw new IllegalArgumentException( "Parameter with name '" + name + "' does not exist" ); } - return getQueryParamerBinding( parameter ); + return getQueryParameterBinding( (ProcedureParameterImplementor) parameter ); } @Override - public

        ProcedureParameterBinding

        getBinding(int position) { - //noinspection unchecked - final var parameter = - (ProcedureParameterImplementor

        ) - parameterMetadata.getQueryParameter( position ); + public ProcedureParameterBinding getBinding(int position) { + final var parameter = parameterMetadata.getQueryParameter( position ); if ( parameter == null ) { throw new IllegalArgumentException( "Parameter at position " + position + "does not exist" ); } - return getQueryParamerBinding( parameter ); + return getQueryParameterBinding( (ProcedureParameterImplementor) parameter ); } @Override public void validate() { - parameterMetadata.visitRegistrations( parameter -> validate( (ProcedureParameterImplementor) parameter ) ); + if ( LOG.isDebugEnabled() ) { + parameterMetadata.visitRegistrations( + parameter -> validate( (ProcedureParameterImplementor) parameter ) ); + } } private void validate(ProcedureParameterImplementor procParam) { - final ParameterMode mode = procParam.getMode(); + final var mode = procParam.getMode(); if ( mode == ParameterMode.IN || mode == ParameterMode.INOUT ) { if ( !getBinding( procParam ).isBound() ) { // depending on "pass nulls" this might be OK - for now, just log a warning - if ( procParam.getPosition() != null ) { + if ( procParam.isOrdinal() ) { LOG.debugf( "Procedure parameter at position %s is not bound", procParam.getPosition() ); } else { - LOG.debugf( "Procedure parameter %s is not bound", procParam.getName() ); + LOG.debugf( "Procedure parameter '%s' is not bound", procParam.getName() ); } } } diff --git a/hibernate-core/src/main/java/org/hibernate/procedure/internal/ProcedureParameterBindingImpl.java b/hibernate-core/src/main/java/org/hibernate/procedure/internal/ProcedureParameterBindingImpl.java index 8b1cf5a39536..cf80e1e8d8ee 100644 --- a/hibernate-core/src/main/java/org/hibernate/procedure/internal/ProcedureParameterBindingImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/procedure/internal/ProcedureParameterBindingImpl.java @@ -14,10 +14,10 @@ * * @author Steve Ebersole */ -public class ProcedureParameterBindingImpl +class ProcedureParameterBindingImpl extends QueryParameterBindingImpl implements ProcedureParameterBinding { - public ProcedureParameterBindingImpl( + ProcedureParameterBindingImpl( ProcedureParameterImplementor queryParameter, SessionFactoryImplementor sessionFactory) { super( queryParameter, sessionFactory ); diff --git a/hibernate-core/src/main/java/org/hibernate/procedure/internal/ProcedureParameterImpl.java b/hibernate-core/src/main/java/org/hibernate/procedure/internal/ProcedureParameterImpl.java index 4ba366ceef69..c8244612676a 100644 --- a/hibernate-core/src/main/java/org/hibernate/procedure/internal/ProcedureParameterImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/procedure/internal/ProcedureParameterImpl.java @@ -8,7 +8,6 @@ import java.util.Objects; import org.hibernate.engine.jdbc.env.spi.ExtractedDatabaseMetaData; -import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.procedure.ParameterTypeException; import org.hibernate.procedure.spi.NamedCallableQueryMemento; import org.hibernate.procedure.spi.ParameterStrategy; @@ -16,7 +15,6 @@ import org.hibernate.procedure.spi.ProcedureParameterImplementor; import org.hibernate.type.BindableType; import org.hibernate.type.OutputableType; -import org.hibernate.type.internal.BindingTypeHelper; import org.hibernate.query.spi.AbstractQueryParameter; import org.hibernate.query.spi.QueryParameterBinding; import org.hibernate.sql.exec.internal.JdbcCallParameterExtractorImpl; @@ -30,10 +28,12 @@ import jakarta.persistence.ParameterMode; +import static org.hibernate.type.internal.BindingTypeHelper.resolveTemporalPrecision; + /** * @author Steve Ebersole */ -public class ProcedureParameterImpl extends AbstractQueryParameter implements ProcedureParameterImplementor { +class ProcedureParameterImpl extends AbstractQueryParameter implements ProcedureParameterImplementor { private final String name; private final Integer position; @@ -43,7 +43,7 @@ public class ProcedureParameterImpl extends AbstractQueryParameter impleme /** * Used for named Query parameters */ - public ProcedureParameterImpl( + ProcedureParameterImpl( String name, ParameterMode mode, Class javaType, @@ -101,12 +101,16 @@ public NamedCallableQueryMemento.ParameterMemento toMemento() { public JdbcCallParameterRegistration toJdbcParameterRegistration( int startIndex, ProcedureCallImplementor procedureCall) { - final QueryParameterBinding binding = procedureCall.getParameterBindings().getBinding( this ); - final boolean isNamed = procedureCall.getParameterStrategy() == ParameterStrategy.NAMED && this.name != null; - final SharedSessionContractImplementor session = procedureCall.getSession(); + final QueryParameterBinding binding = + procedureCall.getParameterBindings() + .getBinding( this ); + final boolean isNamed = + procedureCall.getParameterStrategy() == ParameterStrategy.NAMED + && name != null; + final var session = procedureCall.getSession(); - final OutputableType typeToUse = (OutputableType) - BindingTypeHelper.resolveTemporalPrecision( + final var typeToUse = (OutputableType) + resolveTemporalPrecision( binding == null ? null : binding.getExplicitTemporalPrecision(), getBindableType( binding ), session.getFactory().getQueryEngine().getCriteriaBuilder() @@ -116,14 +120,20 @@ public JdbcCallParameterRegistration toJdbcParameterRegistration( final JdbcParameterBinder parameterBinder; final JdbcCallRefCursorExtractorImpl refCursorExtractor; final JdbcCallParameterExtractorImpl parameterExtractor; - final ExtractedDatabaseMetaData databaseMetaData = + final var databaseMetaData = session.getFactory().getJdbcServices().getJdbcEnvironment() .getExtractedDatabaseMetaData(); final boolean passProcedureParameterNames = - session.getFactory().getSessionFactoryOptions().isPassProcedureParameterNames(); + session.getFactory().getSessionFactoryOptions() + .isPassProcedureParameterNames(); switch ( mode ) { case REF_CURSOR: - jdbcParamName = this.name != null && databaseMetaData.supportsNamedParameters() && passProcedureParameterNames ? this.name : null; + jdbcParamName = + name != null + && databaseMetaData.supportsNamedParameters() + && passProcedureParameterNames + ? name + : null; refCursorExtractor = new JdbcCallRefCursorExtractorImpl( startIndex ); parameterBinder = null; parameterExtractor = null; @@ -151,16 +161,24 @@ public JdbcCallParameterRegistration toJdbcParameterRegistration( break; } - return new JdbcCallParameterRegistrationImpl( jdbcParamName, startIndex, mode, typeToUse, parameterBinder, parameterExtractor, refCursorExtractor ); + return new JdbcCallParameterRegistrationImpl( + jdbcParamName, + startIndex, + mode, + typeToUse, + parameterBinder, + parameterExtractor, + refCursorExtractor + ); } private BindableType getBindableType(QueryParameterBinding binding) { - if ( getHibernateType() != null ) { - return getHibernateType(); + final var type = getHibernateType(); + if ( type != null ) { + return type; } else if ( binding != null ) { - //noinspection unchecked - return (BindableType) binding.getBindType(); + return binding.getBindType(); } else { return null; @@ -175,7 +193,7 @@ private String getJdbcParamName( ExtractedDatabaseMetaData databaseMetaData) { return isNamed && passProcedureParameterNames && canDoNameParameterBinding( typeToUse, procedureCall, databaseMetaData ) - ? this.name + ? name : null; } @@ -203,8 +221,8 @@ private JdbcParameterBinder getParameterBinder(BindableType typeToUse, String ) ); } - else if ( typeToUse instanceof BasicType ) { - return new JdbcParameterImpl( (BasicType) typeToUse ); + else if ( typeToUse instanceof BasicType basicType ) { + return new JdbcParameterImpl( basicType ); } else { throw new UnsupportedOperationException(); @@ -217,8 +235,8 @@ private boolean canDoNameParameterBinding( ExtractedDatabaseMetaData databaseMetaData) { return procedureCall.getFunctionReturn() == null && databaseMetaData.supportsNamedParameters() - && hibernateType instanceof ProcedureParameterNamedBinder - && ( (ProcedureParameterNamedBinder) hibernateType ).canDoSetting(); + && hibernateType instanceof ProcedureParameterNamedBinder binder + && binder.canDoSetting(); } @Override @@ -227,19 +245,21 @@ public int hashCode() { } @Override - public boolean equals(Object o) { - if ( this == o ) { + public boolean equals(Object object) { + if ( this == object ) { return true; } - if ( o == null ) { + else if ( object == null ) { return false; } - if ( !(o instanceof ProcedureParameterImpl that) ) { + else if ( !(object instanceof ProcedureParameterImpl that) ) { return false; } - return Objects.equals( name, that.name ) - && Objects.equals( position, that.position ) - && mode == that.mode; + else { + return Objects.equals( name, that.name ) + && Objects.equals( position, that.position ) + && mode == that.mode; + } } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/procedure/internal/ProcedureParameterMetadataImpl.java b/hibernate-core/src/main/java/org/hibernate/procedure/internal/ProcedureParameterMetadataImpl.java index ac0d02d98462..cd77b4e06cba 100644 --- a/hibernate-core/src/main/java/org/hibernate/procedure/internal/ProcedureParameterMetadataImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/procedure/internal/ProcedureParameterMetadataImpl.java @@ -14,13 +14,10 @@ import jakarta.persistence.Parameter; import org.hibernate.engine.spi.SessionFactoryImplementor; -import org.hibernate.engine.spi.SharedSessionContractImplementor; -import org.hibernate.procedure.spi.NamedCallableQueryMemento; import org.hibernate.procedure.spi.ParameterStrategy; import org.hibernate.type.BindableType; import org.hibernate.query.QueryParameter; import org.hibernate.query.internal.QueryParameterBindingsImpl; -import org.hibernate.procedure.ProcedureParameter; import org.hibernate.procedure.spi.ProcedureParameterImplementor; import org.hibernate.query.spi.ProcedureParameterMetadataImplementor; import org.hibernate.query.spi.QueryParameterBindings; @@ -36,18 +33,11 @@ * * @author Steve Ebersole */ -public class ProcedureParameterMetadataImpl implements ProcedureParameterMetadataImplementor { +class ProcedureParameterMetadataImpl implements ProcedureParameterMetadataImplementor { private ParameterStrategy parameterStrategy = ParameterStrategy.UNKNOWN; private List> parameters; - public ProcedureParameterMetadataImpl() { - } - - public ProcedureParameterMetadataImpl(NamedCallableQueryMemento memento, SharedSessionContractImplementor session) { - memento.getParameterMementos() - .forEach( parameterMemento -> registerParameter( parameterMemento.resolve( session ) ) ); - } - + @Override public void registerParameter(ProcedureParameterImplementor parameter) { if ( parameter.isNamed() ) { if ( parameterStrategy == ParameterStrategy.POSITIONAL ) { @@ -99,17 +89,18 @@ public boolean hasPositionalParameters() { @Override public Set getNamedParameterNames() { - if ( !hasNamedParameters() ) { - return emptySet(); - } - - final Set rtn = new HashSet<>(); - for ( ProcedureParameter parameter : parameters ) { - if ( parameter.getName() != null ) { - rtn.add( parameter.getName() ); + if ( hasNamedParameters() && parameters != null ) { + final Set names = new HashSet<>(); + for ( var parameter : parameters ) { + if ( parameter.getName() != null ) { + names.add( parameter.getName() ); + } } + return names; + } + else { + return emptySet(); } - return rtn; } @Override @@ -123,13 +114,14 @@ public boolean containsReference(QueryParameter parameter) { && parameters.contains( (ProcedureParameterImplementor) parameter ); } + @Override public ParameterStrategy getParameterStrategy() { return parameterStrategy; } @Override public boolean hasAnyMatching(Predicate> filter) { - if ( parameters.isEmpty() ) { + if ( parameters == null || parameters.isEmpty() ) { return false; } else { @@ -144,9 +136,11 @@ public boolean hasAnyMatching(Predicate> filter) { @Override public ProcedureParameterImplementor findQueryParameter(String name) { - for ( var parameter : parameters ) { - if ( name.equals( parameter.getName() ) ) { - return parameter; + if ( parameters != null ) { + for ( var parameter : parameters ) { + if ( name.equals( parameter.getName() ) ) { + return parameter; + } } } return null; @@ -163,9 +157,12 @@ public ProcedureParameterImplementor getQueryParameter(String name) { @Override public ProcedureParameterImplementor findQueryParameter(int positionLabel) { - for ( var parameter : parameters ) { - if ( parameter.getName() == null && positionLabel == parameter.getPosition() ) { - return parameter; + if ( parameters != null ) { + for ( var parameter : parameters ) { + if ( parameter.getName() == null + && positionLabel == parameter.getPosition() ) { + return parameter; + } } } return null; @@ -174,18 +171,20 @@ public ProcedureParameterImplementor findQueryParameter(int positionLabel) { @Override public ProcedureParameterImplementor getQueryParameter(int positionLabel) { final var queryParameter = findQueryParameter( positionLabel ); - if ( queryParameter != null ) { - return queryParameter; + if ( queryParameter == null ) { + throw new IllegalArgumentException( + "Positional parameter " + positionLabel + " is not registered with this procedure call" ); } - throw new IllegalArgumentException( "Positional parameter [" + positionLabel + "] is not registered with this procedure call" ); + return queryParameter; } @Override public

        ProcedureParameterImplementor

        resolve(Parameter

        parameter) { - if ( parameter instanceof ProcedureParameterImplementor

        parameterImplementor ) { + if ( parameters != null + && parameter instanceof ProcedureParameterImplementor

        procedureParam ) { for ( var registered : parameters ) { if ( registered == parameter ) { - return parameterImplementor; + return procedureParam; } } } diff --git a/hibernate-core/src/main/java/org/hibernate/procedure/internal/ScalarDomainResultBuilder.java b/hibernate-core/src/main/java/org/hibernate/procedure/internal/ScalarDomainResultBuilder.java index 240436770c6c..27508941a6bc 100644 --- a/hibernate-core/src/main/java/org/hibernate/procedure/internal/ScalarDomainResultBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/procedure/internal/ScalarDomainResultBuilder.java @@ -7,23 +7,22 @@ import org.hibernate.metamodel.mapping.BasicValuedMapping; import org.hibernate.query.results.ResultBuilder; import org.hibernate.query.results.internal.ResultSetMappingSqlSelection; -import org.hibernate.sql.ast.spi.SqlExpressionResolver; -import org.hibernate.sql.ast.spi.SqlSelection; import org.hibernate.sql.results.graph.DomainResult; import org.hibernate.sql.results.graph.DomainResultCreationState; import org.hibernate.sql.results.graph.basic.BasicResult; import org.hibernate.sql.results.jdbc.spi.JdbcValuesMetadata; import org.hibernate.type.BasicType; import org.hibernate.type.descriptor.java.JavaType; -import org.hibernate.type.spi.TypeConfiguration; + +import static org.hibernate.sql.ast.spi.SqlExpressionResolver.createColumnReferenceKey; /** * @author Steve Ebersole */ -public class ScalarDomainResultBuilder implements ResultBuilder { +class ScalarDomainResultBuilder implements ResultBuilder { private final JavaType typeDescriptor; - public ScalarDomainResultBuilder(JavaType typeDescriptor) { + ScalarDomainResultBuilder(JavaType typeDescriptor) { this.typeDescriptor = typeDescriptor; } @@ -37,17 +36,18 @@ public DomainResult buildResult( JdbcValuesMetadata jdbcResultsMetadata, int resultPosition, DomainResultCreationState domainResultCreationState) { - final SqlExpressionResolver sqlExpressionResolver = - domainResultCreationState.getSqlAstCreationState().getSqlExpressionResolver(); - final TypeConfiguration typeConfiguration = - domainResultCreationState.getSqlAstCreationState().getCreationContext().getTypeConfiguration(); - final SqlSelection sqlSelection = sqlExpressionResolver.resolveSqlSelection( + final var sqlAstCreationState = domainResultCreationState.getSqlAstCreationState(); + final var sqlExpressionResolver = + sqlAstCreationState.getSqlExpressionResolver(); + final var typeConfiguration = + sqlAstCreationState.getCreationContext().getTypeConfiguration(); + final var sqlSelection = sqlExpressionResolver.resolveSqlSelection( sqlExpressionResolver.resolveSqlExpression( - SqlExpressionResolver.createColumnReferenceKey( + createColumnReferenceKey( Integer.toString( resultPosition + 1 ) ), processingState -> { - final BasicType basicType = jdbcResultsMetadata.resolveType( + final var basicType = jdbcResultsMetadata.resolveType( resultPosition + 1, typeDescriptor, typeConfiguration @@ -75,17 +75,16 @@ public ResultBuilder cacheKeyInstance() { } @Override - public boolean equals(Object o) { - if ( this == o ) { + public boolean equals(Object object) { + if ( this == object ) { return true; } - if ( o == null || getClass() != o.getClass() ) { + else if ( !( object instanceof ScalarDomainResultBuilder that ) ) { return false; } - - ScalarDomainResultBuilder that = (ScalarDomainResultBuilder) o; - - return typeDescriptor.equals( that.typeDescriptor ); + else { + return typeDescriptor.equals( that.typeDescriptor ); + } } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/procedure/internal/Util.java b/hibernate-core/src/main/java/org/hibernate/procedure/internal/Util.java index 9145aaf77bca..319ff33ac5ca 100644 --- a/hibernate-core/src/main/java/org/hibernate/procedure/internal/Util.java +++ b/hibernate-core/src/main/java/org/hibernate/procedure/internal/Util.java @@ -6,15 +6,11 @@ import java.util.function.Consumer; -import org.hibernate.internal.util.collections.ArrayHelper; -import org.hibernate.metamodel.MappingMetamodel; -import org.hibernate.persister.entity.EntityPersister; import org.hibernate.query.UnknownSqlResultSetMappingException; import org.hibernate.query.internal.ResultSetMappingResolutionContext; -import org.hibernate.query.named.NamedObjectRepository; -import org.hibernate.query.named.NamedResultSetMappingMemento; import org.hibernate.query.results.ResultSetMapping; -import org.hibernate.type.descriptor.java.spi.JavaTypeRegistry; + +import static org.hibernate.internal.util.collections.ArrayHelper.isEmpty; /** * Utilities used to implement procedure call support. @@ -28,18 +24,18 @@ private Util() { public static void resolveResultSetMappings( String[] resultSetMappingNames, - Class[] resultSetMappingClasses, + Class[] resultSetMappingClasses, ResultSetMapping resultSetMapping, Consumer querySpaceConsumer, ResultSetMappingResolutionContext context) { - if ( ! ArrayHelper.isEmpty( resultSetMappingNames ) ) { + if ( !isEmpty( resultSetMappingNames ) ) { // cannot specify both - if ( ! ArrayHelper.isEmpty( resultSetMappingClasses ) ) { + if ( !isEmpty( resultSetMappingClasses ) ) { throw new IllegalArgumentException( "Cannot specify both result-set mapping names and classes" ); } resolveResultSetMappingNames( resultSetMappingNames, resultSetMapping, querySpaceConsumer, context ); } - else if ( ! ArrayHelper.isEmpty( resultSetMappingClasses ) ) { + else if ( !isEmpty( resultSetMappingClasses ) ) { resolveResultSetMappingClasses( resultSetMappingClasses, resultSetMapping, querySpaceConsumer, context ); } @@ -51,31 +47,26 @@ public static void resolveResultSetMappingNames( ResultSetMapping resultSetMapping, Consumer querySpaceConsumer, ResultSetMappingResolutionContext context) { - final NamedObjectRepository namedObjectRepository = context.getNamedObjectRepository(); + final var namedObjectRepository = context.getNamedObjectRepository(); for ( String resultSetMappingName : resultSetMappingNames ) { - final NamedResultSetMappingMemento memento = + final var memento = namedObjectRepository.getResultSetMappingMemento( resultSetMappingName ); if ( memento == null ) { throw new UnknownSqlResultSetMappingException( "Unknown SqlResultSetMapping [" + resultSetMappingName + "]" ); } - memento.resolve( - resultSetMapping, - querySpaceConsumer, - context - ); + memento.resolve( resultSetMapping, querySpaceConsumer, context ); } } public static void resolveResultSetMappingClasses( - Class[] resultSetMappingClasses, + Class[] resultSetMappingClasses, ResultSetMapping resultSetMapping, Consumer querySpaceConsumer, ResultSetMappingResolutionContext context) { - final MappingMetamodel mappingMetamodel = context.getMappingMetamodel(); - final JavaTypeRegistry javaTypeRegistry = mappingMetamodel.getTypeConfiguration().getJavaTypeRegistry(); - - for ( Class resultSetMappingClass : resultSetMappingClasses ) { - final EntityPersister entityDescriptor = mappingMetamodel.findEntityDescriptor( resultSetMappingClass ); + final var mappingMetamodel = context.getMappingMetamodel(); + final var javaTypeRegistry = mappingMetamodel.getTypeConfiguration().getJavaTypeRegistry(); + for ( var resultSetMappingClass : resultSetMappingClasses ) { + final var entityDescriptor = mappingMetamodel.findEntityDescriptor( resultSetMappingClass ); if ( entityDescriptor != null ) { resultSetMapping.addResultBuilder( new EntityDomainResultBuilder( entityDescriptor ) ); for ( String querySpace : entityDescriptor.getSynchronizedQuerySpaces() ) { diff --git a/hibernate-core/src/main/java/org/hibernate/proxy/pojo/BasicLazyInitializer.java b/hibernate-core/src/main/java/org/hibernate/proxy/pojo/BasicLazyInitializer.java index 11d7f16c8aef..618cd2b28e2f 100644 --- a/hibernate-core/src/main/java/org/hibernate/proxy/pojo/BasicLazyInitializer.java +++ b/hibernate-core/src/main/java/org/hibernate/proxy/pojo/BasicLazyInitializer.java @@ -8,7 +8,6 @@ import org.hibernate.LazyInitializationException; import org.hibernate.engine.spi.SharedSessionContractImplementor; -import org.hibernate.internal.util.MarkerObject; import org.hibernate.proxy.AbstractLazyInitializer; import org.hibernate.type.CompositeType; @@ -21,8 +20,6 @@ */ public abstract class BasicLazyInitializer extends AbstractLazyInitializer { - protected static final Object INVOKE_IMPLEMENTATION = new MarkerObject( "INVOKE_IMPLEMENTATION" ); - protected final Class persistentClass; protected final Method getIdentifierMethod; protected final Method setIdentifierMethod; @@ -50,7 +47,10 @@ protected BasicLazyInitializer( protected abstract Object serializableProxy(); - protected final Object invoke(Method method, Object[] args, Object proxy) throws Throwable { + protected abstract Object call(Object proxy, Method method, Object[] args) throws Throwable; + + protected final Object invoke(Method method, Object[] args, Object proxy) + throws Throwable { final String methodName = method.getName(); switch ( args.length ) { case 0: @@ -74,7 +74,7 @@ else if ( "getHibernateLazyInitializer".equals( methodName ) ) { else if ( method.equals( setIdentifierMethod ) ) { initialize(); setIdentifier( args[0] ); - return INVOKE_IMPLEMENTATION; + return call( proxy, method, args ); } break; } @@ -85,7 +85,7 @@ else if ( method.equals( setIdentifierMethod ) ) { } // otherwise: - return INVOKE_IMPLEMENTATION; + return call( proxy, method, args ); } private Object getReplacement() { diff --git a/hibernate-core/src/main/java/org/hibernate/proxy/pojo/bytebuddy/ByteBuddyInterceptor.java b/hibernate-core/src/main/java/org/hibernate/proxy/pojo/bytebuddy/ByteBuddyInterceptor.java index d625895b55f5..9c3d196dfa39 100644 --- a/hibernate-core/src/main/java/org/hibernate/proxy/pojo/bytebuddy/ByteBuddyInterceptor.java +++ b/hibernate-core/src/main/java/org/hibernate/proxy/pojo/bytebuddy/ByteBuddyInterceptor.java @@ -37,43 +37,42 @@ public ByteBuddyInterceptor( @Override public Object intercept(Object proxy, Method method, Object[] args) throws Throwable { - final Object result = this.invoke( method, args, proxy ); - if ( result == INVOKE_IMPLEMENTATION ) { - final Object target = getImplementation(); - final Object returnValue; - try { - if ( isPublic( persistentClass, method ) ) { - if ( !method.getDeclaringClass().isInstance( target ) ) { - throw new ClassCastException( - target.getClass().getName() - + " incompatible with " - + method.getDeclaringClass().getName() - ); - } - returnValue = method.invoke( target, args ); - } - else { - method.setAccessible( true ); - returnValue = method.invoke( target, args ); - } + return invoke( method, args, proxy ); + } - if ( returnValue == target ) { - final var returnValueClass = returnValue.getClass(); - if ( returnValueClass.isInstance( proxy ) ) { - return proxy; - } - else { - CORE_LOGGER.narrowingProxy( returnValueClass ); - } + @Override + protected Object call(Object proxy, Method method, Object[] args) throws Throwable { + final Object target = getImplementation(); + final Object returnValue; + try { + if ( isPublic( persistentClass, method ) ) { + if ( !method.getDeclaringClass().isInstance( target ) ) { + throw new ClassCastException( + target.getClass().getName() + + " incompatible with " + + method.getDeclaringClass().getName() + ); } - return returnValue; + returnValue = method.invoke( target, args ); } - catch (InvocationTargetException ite) { - throw ite.getTargetException(); + else { + method.setAccessible( true ); + returnValue = method.invoke( target, args ); + } + + if ( returnValue == target ) { + final var returnValueClass = returnValue.getClass(); + if ( returnValueClass.isInstance( proxy ) ) { + return proxy; + } + else { + CORE_LOGGER.narrowingProxy( returnValueClass ); + } } + return returnValue; } - else { - return result; + catch (InvocationTargetException ite) { + throw ite.getTargetException(); } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/CommonQueryContract.java b/hibernate-core/src/main/java/org/hibernate/query/CommonQueryContract.java index 4986bcaaf28f..b22c990ac617 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/CommonQueryContract.java +++ b/hibernate-core/src/main/java/org/hibernate/query/CommonQueryContract.java @@ -59,9 +59,7 @@ * Setting the {@linkplain QueryFlushMode query flush mode} does not affect the flush * mode of other operations performed via the parent {@linkplain Session session}. * This operation is usually used as follows: - *

        - *

        query.setQueryFlushMode(NO_FLUSH).getResultList()
        - *

        + *

        {@code query.setQueryFlushMode(NO_FLUSH).getResultList() }
        * The call to {@code setQueryFlushMode(NO_FLUSH)} disables the usual automatic flush * operation that occurs before query execution. * diff --git a/hibernate-core/src/main/java/org/hibernate/query/NativeQuery.java b/hibernate-core/src/main/java/org/hibernate/query/NativeQuery.java index 34965418c134..b00a80d7ea34 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/NativeQuery.java +++ b/hibernate-core/src/main/java/org/hibernate/query/NativeQuery.java @@ -619,8 +619,8 @@ interface FetchReturn extends ResultNode { NativeQuery setHint(String hintName, Object value); /** - * @inheritDoc - * + * {@inheritDoc} + *

        * This operation is supported even for native queries. * Note that specifying an explicit lock mode might * result in changes to the native SQL query that is @@ -630,7 +630,7 @@ interface FetchReturn extends ResultNode { LockOptions getLockOptions(); /** - * @inheritDoc + * {@inheritDoc} * * This operation is supported even for native queries. * Note that specifying an explicit lock mode might @@ -652,8 +652,8 @@ interface FetchReturn extends ResultNode { LockModeType getLockMode(); /** - * @inheritDoc - * + * {@inheritDoc} + *

        * This operation is supported even for native queries. * Note that specifying an explicit lock mode might * result in changes to the native SQL query that is @@ -676,8 +676,8 @@ interface FetchReturn extends ResultNode { NativeQuery setLockMode(LockModeType lockMode); /** - * @inheritDoc - * + * {@inheritDoc} + *

        * This operation is supported even for native queries. * Note that specifying an explicit lock mode might * result in changes to the native SQL query that is diff --git a/hibernate-core/src/main/java/org/hibernate/query/QueryArgumentException.java b/hibernate-core/src/main/java/org/hibernate/query/QueryArgumentException.java index 828f3ac61e17..1b6705d39364 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/QueryArgumentException.java +++ b/hibernate-core/src/main/java/org/hibernate/query/QueryArgumentException.java @@ -15,11 +15,20 @@ */ public class QueryArgumentException extends IllegalArgumentException { private final Class parameterType; + private final Class argumentType; private final Object argument; public QueryArgumentException(String message, Class parameterType, Object argument) { - super(message); + super( message + " (argument [" + argument + "] is not assignable to " + parameterType.getName() + ")" ); this.parameterType = parameterType; + this.argumentType = argument == null ? null : argument.getClass(); + this.argument = argument; + } + + public QueryArgumentException(String message, Class parameterType, Class argumentType, Object argument) { + super( message + " (" + argumentType.getName() + " is not assignable to " + parameterType.getName() + ")" ); + this.parameterType = parameterType; + this.argumentType = argumentType; this.argument = argument; } @@ -27,6 +36,10 @@ public Class getParameterType() { return parameterType; } + public Class getArgumentType() { + return argumentType; + } + public Object getArgument() { return argument; } diff --git a/hibernate-core/src/main/java/org/hibernate/query/QueryLogging.java b/hibernate-core/src/main/java/org/hibernate/query/QueryLogging.java index c3165a97e5a3..eae24d66f98c 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/QueryLogging.java +++ b/hibernate-core/src/main/java/org/hibernate/query/QueryLogging.java @@ -17,6 +17,7 @@ import org.jboss.logging.annotations.ValidIdRange; import java.lang.invoke.MethodHandles; +import java.util.Locale; import static org.jboss.logging.Logger.Level.ERROR; import static org.jboss.logging.Logger.Level.INFO; @@ -36,7 +37,7 @@ public interface QueryLogging extends BasicLogger { String LOGGER_NAME = SubSystemLogging.BASE + ".query"; Logger QUERY_LOGGER = Logger.getLogger( LOGGER_NAME ); - QueryLogging QUERY_MESSAGE_LOGGER = Logger.getMessageLogger( MethodHandles.lookup(), QueryLogging.class, LOGGER_NAME ); + QueryLogging QUERY_MESSAGE_LOGGER = Logger.getMessageLogger( MethodHandles.lookup(), QueryLogging.class, LOGGER_NAME, Locale.ROOT ); static String subLoggerName(String subName) { return LOGGER_NAME + '.' + subName; diff --git a/hibernate-core/src/main/java/org/hibernate/query/QueryProducer.java b/hibernate-core/src/main/java/org/hibernate/query/QueryProducer.java index 1c211239818b..edbb71ad176f 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/QueryProducer.java +++ b/hibernate-core/src/main/java/org/hibernate/query/QueryProducer.java @@ -38,6 +38,7 @@ * * This interface declares operations for creating instances of these objects. *

    + * * * * @@ -68,7 +69,6 @@ * * *
    Operations of different query types
    Selection{@link #createNamedMutationQuery(String)}
    - *

    * Operations like {@link #createSelectionQuery(String, Class) createSelectionQuery()}, * {@link #createNamedSelectionQuery(String, Class) createNamedSelectionQuery()}, and * {@link #createNativeQuery(String, Class) createNativeQuery()} accept a Java diff --git a/hibernate-core/src/main/java/org/hibernate/query/ResultListTransformer.java b/hibernate-core/src/main/java/org/hibernate/query/ResultListTransformer.java index 6c56622f4841..0a6b6afaf452 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/ResultListTransformer.java +++ b/hibernate-core/src/main/java/org/hibernate/query/ResultListTransformer.java @@ -4,14 +4,19 @@ */ package org.hibernate.query; +import java.util.Arrays; import java.util.List; +import java.util.Objects; import org.hibernate.Incubating; +import org.hibernate.NonUniqueResultException; /** * Defines some processing performed on the overall result {@link List} * of a {@link Query} before the result list is returned to the caller. * + * @param The result list element type + * * @see Query#setResultListTransformer * @see Query#list * @see Query#getResultList @@ -36,4 +41,33 @@ public interface ResultListTransformer { * @return The transformed result. */ List transformList(List resultList); + + /** + * A {@code ResultListTransformer} which collapses a list of results + * into a single result when all results are equal, or throws + * {@link NonUniqueResultException} otherwise. + * + * @param The result list element type + * + * @since 7.3 + */ + static ResultListTransformer uniqueResultTransformer() { + return resultList -> + switch ( resultList.size() ) { + case 0, 1 -> resultList; + default -> { + final var first = resultList.get( 0 ); + for ( int i = 1; i < resultList.size(); i++ ) { + final T current = resultList.get( i ); + if ( !Objects.equals( first, current ) + // also consider case of multiple select items in an Object[] + && !( first instanceof Object[] array + && !Arrays.equals( array, (Object[]) current ) ) ) { + throw new NonUniqueResultException( resultList.size() ); + } + } + yield List.of( first ); + } + }; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/HibernateCriteriaBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/HibernateCriteriaBuilder.java index d21452932703..d76383e2f79f 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/HibernateCriteriaBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/HibernateCriteriaBuilder.java @@ -90,6 +90,7 @@ * @since 6.0 * * @author Steve Ebersole + * @author Yoobin Yoon */ @Incubating public interface HibernateCriteriaBuilder extends CriteriaBuilder { @@ -2744,6 +2745,54 @@ JpaExpression arrayAgg( @Incubating JpaExpression arrayTrim(Expression arrayExpression, Integer elementCount); + /** + * Reverses the order of elements in an array. + * + * @since 7.2 + */ + @Incubating + JpaExpression arrayReverse(Expression arrayExpression); + + /** + * Sorts the elements of an array in ascending order. + * + * @since 7.2 + */ + @Incubating + JpaExpression arraySort(Expression arrayExpression); + + /** + * Sorts the elements of an array in the specified order. + * + * @since 7.2 + */ + @Incubating + JpaExpression arraySort(Expression arrayExpression, boolean descending); + + /** + * Sorts the elements of an array in the specified order. + * + * @since 7.2 + */ + @Incubating + JpaExpression arraySort(Expression arrayExpression, Expression descendingExpression); + + /** + * Create an expression that sorts the given array with explicit null ordering. + * + * @since 7.2 + */ + @Incubating + JpaExpression arraySort(Expression arrayExpression, boolean descending, boolean nullsFirst); + + /** + * Create an expression that sorts the given array with explicit null ordering. + * + * @since 7.2 + */ + @Incubating + JpaExpression arraySort(Expression arrayExpression, Expression descendingExpression, Expression nullsFirstExpression); + /** * Creates array with the same element N times, as specified by the arguments. * @@ -3403,6 +3452,62 @@ default JpaPredicate arrayOverlapsNullable(T[] array1, Expression array @Incubating > JpaExpression collectionTrim(Expression arrayExpression, Integer elementCount); + /** + * Create an expression that reverses the order of the elements of a collection. + * + * @since 7.2 + */ + @Incubating + > JpaExpression collectionReverse(Expression collectionExpression); + + /** + * Create an expression that sorts the elements of a collection. + * + * @since 7.2 + */ + @Incubating + > JpaExpression collectionSort(Expression collectionExpression); + + /** + * Create an expression that sorts the given collection in specified order. + * + * @since 7.2 + */ + @Incubating + > JpaExpression collectionSort(Expression collectionExpression, boolean descending); + + /** + * Create an expression that sorts the given collection in specified order. + * + * @since 7.2 + */ + @Incubating + > JpaExpression collectionSort( + Expression collectionExpression, + Expression descendingExpression); + + /** + * Create an expression that sorts the given collection with explicit null ordering. + * + * @since 7.2 + */ + @Incubating + > JpaExpression collectionSort( + Expression collectionExpression, + boolean descending, + boolean nullsFirst); + + /** + * Create an expression that sorts the given collection with explicit null ordering. + * + * @since 7.2 + */ + @Incubating + > JpaExpression collectionSort( + Expression collectionExpression, + Expression descendingExpression, + Expression nullsFirstExpression); + /** * Creates basic collection with the same element N times, as specified by the arguments. * diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/spi/HibernateCriteriaBuilderDelegate.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/spi/HibernateCriteriaBuilderDelegate.java index 2a67e5ab3625..71e4cd403596 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/spi/HibernateCriteriaBuilderDelegate.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/spi/HibernateCriteriaBuilderDelegate.java @@ -2557,6 +2557,45 @@ public JpaExpression arrayTrim(Expression arrayExpression, Integer return criteriaBuilder.arrayTrim( arrayExpression, elementCount ); } + @Override + @Incubating + public JpaExpression arrayReverse(Expression arrayExpression) { + return criteriaBuilder.arrayReverse( arrayExpression ); + } + + @Override + @Incubating + public JpaExpression arraySort(Expression arrayExpression) { + return criteriaBuilder.arraySort( arrayExpression ); + } + + @Override + @Incubating + public JpaExpression arraySort(Expression arrayExpression, boolean descending) { + return criteriaBuilder.arraySort( arrayExpression, descending ); + } + + @Override + @Incubating + public JpaExpression arraySort(Expression arrayExpression, Expression descendingExpression) { + return criteriaBuilder.arraySort( arrayExpression, descendingExpression ); + } + + @Override + @Incubating + public JpaExpression arraySort(Expression arrayExpression, boolean descending, boolean nullsFirst) { + return criteriaBuilder.arraySort( arrayExpression, descending, nullsFirst ); + } + + @Override + @Incubating + public JpaExpression arraySort( + Expression arrayExpression, + Expression descendingExpression, + Expression nullsFirstExpression) { + return criteriaBuilder.arraySort( arrayExpression, descendingExpression, nullsFirstExpression ); + } + @Override @Incubating public JpaExpression arrayFill( @@ -3103,6 +3142,52 @@ public > JpaExpression collectionTrim( return criteriaBuilder.collectionTrim( arrayExpression, elementCount ); } + @Override + @Incubating + public > JpaExpression collectionReverse(Expression collectionExpression) { + return criteriaBuilder.collectionReverse( collectionExpression ); + } + + @Override + @Incubating + public > JpaExpression collectionSort(Expression collectionExpression) { + return criteriaBuilder.collectionSort( collectionExpression ); + } + + @Override + @Incubating + public > JpaExpression collectionSort( + Expression collectionExpression, + boolean descending) { + return criteriaBuilder.collectionSort( collectionExpression, descending ); + } + + @Override + @Incubating + public > JpaExpression collectionSort( + Expression collectionExpression, + Expression descendingExpression) { + return criteriaBuilder.collectionSort( collectionExpression, descendingExpression ); + } + + @Override + @Incubating + public > JpaExpression collectionSort( + Expression collectionExpression, + boolean descending, + boolean nullsFirst) { + return criteriaBuilder.collectionSort( collectionExpression, descending, nullsFirst ); + } + + @Override + @Incubating + public > JpaExpression collectionSort( + Expression collectionExpression, + Expression descendingExpression, + Expression nullsFirstExpression) { + return criteriaBuilder.collectionSort( collectionExpression, descendingExpression, nullsFirstExpression ); + } + @Override @Incubating public JpaExpression> collectionFill( diff --git a/hibernate-core/src/main/java/org/hibernate/query/hql/HqlLogging.java b/hibernate-core/src/main/java/org/hibernate/query/hql/HqlLogging.java index dc984324048c..d9fff5863628 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/hql/HqlLogging.java +++ b/hibernate-core/src/main/java/org/hibernate/query/hql/HqlLogging.java @@ -14,6 +14,7 @@ import org.jboss.logging.annotations.ValidIdRange; import java.lang.invoke.MethodHandles; +import java.util.Locale; /** * @author Steve Ebersole @@ -28,7 +29,7 @@ public interface HqlLogging extends BasicLogger { String LOGGER_NAME = QueryLogging.LOGGER_NAME + ".hql"; - HqlLogging QUERY_LOGGER = Logger.getMessageLogger( MethodHandles.lookup(), HqlLogging.class, LOGGER_NAME ); + HqlLogging QUERY_LOGGER = Logger.getMessageLogger( MethodHandles.lookup(), HqlLogging.class, LOGGER_NAME, Locale.ROOT ); static String subLoggerName(String subName) { return LOGGER_NAME + '.' + subName; @@ -39,6 +40,6 @@ static Logger subLogger(String subName) { } static T subLogger(String subName, Class loggerJavaType) { - return Logger.getMessageLogger( MethodHandles.lookup(), loggerJavaType, subLoggerName( subName ) ); + return Logger.getMessageLogger( MethodHandles.lookup(), loggerJavaType, subLoggerName( subName ), Locale.ROOT ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/BasicDotIdentifierConsumer.java b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/BasicDotIdentifierConsumer.java index 1cd1d5d0e2ac..a1dabf39bdc8 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/BasicDotIdentifierConsumer.java +++ b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/BasicDotIdentifierConsumer.java @@ -5,7 +5,6 @@ package org.hibernate.query.hql.internal; import org.hibernate.metamodel.model.domain.JpaMetamodel; -import org.hibernate.metamodel.model.domain.ManagedDomainType; import org.hibernate.query.SemanticException; import org.hibernate.query.hql.spi.DotIdentifierConsumer; import org.hibernate.query.hql.spi.SemanticPathPart; @@ -16,6 +15,7 @@ import org.hibernate.query.sqm.function.SqmFunctionDescriptor; import org.hibernate.query.sqm.spi.SqmCreationContext; import org.hibernate.query.sqm.tree.domain.SqmPath; +import org.hibernate.query.sqm.tree.domain.SqmTreatedPath; import org.hibernate.query.sqm.tree.expression.SqmEnumLiteral; import org.hibernate.query.sqm.tree.expression.SqmExpression; import org.hibernate.query.sqm.tree.expression.SqmFieldLiteral; @@ -92,15 +92,18 @@ public void consumeIdentifier(String identifier, boolean isBase, boolean isTermi @Override public void consumeTreat(String importableName, boolean isTerminal) { - final SqmPath sqmPath = (SqmPath) currentPart; - currentPart = sqmPath.treatAs( treatTarget( importableName ) ); + currentPart = treat( importableName, (SqmPath) currentPart ); } - private Class treatTarget(String typeName) { - final ManagedDomainType managedType = + private SqmTreatedPath treat(String importableName, SqmPath path) { + return path.treatAs( treatTarget( path, importableName ) ); + } + + private Class treatTarget(SqmPath path, String typeName) { + final var javaType = creationState.getCreationContext().getJpaMetamodel() - .managedType( typeName ); - return managedType.getJavaType(); + .managedType( typeName ).getJavaType(); + return javaType.asSubclass( path.getJavaType() ); } protected void reset() { @@ -195,8 +198,7 @@ private SemanticPathPart resolvePath(String identifier, boolean isTerminal, SqmC if ( pathRootByExposedNavigable != null ) { // identifier is an "unqualified attribute reference" validateAsRoot( pathRootByExposedNavigable ); - final SqmPath sqmPath = - pathRootByExposedNavigable.get( identifier, true ); + final var sqmPath = pathRootByExposedNavigable.get( identifier, true ); return isTerminal ? sqmPath : new DomainPathPart( sqmPath ); } @@ -213,7 +215,7 @@ private SemanticPathPart resolveLiteralType(JpaMetamodel jpaMetamodel, NodeBuild return null; } else { - final ManagedDomainType managedType = jpaMetamodel.managedType( importableName ); + final var managedType = jpaMetamodel.managedType( importableName ); if ( managedType instanceof SqmEntityDomainType entityDomainType ) { return new SqmLiteralEntityType<>( entityDomainType, nodeBuilder ); } @@ -257,7 +259,7 @@ private static SqmFieldLiteral sqmFieldLiteral( JavaType fieldJtdTest, NodeBuilder nodeBuilder) { return new SqmFieldLiteral<>( - jpaMetamodel.getJavaConstant( prefix, terminal ), + jpaMetamodel.getJavaConstant( prefix, terminal, fieldJtdTest.getJavaTypeClass() ), fieldJtdTest, terminal, nodeBuilder 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 7f2e838a6067..39aeed17f989 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 @@ -4,22 +4,19 @@ */ package org.hibernate.query.hql.internal; -import org.hibernate.metamodel.model.domain.EntityDomainType; -import org.hibernate.metamodel.model.domain.ManagedDomainType; import org.hibernate.query.PathException; import org.hibernate.query.SemanticException; import org.hibernate.query.hql.spi.DotIdentifierConsumer; import org.hibernate.query.hql.spi.SemanticPathPart; import org.hibernate.query.hql.spi.SqmCreationProcessingState; import org.hibernate.query.hql.spi.SqmCreationState; -import org.hibernate.query.hql.spi.SqmPathRegistry; import org.hibernate.query.sqm.SqmJoinable; import org.hibernate.query.sqm.SqmPathSource; import org.hibernate.query.sqm.spi.SqmCreationHelper; import org.hibernate.query.sqm.tree.SqmJoinType; -import org.hibernate.query.sqm.tree.cte.SqmCteStatement; import org.hibernate.query.sqm.tree.domain.SqmPath; import org.hibernate.query.sqm.tree.domain.SqmPolymorphicRootDescriptor; +import org.hibernate.query.sqm.tree.domain.SqmTreatedFrom; import org.hibernate.query.sqm.tree.from.SqmAttributeJoin; import org.hibernate.query.sqm.tree.from.SqmCteJoin; import org.hibernate.query.sqm.tree.from.SqmEntityJoin; @@ -27,6 +24,7 @@ import org.hibernate.query.sqm.tree.from.SqmJoin; import org.hibernate.query.sqm.tree.from.SqmRoot; +import org.hibernate.query.sqm.tree.from.SqmTreatedAttributeJoin; import org.jboss.logging.Logger; import static org.hibernate.query.sqm.internal.SqmUtil.findCompatibleFetchJoin; @@ -125,7 +123,7 @@ public void consumeTreat(String entityName, boolean isTerminal) { private ConsumerDelegate resolveBase(String identifier, boolean isTerminal) { final SqmCreationProcessingState processingState = creationState.getCurrentProcessingState(); - final SqmPathRegistry pathRegistry = processingState.getPathRegistry(); + final var pathRegistry = processingState.getPathRegistry(); final SqmFrom pathRootByAlias = pathRegistry.findFromByAlias( identifier, true ); if ( pathRootByAlias != null ) { return resolveAlias( identifier, isTerminal, pathRootByAlias ); @@ -240,7 +238,7 @@ else if ( fetch ) { boolean allowReuse, SqmCreationState creationState, SqmJoinable joinSource) { - final SqmJoin join = joinSource.createSqmJoin( + final var join = joinSource.createSqmJoin( lhs, joinType, isTerminal ? alias : allowReuse ? SqmCreationHelper.IMPLICIT_ALIAS : null, @@ -296,22 +294,39 @@ public void consumeIdentifier(String identifier, boolean isTerminal, boolean all @Override public void consumeTreat(String typeName, boolean isTerminal) { + currentPath = treat( typeName, isTerminal ); + creationState.getCurrentProcessingState().getPathRegistry().register( currentPath ); + } + + private SqmTreatedFrom treat(String typeName, boolean isTerminal) { if ( isTerminal ) { - currentPath = fetch - ? ( (SqmAttributeJoin) currentPath ).treatAs( treatTarget( typeName ), alias, true ) - : currentPath.treatAs( treatTarget( typeName ), alias ); + return fetch + ? treatTerminalFetch( currentPath, typeName ) + : treatTerminal( currentPath, typeName ); } else { - currentPath = currentPath.treatAs( treatTarget( typeName ) ); + return treatNonTerminal( currentPath, typeName ); } - creationState.getCurrentProcessingState().getPathRegistry().register( currentPath ); } - private Class treatTarget(String typeName) { - final ManagedDomainType managedType = creationState.getCreationContext() - .getJpaMetamodel() - .managedType( typeName ); - return managedType.getJavaType(); + private SqmTreatedFrom treatNonTerminal(SqmFrom path, String typeName) { + return path.treatAs( treatTarget( path, typeName ) ); + } + + private SqmTreatedAttributeJoin treatTerminalFetch(SqmFrom path, String typeName) { + final var attributeJoin = (SqmAttributeJoin) path; + return attributeJoin.treatAs( treatTarget( path, typeName ), alias, true ); + } + + private SqmTreatedFrom treatTerminal(SqmFrom path, String typeName) { + return path.treatAs( treatTarget( path, typeName ), alias ); + } + + private Class treatTarget(SqmPath path, String typeName) { + final var javaType = + creationState.getCreationContext().getJpaMetamodel() + .managedType( typeName ).getJavaType(); + return javaType.asSubclass( path.getJavaType() ); } @Override @@ -357,11 +372,11 @@ public void consumeIdentifier(String identifier, boolean isTerminal, boolean all path.append( identifier ); if ( isTerminal ) { final String fullPath = path.toString(); - final EntityDomainType joinedEntityType = + final var joinedEntityType = creationState.getCreationContext().getJpaMetamodel() .getHqlEntityReference( fullPath ); if ( joinedEntityType == null ) { - final SqmCteStatement cteStatement = creationState.findCteStatement( fullPath ); + final var cteStatement = creationState.findCteStatement( fullPath ); if ( cteStatement != null ) { //noinspection rawtypes,unchecked join = new SqmCteJoin( cteStatement, alias, joinType, sqmRoot ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/QuerySplitter.java b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/QuerySplitter.java index b7c294e5577d..cca925e7b74f 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/QuerySplitter.java +++ b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/QuerySplitter.java @@ -78,7 +78,7 @@ private static > S copyStatement( final SqmCopyContext context = SqmCopyContext.noParamCopyContext(); // Copy the statement replacing the root's unmapped polymorphic reference with // the concrete mapped descriptor entity domain type. - final SqmRoot path = context.registerCopy( + final var path = context.registerCopy( unmappedPolymorphicReference, new SqmRoot<>( mappedDescriptor, 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 2e721a911f52..6ac675948e1f 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 @@ -44,6 +44,7 @@ import org.hibernate.metamodel.CollectionClassification; import org.hibernate.metamodel.mapping.CollectionPart; import org.hibernate.metamodel.mapping.internal.AnyKeyPart; +import org.hibernate.metamodel.model.domain.AnyMappingDomainType; import org.hibernate.metamodel.model.domain.EntityDomainType; import org.hibernate.metamodel.model.domain.IdentifiableDomainType; import org.hibernate.metamodel.model.domain.JpaMetamodel; @@ -52,7 +53,6 @@ import org.hibernate.metamodel.model.domain.PluralPersistentAttribute; import org.hibernate.metamodel.model.domain.SingularPersistentAttribute; import org.hibernate.metamodel.model.domain.internal.AnyDiscriminatorSqmPath; -import org.hibernate.metamodel.model.domain.internal.EntitySqmPathSource; import org.hibernate.query.ParameterLabelException; import org.hibernate.query.PathException; import org.hibernate.query.SemanticException; @@ -2138,8 +2138,6 @@ public final SqmCrossJoin visitCrossJoin(HqlParser.CrossJoinContext ctx) { protected void consumeCrossJoin(HqlParser.CrossJoinContext parserJoin, SqmRoot sqmRoot) { final String name = getEntityName( parserJoin.entityName() ); -// SqmTreeCreationLogger.LOGGER.tracef( "Handling root path - %s", name ); - final var entityDescriptor = getJpaMetamodel().resolveHqlEntityReference( name ); if ( entityDescriptor instanceof SqmPolymorphicRootDescriptor ) { @@ -2579,8 +2577,8 @@ else if ( r instanceof AnyDiscriminatorSqmPath anyDiscriminatorPath && l inst ); } - private SqmExpression createDiscriminatorValue( - AnyDiscriminatorSqmPath anyDiscriminatorTypeSqmPath, + private SqmExpression createDiscriminatorValue( + AnyDiscriminatorSqmPath anyDiscriminatorTypeSqmPath, HqlParser.ExpressionContext valueExpressionContext) { final var expressible = anyDiscriminatorTypeSqmPath.getExpressible(); return new SqmAnyDiscriminatorValue<>( @@ -3531,10 +3529,10 @@ else if ( attributes.size() >1 ) { @Override public SqmFkExpression visitToOneFkReference(HqlParser.ToOneFkReferenceContext ctx) { final var sqmPath = consumeDomainPath( (HqlParser.PathContext) ctx.getChild( 2 ) ); - final var toOneReference = sqmPath.getReferencedPathSource(); + final var toOneReference = sqmPath.getResolvedModel(); final boolean validToOneRef = toOneReference.getBindableType() == Bindable.BindableType.SINGULAR_ATTRIBUTE - && toOneReference instanceof EntitySqmPathSource; + && toOneReference.getPathType() instanceof SqmEntityDomainType; if ( !validToOneRef ) { throw new FunctionArgumentException( String.format( @@ -6073,24 +6071,26 @@ private SqmPath consumeDomainPath(HqlParser.SimplePathContext sequence) { private SqmPath consumeManagedTypeReference(HqlParser.PathContext parserPath) { final var sqmPath = consumeDomainPath( parserPath ); - final var pathSource = sqmPath.getReferencedPathSource(); - if ( pathSource.getPathType() instanceof ManagedDomainType ) { + final var pathType = sqmPath.getReferencedPathSource().getPathType(); + if ( pathType instanceof ManagedDomainType || pathType instanceof AnyMappingDomainType ) { return sqmPath; } else { - throw new PathException( "Expecting ManagedType valued path [" + sqmPath.getNavigablePath() - + "], but found: " + pathSource.getPathType() ); + throw new PathException( "Expecting ManagedType or @Any valued path [" + + sqmPath.getNavigablePath() + "], but found: " + pathType ); } } private SqmPath consumePluralAttributeReference(HqlParser.PathContext parserPath) { final var sqmPath = consumeDomainPath( parserPath ); - if ( sqmPath.getReferencedPathSource() instanceof PluralPersistentAttribute ) { + final var pathSource = sqmPath.getReferencedPathSource(); + if ( pathSource instanceof PluralPersistentAttribute ) { return sqmPath; } else { - throw new PathException( "Expecting plural attribute valued path [" + sqmPath.getNavigablePath() - + "], but found: " + sqmPath.getReferencedPathSource().getPathType() ); + throw new PathException( "Expecting plural attribute valued path [" + + sqmPath.getNavigablePath() + "], but found: " + + pathSource.getPathType() ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/StandardHqlTranslator.java b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/StandardHqlTranslator.java index 3b6eef1846c7..50ee1fbc9bb9 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/StandardHqlTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/StandardHqlTranslator.java @@ -8,7 +8,6 @@ import org.antlr.v4.runtime.InputMismatchException; import org.antlr.v4.runtime.NoViableAltException; import org.hibernate.QueryException; -import org.hibernate.grammars.hql.HqlLexer; import org.hibernate.grammars.hql.HqlParser; import org.hibernate.query.sqm.EntityTypeException; import org.hibernate.query.sqm.PathElementException; @@ -24,7 +23,6 @@ import org.hibernate.query.sqm.spi.SqmCreationContext; import org.hibernate.query.sqm.tree.SqmStatement; -import org.antlr.v4.runtime.ANTLRErrorListener; import org.antlr.v4.runtime.BailErrorStrategy; import org.antlr.v4.runtime.BaseErrorListener; import org.antlr.v4.runtime.DefaultErrorStrategy; @@ -33,7 +31,6 @@ import org.antlr.v4.runtime.atn.PredictionMode; import org.antlr.v4.runtime.misc.ParseCancellationException; -import static java.util.stream.Collectors.toList; import static org.hibernate.query.hql.HqlLogging.QUERY_LOGGER; /** @@ -91,10 +88,9 @@ public SqmStatement translate(String query, Class expectedResultType) private HqlParser.StatementContext parseHql(String hql) { // Build the lexer - final HqlLexer hqlLexer = HqlParseTreeBuilder.INSTANCE.buildHqlLexer( hql ); - + final var hqlLexer = HqlParseTreeBuilder.INSTANCE.buildHqlLexer( hql ); // Build the parse tree - final HqlParser hqlParser = HqlParseTreeBuilder.INSTANCE.buildHqlParser( hql, hqlLexer ); + final var hqlParser = HqlParseTreeBuilder.INSTANCE.buildHqlParser( hql, hqlLexer ); // try to use SLL(k)-based parsing first - it's faster hqlParser.getInterpreter().setPredictionMode( PredictionMode.SLL ); @@ -104,7 +100,7 @@ private HqlParser.StatementContext parseHql(String hql) { try { return hqlParser.statement(); } - catch (ParseCancellationException e) { + catch ( ParseCancellationException e ) { // When resetting the parser, its CommonTokenStream will seek(0) i.e. restart emitting buffered tokens. // This is enough when reusing the lexer and parser, and it would be wrong to also reset the lexer. // Resetting the lexer causes it to hand out tokens again from the start, which will then append to the @@ -117,14 +113,12 @@ private HqlParser.StatementContext parseHql(String hql) { // fall back to LL(k)-based parsing hqlParser.getInterpreter().setPredictionMode( PredictionMode.LL ); hqlParser.setErrorHandler( new DefaultErrorStrategy() ); - - final ANTLRErrorListener errorListener = new BaseErrorListener() { + hqlParser.addErrorListener( new BaseErrorListener() { @Override - public void syntaxError(Recognizer recognizer, Object offendingSymbol, int line, int charPositionInLine, String msg, RecognitionException e) { - throw new SyntaxException( prettifyAntlrError( offendingSymbol, line, charPositionInLine, msg, e, hql, true ), hql ); + public void syntaxError(Recognizer recognizer, Object offendingSymbol, int line, int charPositionInLine, String msg, RecognitionException e1) { + throw new SyntaxException( prettifyAntlrError( offendingSymbol, line, charPositionInLine, msg, e1, hql, true ), hql ); } - }; - hqlParser.addErrorListener( errorListener ); + } ); return hqlParser.statement(); } @@ -163,8 +157,34 @@ public static String prettifyAntlrError( errorText += "'*' (empty query string)"; } else { - String lineText = hql.lines().collect( toList() ).get( line -1 ); - String text = lineText.substring( 0, charPositionInLine) + "*" + lineText.substring(charPositionInLine); + // don't use String.lines() because it strips trailing blank lines + final String[] lines = hql.split( "\\R", -1 ); + final String currentLineText = lines[line - 1]; + final String errorLineText; + if ( currentLineText.isEmpty() ) { // no text on the line with the error + if ( lines.length == line ) { // on last line + // back up to end of previous line, if any + if ( line == 1 ) { + // query has no text + errorLineText = currentLineText; + } + else { + errorLineText = lines[line - 2]; + charPositionInLine = errorLineText.length(); + } + } + else { + // move forward to start of next line + errorLineText = lines[line]; + charPositionInLine = 0; + } + } + else { + errorLineText = currentLineText; + } + final String text = + errorLineText.substring( 0, charPositionInLine ) + + "*" + errorLineText.substring( charPositionInLine ); errorText += "'" + text + "'"; } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/hql/spi/SemanticPathPart.java b/hibernate-core/src/main/java/org/hibernate/query/hql/spi/SemanticPathPart.java index a8947c0284f5..b8b84f3c9c8a 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/hql/spi/SemanticPathPart.java +++ b/hibernate-core/src/main/java/org/hibernate/query/hql/spi/SemanticPathPart.java @@ -5,22 +5,17 @@ package org.hibernate.query.hql.spi; import org.hibernate.query.sqm.tree.domain.SqmPath; -import org.hibernate.query.sqm.tree.domain.SqmSimplePath; import org.hibernate.query.sqm.tree.expression.SqmExpression; -/** - * @asciidoc - * - * Contract for things that can be part of a path structure, including: - * - * * package name - * * class name - * * field name - * * enum name - * * {@link SqmSimplePath} - * - * @author Steve Ebersole - */ +/// Contract for things that can be part of a path structure, including: +/// +/// * package name +/// * class name +/// * field name +/// * enum name +/// * [org.hibernate.query.sqm.tree.domain.SqmSimplePath] +/// +/// @author Steve Ebersole public interface SemanticPathPart { SemanticPathPart resolvePathPart( String name, diff --git a/hibernate-core/src/main/java/org/hibernate/query/internal/QueryArguments.java b/hibernate-core/src/main/java/org/hibernate/query/internal/QueryArguments.java new file mode 100644 index 000000000000..b11e89d9416d --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/internal/QueryArguments.java @@ -0,0 +1,127 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.query.internal; + +import jakarta.persistence.metamodel.Type; +import org.hibernate.HibernateException; +import org.hibernate.type.BindingContext; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.java.spi.EntityJavaType; + +import java.util.Collection; + +import static org.hibernate.proxy.HibernateProxy.extractLazyInitializer; + +/** + * @author Gavin King + */ +public class QueryArguments { + + private static boolean isInstance(Object value, JavaType javaType) { + try { + if ( value == null ) { + return true; + } + else if ( javaType instanceof EntityJavaType ) { + // special handling for entity arguments due to + // the possibility of an uninitialized proxy + // (which we don't want or need to fetch) + final var javaTypeClass = javaType.getJavaTypeClass(); + final var initializer = extractLazyInitializer( value ); + final var valueEntityClass = + initializer != null + ? initializer.getPersistentClass() + : value.getClass(); + // accept assignability in either direction + return javaTypeClass.isAssignableFrom( valueEntityClass ) + || valueEntityClass.isAssignableFrom( javaTypeClass ); + } + else { + // require that the argument be assignable to the parameter + return javaType.isInstance( javaType.coerce( value ) ); + } + } + catch (HibernateException ce) { + return false; + } + } + + public static boolean isInstance( + Type parameterType, Object value, + BindingContext bindingContext) { + if ( value == null ) { + return true; + } + final var sqmExpressible = bindingContext.resolveExpressible( parameterType ); + assert sqmExpressible != null; + final var javaType = sqmExpressible.getExpressibleJavaType(); + return isInstance( value, javaType ); + } + + public static boolean areInstances( + Type parameterType, Collection values, + BindingContext bindingContext) { + if ( values.isEmpty() ) { + return true; + } + final var sqmExpressible = bindingContext.resolveExpressible( parameterType ); + assert sqmExpressible != null; + final var javaType = sqmExpressible.getExpressibleJavaType(); + for ( Object value : values ) { + if ( !isInstance( value, javaType ) ) { + return false; + } + } + return true; + } + + public static boolean areInstances( + Type parameterType, Object[] values, + BindingContext bindingContext) { + if ( values.length == 0 ) { + return true; + } + if ( parameterType.getJavaType() + .isAssignableFrom( values.getClass().getComponentType() ) ) { + return true; + } + final var sqmExpressible = bindingContext.resolveExpressible( parameterType ); + assert sqmExpressible != null; + final var javaType = sqmExpressible.getExpressibleJavaType(); + for ( Object value : values ) { + if ( !isInstance( value, javaType ) ) { + return false; + } + } + return true; + } + + public static T cast(Object value, JavaType javaType) { + if ( value == null ) { + return null; + } + else if ( javaType instanceof EntityJavaType ) { + // special handling for entity arguments due to + // the possibility of an uninitialized proxy + // (which we don't want or need to fetch) + if ( isInstance( value, javaType ) ) { + // The proxy might not literally be an + // instance of the entity class represented + // by the unreified type T, but it is an + // instance in spirit + //noinspection unchecked + return (T) value; + } + else { + throw new ClassCastException( "Cannot cast to entity type '" + + javaType.getJavaTypeClass().getTypeName() + "'" ); + } + } + else { + // require that the argument be assignable to the parameter + return javaType.cast( javaType.coerce( value ) ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/internal/QueryInterpretationCacheStandardImpl.java b/hibernate-core/src/main/java/org/hibernate/query/internal/QueryInterpretationCacheStandardImpl.java index 7c3759472b80..5884213c5fb0 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/internal/QueryInterpretationCacheStandardImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/internal/QueryInterpretationCacheStandardImpl.java @@ -18,7 +18,6 @@ import org.hibernate.query.spi.QueryInterpretationCache; import org.hibernate.query.spi.QueryPlan; import org.hibernate.query.spi.SelectQueryPlan; -import org.hibernate.query.spi.SimpleHqlInterpretationImpl; import org.hibernate.query.sql.spi.ParameterInterpretation; import org.hibernate.query.sqm.internal.DomainParameterXref; import org.hibernate.service.ServiceRegistry; 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 1341b1fdd2ff..0b2279627f09 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 @@ -4,20 +4,25 @@ */ package org.hibernate.query.internal; +import java.util.ArrayList; import java.util.Collection; +import java.util.List; +import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.HibernateException; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.metamodel.mapping.BasicValuedMapping; +import org.hibernate.metamodel.mapping.JdbcMapping; import org.hibernate.metamodel.mapping.MappingModelExpressible; import org.hibernate.metamodel.mapping.ModelPart; +import org.hibernate.query.QueryArgumentException; import org.hibernate.type.BindableType; import org.hibernate.query.QueryParameter; import org.hibernate.query.spi.QueryParameterBinding; import org.hibernate.query.spi.QueryParameterBindingTypeResolver; -import org.hibernate.query.spi.QueryParameterBindingValidator; import org.hibernate.query.sqm.NodeBuilder; +import org.hibernate.type.NullType; import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.spi.TypeConfiguration; @@ -31,18 +36,18 @@ * * @author Steve Ebersole */ -public class QueryParameterBindingImpl implements QueryParameterBinding, JavaType.CoercionContext { +public class QueryParameterBindingImpl implements QueryParameterBinding { private final QueryParameter queryParameter; private final SessionFactoryImplementor sessionFactory; private boolean isBound; private boolean isMultiValued; - private @Nullable BindableType bindType; + private @Nullable BindableType bindType; private @Nullable MappingModelExpressible type; - private @Nullable TemporalType explicitTemporalPrecision; + private @Nullable @SuppressWarnings("deprecation") TemporalType explicitTemporalPrecision; - private Object bindValue; + private T bindValue; private Collection bindValues; /** @@ -60,7 +65,7 @@ protected QueryParameterBindingImpl( public QueryParameterBindingImpl( QueryParameter queryParameter, SessionFactoryImplementor sessionFactory, - BindableType bindType) { + @Nullable BindableType bindType) { this.queryParameter = queryParameter; this.sessionFactory = sessionFactory; this.bindType = bindType; @@ -70,13 +75,17 @@ private QueryParameterBindingTypeResolver getParameterBindingTypeResolver() { return sessionFactory.getMappingMetamodel(); } + public TypeConfiguration getTypeConfiguration() { + return sessionFactory.getTypeConfiguration(); + } + @Override - public @Nullable BindableType getBindType() { + public @Nullable BindableType getBindType() { return bindType; } @Override - public @Nullable TemporalType getExplicitTemporalPrecision() { + public @Nullable @SuppressWarnings("deprecation") TemporalType getExplicitTemporalPrecision() { return explicitTemporalPrecision; } @@ -107,90 +116,101 @@ public T getBindValue() { if ( isMultiValued ) { throw new IllegalStateException( "Binding is multi-valued; illegal call to #getBindValue" ); } - - //TODO: I believe this cast is unsound due to coercion - return (T) bindValue; + return bindValue; } @Override - public void setBindValue(T value, boolean resolveJdbcTypeIfNecessary) { - if ( !handleAsMultiValue( value, null ) ) { - final Object coerced = coerceIfNotJpa( value ); + public void setBindValue(Object value, boolean resolveJdbcTypeIfNecessary) { + if ( handleAsMultiValue( value, bindType ) ) { + setBindValues( (Collection) value ); + } + else { + final Object coerced = coerce( value ); validate( coerced ); - if ( value == null ) { // needed when setting a null value to the parameter of a native SQL query // TODO: this does not look like a very disciplined way to handle this bindNull( resolveJdbcTypeIfNecessary ); } else { - bindValue( coerced ); + initBindType( value ); + bindSingleValue( coerced ); } } } + @Override + public void setBindValue( + Object value, + @SuppressWarnings("deprecation") + TemporalType temporalTypePrecision) { + if ( handleAsMultiValue( value, bindType ) ) { + setBindValues( (Collection) value, temporalTypePrecision ); + } + else { + setExplicitTemporalPrecision( temporalTypePrecision ); + final Object coerced = coerce( value ); + validate( coerced ); + initBindType( value ); + bindSingleValue( coerced ); + } + } + + @Override + public void setBindValue(A value, @Nullable BindableType clarifiedType) { + // don't coerce, because value is already of the clarified type + validate( value ); + clarifyType( value, clarifiedType ); + bindSingleValue( value ); + } + + private void initBindType(Object value) { + if ( bindType == null ) { + @SuppressWarnings("unchecked") + // If there is no bindType set, then this is effectively a + // parameter of the top type. At least arguably safe cast. + final var self = (QueryParameterBindingImpl) this; + //noinspection UnnecessaryLocalVariable (needed to make javac happy) + final var valueType = + getParameterBindingTypeResolver() + .resolveParameterBindType( value ); + self.bindType = valueType; + } + } + private void bindNull(boolean resolveJdbcTypeIfNecessary) { isBound = true; bindValue = null; if ( resolveJdbcTypeIfNecessary && bindType == null ) { - bindType = (BindableType) + final var nullType = getTypeConfiguration().getBasicTypeRegistry() .getRegisteredType( "null" ); - } - } - - private boolean handleAsMultiValue(T value, @Nullable BindableType bindableType) { - if ( queryParameter.allowsMultiValuedBinding() - && value instanceof Collection - && !( bindableType == null - ? isRegisteredAsBasicType( value.getClass() ) - : bindableType.getJavaType().isInstance( value ) ) ) { //noinspection unchecked - setBindValues( (Collection) value ); - return true; - } - else { - return false; + bindType = (BindableType) nullType; } } - private boolean isRegisteredAsBasicType(Class valueClass) { - return getTypeConfiguration().getBasicTypeForJavaType( valueClass ) != null; + private boolean handleAsMultiValue(Object value, @Nullable BindableType bindableType) { + return queryParameter.allowsMultiValuedBinding() + && value instanceof Collection + && !validInstance( value, bindableType ); } - private void bindValue(Object value) { + private void bindSingleValue(Object value) { + bindValue = cast( value ); + bindValues = null; + isMultiValued = false; isBound = true; - bindValue = value; - if ( canBindValueBeSet( value, bindType ) ) { - bindType = getParameterBindingTypeResolver().resolveParameterBindType( value ); - } } - @Override - public void setBindValue(T value, @Nullable BindableType clarifiedType) { - if ( !handleAsMultiValue( value, clarifiedType ) ) { - if ( clarifiedType != null ) { - bindType = clarifiedType; - } - - final Object coerced = coerce( value ); - validate( coerced, clarifiedType ); - bindValue( coerced ); - } + private boolean validInstance(Object value, @Nullable BindableType bindableType) { + return bindableType == null + ? isRegisteredAsBasicType( value.getClass() ) + : bindableType.getJavaType().isInstance( value ); } - @Override - public void setBindValue(T value, TemporalType temporalTypePrecision) { - if ( !handleAsMultiValue( value, null ) ) { - if ( bindType == null ) { - bindType = queryParameter.getHibernateType(); - } - - final Object coerced = coerceIfNotJpa( value ); - validate( coerced, temporalTypePrecision ); - bindValue( coerced ); - setExplicitTemporalPrecision( temporalTypePrecision ); - } + private boolean isRegisteredAsBasicType(Class valueClass) { + return getTypeConfiguration().getBasicTypeForJavaType( valueClass ) != null; } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -205,55 +225,58 @@ public Collection getBindValues() { } @Override - public void setBindValues(Collection values) { - if ( !queryParameter.allowsMultiValuedBinding() ) { - throw new IllegalArgumentException( - "Illegal attempt to bind a collection value to a single-valued parameter" - ); - } - - this.isBound = true; - this.isMultiValued = true; - - this.bindValue = null; - this.bindValues = values; - - final var iterator = values.iterator(); - T value = null; - while ( value == null && iterator.hasNext() ) { - value = iterator.next(); - } - - if ( canBindValueBeSet( value, bindType ) ) { - bindType = getParameterBindingTypeResolver().resolveParameterBindType( value ); - } + public void setBindValues(Collection values) { + assertMultivalued(); + final var coerced = values.stream().map( this::coerce ).toList(); + coerced.forEach( this::validate ); + initBindType( firstNonNull( values ) ); + bindMultipleValues( coerced ); } @Override - public void setBindValues(Collection values, BindableType clarifiedType) { - if ( clarifiedType != null ) { - bindType = clarifiedType; - } + public void setBindValues( + Collection values, + @SuppressWarnings("deprecation") TemporalType temporalTypePrecision) { + setExplicitTemporalPrecision( temporalTypePrecision ); setBindValues( values ); } @Override - public void setBindValues( - Collection values, - TemporalType temporalTypePrecision, - TypeConfiguration typeConfiguration) { - setBindValues( values ); - setExplicitTemporalPrecision( temporalTypePrecision ); + public void setBindValues(Collection values, BindableType clarifiedType) { + assertMultivalued(); + // don't coerce, because value is already of the clarified type + values.forEach( this::validate ); + clarifyType( values, clarifiedType ); + bindMultipleValues( values ); + } + + private void bindMultipleValues(Collection coerced) { + final List list = new ArrayList<>(); + for ( var value : coerced ) { + list.add( cast( value ) ); + } + bindValues = list; + bindValue = null; + isMultiValued = true; + isBound = true; } - private void setExplicitTemporalPrecision(TemporalType precision) { + private void assertMultivalued() { + if ( !queryParameter.allowsMultiValuedBinding() ) { + throw new IllegalArgumentException( + "Illegal attempt to bind a collection value to a single-valued parameter" + ); + } + } + + private void setExplicitTemporalPrecision(@SuppressWarnings("deprecation") TemporalType precision) { explicitTemporalPrecision = precision; if ( bindType == null || isTemporal( determineJavaType( bindType ) ) ) { bindType = resolveTemporalPrecision( precision, bindType, getCriteriaBuilder() ); } } - private JavaType determineJavaType(BindableType bindType) { + private JavaType determineJavaType(BindableType bindType) { return getCriteriaBuilder().resolveExpressible( bindType ).getExpressibleJavaType(); } @@ -264,88 +287,133 @@ private JavaType determineJavaType(BindableType bindType) @Override @SuppressWarnings("unchecked") public boolean setType(@Nullable MappingModelExpressible type) { + final MappingModelExpressible previousType = this.type; this.type = type; // If the bind type is undetermined or the given type is a model part, then we try to apply a new bind type if ( bindType == null || bindType.getJavaType() == Object.class || type instanceof ModelPart ) { if ( type instanceof BindableType ) { final boolean changed = bindType != null && type != bindType; - bindType = (BindableType) type; + bindType = (BindableType) type; return changed; } else if ( type instanceof BasicValuedMapping basicValuedMapping ) { final var jdbcMapping = basicValuedMapping.getJdbcMapping(); if ( jdbcMapping instanceof BindableType ) { final boolean changed = bindType != null && jdbcMapping != bindType; - bindType = (BindableType) jdbcMapping; - return changed; + if ( changed && previousType instanceof BasicValuedMapping previousBasicValuedMapping + && !areTypesCompatible( basicValuedMapping, previousBasicValuedMapping ) ) { + // An SQM parameter is used in multiple type-incompatible contexts, + // so let's play safe and not force a type onto the binding, + // but let SQM type inference dictate the type instead + this.type = NullType.INSTANCE; + this.bindType = (BindableType) NullType.INSTANCE; + return true; + } + else { + this.bindType = (BindableType) jdbcMapping; + return changed; + } } } } return false; } - private void validate(Object value) { - QueryParameterBindingValidator.INSTANCE.validate( getBindType(), value, getCriteriaBuilder() ); + private boolean areTypesCompatible(BasicValuedMapping mapping1, BasicValuedMapping mapping2) { + final JdbcMapping jdbcMapping1 = mapping1.getSingleJdbcMapping(); + final JdbcMapping jdbcMapping2 = mapping2.getSingleJdbcMapping(); + // We can assume the java types are compatible, since this is relevant for cases when using the same parameter + // in multiple contexts e.g. assignment or comparison. + return jdbcMapping1.getJdbcType() == jdbcMapping2.getJdbcType() + && jdbcMapping1.getValueConverter() == jdbcMapping2.getValueConverter(); } - private void validate(Object value, BindableType clarifiedType) { - QueryParameterBindingValidator.INSTANCE.validate( clarifiedType, value, getCriteriaBuilder() ); + private void clarifyType(Object valueOrValues, BindableType clarifiedType) { + if ( clarifiedType != null ) { + checkClarifiedType( clarifiedType, valueOrValues ); + @SuppressWarnings("unchecked") // safe + final var newType = (BindableType) clarifiedType; + bindType = newType; + } } - private void validate(Object value, TemporalType clarifiedTemporalType) { - QueryParameterBindingValidator.INSTANCE.validate( getBindType(), value, clarifiedTemporalType, getCriteriaBuilder() ); + private void checkClarifiedType( + @NonNull BindableType clarifiedType, + Object valueOrValues) { + final var parameterType = queryParameter.getParameterType(); + if ( parameterType != null ) { + final var clarifiedJavaType = clarifiedType.getJavaType(); + if ( !parameterType.isAssignableFrom( clarifiedJavaType ) ) { + throw new QueryArgumentException( + "Given type is incompatible with parameter type", + parameterType, clarifiedJavaType, valueOrValues + ); + } + } + else { + assert queryParameter.getHibernateType() == null; + } } - @Override - public TypeConfiguration getTypeConfiguration() { - return sessionFactory.getTypeConfiguration(); + private T cast(Object value) { + if ( value == null ) { + return null; + } + else { + final var bindableType = + getCriteriaBuilder() + .resolveExpressible( bindType ); + if ( bindableType != null ) { + return QueryArguments.cast( value, + bindableType.getExpressibleJavaType() ); + } + else if ( bindType != null ) { + return bindType.getJavaType().cast( value ); + } + else { + // no typing information, but in this + // case we can view this as T = Object + // noinspection unchecked + return (T) value; + } + } } - private Object coerceIfNotJpa(T value) { - return sessionFactory.getSessionFactoryOptions().getJpaCompliance().isLoadByIdComplianceEnabled() - ? value - : coerce( value ); + private void validate(Object value) { + QueryParameterBindingValidator.validate( queryParameter, bindType, value, sessionFactory ); } - private Object coerce(T value) { + private Object coerce(Object value) { try { - if ( canValueBeCoerced( bindType ) ) { + if ( bindType != null ) { return coerce( value, bindType ); } - else if ( canValueBeCoerced( queryParameter.getHibernateType() ) ) { - return coerce( value, queryParameter.getHibernateType() ); - } +// else if ( queryParameter.getHibernateType() != null ) { +// return coerce( value, queryParameter.getHibernateType() ); +// } else { return value; } } catch (HibernateException ex) { - throw new IllegalArgumentException( - String.format( - "Parameter value [%s] did not match expected type [%s]", - value, - bindType - ), - ex - ); + throw new QueryArgumentException( "Argument to query parameter has an incompatible type: " + ex.getMessage(), + queryParameter.getParameterType(), value ); } } - private Object coerce(T value, BindableType parameterType) { - if ( value == null ) { - return null; - } - else { - return getCriteriaBuilder().resolveExpressible( parameterType ) - .getExpressibleJavaType().coerce( value, this ); - } + private Object coerce(Object value, BindableType parameterType) { + return value == null ? null + : getCriteriaBuilder().resolveExpressible( parameterType ) + .getExpressibleJavaType().coerce( value ); } - private static boolean canValueBeCoerced(BindableType bindType) { - return bindType != null; + private static @Nullable T firstNonNull(Collection values) { + final var iterator = values.iterator(); + T value = null; + while ( value == null && iterator.hasNext() ) { + value = iterator.next(); + } + return value; } - private static boolean canBindValueBeSet(Object value, BindableType bindType) { - return value != null && bindType == null; - } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/internal/QueryParameterBindingValidator.java b/hibernate-core/src/main/java/org/hibernate/query/internal/QueryParameterBindingValidator.java new file mode 100644 index 000000000000..2ab637b5a014 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/internal/QueryParameterBindingValidator.java @@ -0,0 +1,113 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.query.internal; + +import java.util.Collection; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.query.QueryArgumentException; +import org.hibernate.query.QueryParameter; +import org.hibernate.type.BindableType; + +import static org.hibernate.query.internal.QueryArguments.areInstances; +import static org.hibernate.query.internal.QueryArguments.isInstance; + +/** + * @author Andrea Boriero + */ +class QueryParameterBindingValidator { + + private QueryParameterBindingValidator() { + } + + public static void validate( + QueryParameter parameter, + BindableType parameterType, + Object argument, + SessionFactoryImplementor factory) { + if ( argument != null && parameterType != null ) { + final var parameterJavaType = getParameterJavaType( parameterType, factory ); + if ( parameterJavaType != null ) { + final var criteriaBuilder = factory.getQueryEngine().getCriteriaBuilder(); + if ( argument instanceof Collection collection + && !Collection.class.isAssignableFrom( parameterJavaType ) ) { + // We have a collection passed in where we were expecting a non-collection. + // NOTE: This can happen in Hibernate's notion of "parameter list" binding. + // NOTE2: The case of a collection value and an expected collection + // (if that can even happen) will fall through to the main check. + if ( !areInstances( parameterType, collection, criteriaBuilder ) ) { + throw queryArgumentException( + "Collection-valued argument to parameter %s has an incompatible type", + parameterJavaType, collection, parameter ); + } + } + else if ( !argument.getClass().isArray() ) { + // assume single-valued argument + if ( !isInstance( parameterType, argument, criteriaBuilder ) ) { + throw queryArgumentException( + "Argument to parameter %s has an incompatible type", + parameterJavaType, argument, parameter ); + } + } + else { + validateArrayValuedParameterBinding( parameterJavaType, argument, parameter ); + } + } + // else nothing we can check + } + } + + private static Class getParameterJavaType( + BindableType parameterType, SessionFactoryImplementor factory) { + final var javaType = parameterType.getJavaType(); + return javaType != null + ? javaType + : factory.getQueryEngine().getCriteriaBuilder() + .resolveExpressible( parameterType ) + .getJavaType(); + } + + private static @NonNull QueryArgumentException queryArgumentException( + String messagePattern, Class parameterJavaType, Object value, QueryParameter parameter) { + final String message = String.format( messagePattern, + parameter.isNamed() ? "named '" + parameter.getName() + "'" + : "at position " + parameter.getPosition() ); + return new QueryArgumentException( message, parameterJavaType, value ); + } + + private static void validateArrayValuedParameterBinding( + Class parameterType, Object value, QueryParameter parameter) { + if ( !parameterType.isArray() ) { + throw queryArgumentException( "Unexpected array-valued argument to parameter %s", + parameterType, value, parameter ); + } + + final var componentType = value.getClass().getComponentType(); + final var parameterComponentType = parameterType.getComponentType(); + if ( componentType.isPrimitive() ) { + // We have a primitive array. + // We validate that the actual array has the component type (type of elements) + // that we expect based on the component type of the parameter specification. + if ( !parameterComponentType.isAssignableFrom( componentType ) ) { + throw queryArgumentException( + "Primitive array-valued argument to parameter %s has an incompatible component type", + parameterType, value, parameter ); + } + } + else { + // We have an object array. + // Here we loop over the array and physically check each element against the + // type we expect based on the component type of the parameter specification. + for ( Object element : (Object[]) value ) { + if ( element != null && !parameterComponentType.isInstance( element ) ) { + throw queryArgumentException( + "Array-valued argument to parameter %s has an element with an incompatible type", + parameterComponentType, element, parameter ); + } + } + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/internal/QueryParameterBindingsImpl.java b/hibernate-core/src/main/java/org/hibernate/query/internal/QueryParameterBindingsImpl.java index 281a0b5de19e..033feee2ddf3 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/internal/QueryParameterBindingsImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/internal/QueryParameterBindingsImpl.java @@ -13,6 +13,7 @@ import org.hibernate.Incubating; import org.hibernate.QueryParameterException; +import org.hibernate.bytecode.enhance.spi.interceptor.EnhancementAsProxyLazinessInterceptor; import org.hibernate.cache.MutableCacheKeyBuilder; import org.hibernate.cache.spi.QueryKey; import org.hibernate.engine.spi.SessionFactoryImplementor; @@ -29,7 +30,9 @@ import org.hibernate.type.spi.TypeConfiguration; import static org.hibernate.engine.internal.CacheHelper.addBasicValueToCacheKey; +import static org.hibernate.engine.internal.ManagedTypeHelper.asPersistentAttributeInterceptable; import static org.hibernate.engine.internal.ManagedTypeHelper.isHibernateProxy; +import static org.hibernate.engine.internal.ManagedTypeHelper.isPersistentAttributeInterceptable; import static org.hibernate.internal.util.collections.CollectionHelper.linkedMapOfSize; import static org.hibernate.internal.util.collections.CollectionHelper.mapOfSize; @@ -65,10 +68,11 @@ private QueryParameterBindingsImpl( ParameterMetadataImplementor parameterMetadata) { this.parameterMetadata = parameterMetadata; final var queryParameters = parameterMetadata.getRegistrations(); - this.parameterBindingMap = linkedMapOfSize( queryParameters.size() ); - this.parameterBindingMapByNameOrPosition = mapOfSize( queryParameters.size() ); + parameterBindingMap = linkedMapOfSize( queryParameters.size() ); + parameterBindingMapByNameOrPosition = mapOfSize( queryParameters.size() ); for ( var queryParameter : queryParameters ) { - parameterBindingMap.put( queryParameter, createBinding( sessionFactory, parameterMetadata, queryParameter ) ); + parameterBindingMap.put( queryParameter, + createBinding( sessionFactory, parameterMetadata, queryParameter ) ); } for ( var entry : parameterBindingMap.entrySet() ) { final var queryParameter = entry.getKey(); @@ -83,28 +87,30 @@ else if ( queryParameter.isOrdinal() ) { } private static QueryParameterBindingImpl createBinding( - SessionFactoryImplementor factory, ParameterMetadataImplementor parameterMetadata, QueryParameter parameter) { + SessionFactoryImplementor factory, + ParameterMetadataImplementor parameterMetadata, + QueryParameter parameter) { return new QueryParameterBindingImpl<>( parameter, factory, parameterMetadata.getInferredParameterType( parameter ) ); } - private QueryParameterBindingsImpl(QueryParameterBindingsImpl original, SessionFactoryImplementor sessionFactory) { + private QueryParameterBindingsImpl( + QueryParameterBindingsImpl original, + SessionFactoryImplementor sessionFactory) { this.parameterMetadata = original.parameterMetadata; this.parameterBindingMap = linkedMapOfSize( original.parameterBindingMap.size() ); - this.parameterBindingMapByNameOrPosition = mapOfSize( original.parameterBindingMapByNameOrPosition.size() ); - for ( var entry : original.parameterBindingMap.entrySet() ) { - parameterBindingMap.put( entry.getKey(), createBinding( sessionFactory, entry.getValue() ) ); - } - for ( var entry : parameterBindingMap.entrySet() ) { - final var queryParameter = entry.getKey(); - final var parameterBinding = entry.getValue(); + this.parameterBindingMapByNameOrPosition = + mapOfSize( original.parameterBindingMapByNameOrPosition.size() ); + original.parameterBindingMap.forEach( (key, value) -> + parameterBindingMap.put( key, createBinding( sessionFactory, value ) ) ); + parameterBindingMap.forEach( (queryParameter, parameterBinding) -> { if ( queryParameter.isNamed() ) { parameterBindingMapByNameOrPosition.put( queryParameter.getName(), parameterBinding ); } - else if ( queryParameter.getPosition() != null ) { + else if ( queryParameter.isOrdinal() ) { parameterBindingMapByNameOrPosition.put( queryParameter.getPosition(), parameterBinding ); } - } + } ); } private static QueryParameterBindingImpl createBinding( @@ -129,30 +135,32 @@ public

    QueryParameterBinding

    getBinding(QueryParameterImplementor

    para "Cannot create binding for parameter reference [" + parameter + "] - reference is not a parameter of this query" ); } - //noinspection unchecked - return (QueryParameterBinding

    ) binding; + if ( !binding.getQueryParameter().equals( parameter ) ) { + throw new IllegalStateException( "Parameter binding corrupted for: " + parameter.getName() ); + } + @SuppressWarnings("unchecked") // safe because we checked the parameter + final var castBinding = (QueryParameterBinding

    ) binding; + return castBinding; } @Override - public

    QueryParameterBinding

    getBinding(int position) { + public QueryParameterBinding getBinding(int position) { final var binding = parameterBindingMapByNameOrPosition.get( position ); if ( binding == null ) { // Invoke this method to throw the exception parameterMetadata.getQueryParameter( position ); } - //noinspection unchecked - return (QueryParameterBinding

    ) binding; + return binding; } @Override - public

    QueryParameterBinding

    getBinding(String name) { + public QueryParameterBinding getBinding(String name) { final var binding = parameterBindingMapByNameOrPosition.get( name ); if ( binding == null ) { // Invoke this method to throw the exception parameterMetadata.getQueryParameter( name ); } - //noinspection unchecked - return (QueryParameterBinding

    ) binding; + return binding; } @Override @@ -161,10 +169,14 @@ public void validate() { if ( !entry.getValue().isBound() ) { final var queryParameter = entry.getKey(); if ( queryParameter.isNamed() ) { - throw new QueryParameterException( "No argument for named parameter ':" + queryParameter.getName() + "'" ); + throw new QueryParameterException( + "No argument for named parameter ':" + + queryParameter.getName() + "'" ); } else { - throw new QueryParameterException( "No argument for ordinal parameter '?" + queryParameter.getPosition() + "'" ); + throw new QueryParameterException( + "No argument for ordinal parameter '?" + + queryParameter.getPosition() + "'" ); } } } @@ -202,12 +214,20 @@ public boolean hasAnyTransientEntityBindings(SharedSessionContractImplementor se private static boolean isTransientEntityBinding( SharedSessionContractImplementor session, QueryParameterBinding binding, Object value) { return value != null && !isHibernateProxy( value ) + && !isUninitializedEnhancedEntity( value ) && binding.getBindType() instanceof EntityDomainType entityDomainType && session.getFactory().getMappingMetamodel() .getEntityDescriptor( entityDomainType.getHibernateEntityName() ) .isTransient( value, session ) == Boolean.TRUE; } + private static boolean isUninitializedEnhancedEntity(Object value) { + return isPersistentAttributeInterceptable( value ) + && asPersistentAttributeInterceptable( value ).$$_hibernate_getInterceptor() + instanceof EnhancementAsProxyLazinessInterceptor enhancementInterceptor + && !enhancementInterceptor.isInitialized(); + } + @Override public void visitBindings(BiConsumer, ? super QueryParameterBinding> action) { parameterBindingMap.forEach( action ); @@ -228,7 +248,8 @@ public QueryKey.ParameterBindingsMemento generateQueryKeyMemento(SharedSessionCo private void handleQueryParameters(SharedSessionContractImplementor session, MutableCacheKeyImpl mutableCacheKey) { final var typeConfiguration = session.getFactory().getTypeConfiguration(); - // We know that parameters are consumed in processing order, this ensures consistency of generated cache keys + // We know that parameters are consumed in processing order; + // this ensures the consistency of generated cache keys for ( var entry : parameterBindingMap.entrySet() ) { final var queryParameter = entry.getKey(); final var binding = entry.getValue(); @@ -298,12 +319,14 @@ else if ( binding.getBindValue() != null ) { if ( bindType == null ) { if ( queryParameter.isNamed() ) { - throw new QueryParameterException( "Could not determine mapping type for named parameter ':" - + queryParameter.getName() + "'" ); + throw new QueryParameterException( + "Could not determine mapping type for named parameter ':" + + queryParameter.getName() + "'" ); } else { - throw new QueryParameterException( "Could not determine mapping type for ordinal parameter '?" - + queryParameter.getPosition() + "'" ); + throw new QueryParameterException( + "Could not determine mapping type for ordinal parameter '?" + + queryParameter.getPosition() + "'" ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/internal/ResultMementoBasicStandard.java b/hibernate-core/src/main/java/org/hibernate/query/internal/ResultMementoBasicStandard.java index ddba63c2b754..ff6270abf65e 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/internal/ResultMementoBasicStandard.java +++ b/hibernate-core/src/main/java/org/hibernate/query/internal/ResultMementoBasicStandard.java @@ -4,7 +4,7 @@ */ package org.hibernate.query.internal; -import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; import java.util.function.Consumer; import org.hibernate.metamodel.mapping.BasicValuedMapping; @@ -24,7 +24,7 @@ import jakarta.persistence.AttributeConverter; import jakarta.persistence.ColumnResult; -import static org.hibernate.boot.model.convert.internal.ConverterHelper.extractAttributeConverterParameterizedType; +import static org.hibernate.internal.util.GenericsHelper.typeArguments; /** * Implementation of {@link ResultMementoBasic} for scalar (basic) results. @@ -86,15 +86,16 @@ public ResultMementoBasicStandard( typeConfiguration.getJavaTypeRegistry() .resolveDescriptor( converterClass ); - final var parameterizedType = - extractAttributeConverterParameterizedType( converterBean.getBeanClass() ); + final var typeArguments = + typeArguments( AttributeConverter.class, + converterBean.getBeanClass() ); builder = new CompleteResultBuilderBasicValuedConverted( explicitColumnName, converterBean, converterJtd, - determineDomainJavaType( parameterizedType, typeConfiguration.getJavaTypeRegistry() ), - resolveUnderlyingMapping( parameterizedType, typeConfiguration ) + determineDomainJavaType( typeArguments, typeConfiguration.getJavaTypeRegistry() ), + resolveUnderlyingMapping( typeArguments, typeConfiguration ) ); } else { @@ -139,25 +140,21 @@ else if ( UserType.class.isAssignableFrom( registeredJtd.getJavaTypeClass() ) ) } private BasicJavaType determineDomainJavaType( - ParameterizedType parameterizedType, + Type[] typeArguments, JavaTypeRegistry jtdRegistry) { - final var typeParameters = parameterizedType.getActualTypeArguments(); - final var domainTypeType = typeParameters[ 0 ]; - final var domainClass = (Class) domainTypeType; + final var domainClass = (Class) typeArguments[0]; return (BasicJavaType) jtdRegistry.resolveDescriptor( domainClass ); } private BasicValuedMapping resolveUnderlyingMapping( - ParameterizedType parameterizedType, + Type[] typeArguments, TypeConfiguration typeConfiguration) { - final var typeParameters = parameterizedType.getActualTypeArguments(); - return typeConfiguration.standardBasicTypeForJavaType( typeParameters[ 1 ] ); + return typeConfiguration.standardBasicTypeForJavaType( typeArguments[1] ); } public ResultMementoBasicStandard( String explicitColumnName, - BasicType explicitType, - ResultSetMappingResolutionContext context) { + BasicType explicitType) { this.explicitColumnName = explicitColumnName; this.builder = new CompleteResultBuilderBasicValuedStandard( explicitColumnName, diff --git a/hibernate-core/src/main/java/org/hibernate/query/spi/SimpleHqlInterpretationImpl.java b/hibernate-core/src/main/java/org/hibernate/query/internal/SimpleHqlInterpretationImpl.java similarity index 87% rename from hibernate-core/src/main/java/org/hibernate/query/spi/SimpleHqlInterpretationImpl.java rename to hibernate-core/src/main/java/org/hibernate/query/internal/SimpleHqlInterpretationImpl.java index 9aecd24816db..120bceae71f5 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/spi/SimpleHqlInterpretationImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/internal/SimpleHqlInterpretationImpl.java @@ -2,11 +2,12 @@ * SPDX-License-Identifier: Apache-2.0 * Copyright Red Hat Inc. and Hibernate Authors */ -package org.hibernate.query.spi; +package org.hibernate.query.internal; import java.util.concurrent.ConcurrentHashMap; -import org.hibernate.Internal; +import org.hibernate.query.spi.HqlInterpretation; +import org.hibernate.query.spi.ParameterMetadataImplementor; import org.hibernate.query.sqm.internal.DomainParameterXref; import org.hibernate.query.sqm.tree.SqmStatement; import org.hibernate.query.sqm.tree.select.SqmSelectStatement; @@ -17,13 +18,8 @@ /** * Default implementation if {@link HqlInterpretation}. * - * @apiNote This class is now considered internal implementation - * and will move to an internal package in a future version. - * Application programs should never depend directly on this class. - * * @author Steve Ebersole */ -@Internal public class SimpleHqlInterpretationImpl implements HqlInterpretation { private final SqmStatement sqmStatement; private final ParameterMetadataImplementor parameterMetadata; diff --git a/hibernate-core/src/main/java/org/hibernate/query/results/ResultBuilderEntityValued.java b/hibernate-core/src/main/java/org/hibernate/query/results/ResultBuilderEntityValued.java index 2171d133b976..d22eb84dbbe9 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/results/ResultBuilderEntityValued.java +++ b/hibernate-core/src/main/java/org/hibernate/query/results/ResultBuilderEntityValued.java @@ -17,7 +17,7 @@ */ public interface ResultBuilderEntityValued extends ResultBuilder { @Override - EntityResult buildResult( + EntityResult buildResult( JdbcValuesMetadata jdbcResultsMetadata, int resultPosition, DomainResultCreationState domainResultCreationState); diff --git a/hibernate-core/src/main/java/org/hibernate/query/results/ResultSetMapping.java b/hibernate-core/src/main/java/org/hibernate/query/results/ResultSetMapping.java index 3564169f3288..48a07fc01743 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/results/ResultSetMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/query/results/ResultSetMapping.java @@ -67,7 +67,7 @@ public interface ResultSetMapping extends JdbcValuesMappingProducer { /** * Visit the "legacy" fetch builders. - *

    + *

    * Historically these mappings in Hibernate were defined such that results and fetches are * unaware of each other. So while {@link ResultBuilder} encapsulates the fetches (see * {@link ResultBuilder#visitFetchBuilders}), fetches defined in the legacy way are unassociated diff --git a/hibernate-core/src/main/java/org/hibernate/query/results/internal/DomainResultCreationStateImpl.java b/hibernate-core/src/main/java/org/hibernate/query/results/internal/DomainResultCreationStateImpl.java index cf38c55f2281..8ad9b54376a1 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/results/internal/DomainResultCreationStateImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/results/internal/DomainResultCreationStateImpl.java @@ -4,6 +4,8 @@ */ package org.hibernate.query.results.internal; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.Internal; import org.hibernate.LockMode; import org.hibernate.engine.FetchTiming; @@ -12,11 +14,9 @@ import org.hibernate.internal.util.collections.Stack; import org.hibernate.internal.util.collections.StandardStack; import org.hibernate.metamodel.mapping.Association; -import org.hibernate.metamodel.mapping.EntityIdentifierMapping; -import org.hibernate.metamodel.mapping.EntityValuedModelPart; import org.hibernate.metamodel.mapping.ForeignKeyDescriptor; import org.hibernate.metamodel.mapping.ModelPart; -import org.hibernate.metamodel.mapping.internal.CaseStatementDiscriminatorMappingImpl; +import org.hibernate.metamodel.mapping.internal.CaseStatementDiscriminatorMappingImpl.CaseStatementDiscriminatorExpression; import org.hibernate.query.results.FetchBuilder; import org.hibernate.query.results.LegacyFetchBuilder; import org.hibernate.spi.EntityIdentifierNavigablePath; @@ -30,12 +30,10 @@ import org.hibernate.sql.ast.spi.SqlSelection; import org.hibernate.sql.ast.tree.expression.ColumnReference; import org.hibernate.sql.ast.tree.expression.Expression; -import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.results.graph.DomainResultCreationState; import org.hibernate.sql.results.graph.Fetch; import org.hibernate.sql.results.graph.FetchParent; import org.hibernate.sql.results.graph.Fetchable; -import org.hibernate.sql.results.graph.FetchableContainer; import org.hibernate.sql.results.graph.entity.EntityResultGraphNode; import org.hibernate.sql.results.graph.internal.ImmutableFetchList; import org.hibernate.sql.results.jdbc.spi.JdbcValuesMetadata; @@ -47,7 +45,9 @@ import java.util.function.Consumer; import java.util.function.Function; +import static org.hibernate.query.results.internal.Builders.implicitFetchBuilder; import static org.hibernate.query.results.internal.ResultsHelper.attributeName; +import static org.hibernate.query.results.internal.ResultsHelper.jdbcPositionToValuesArrayPosition; import static org.hibernate.sql.results.ResultsLogger.RESULTS_LOGGER; /** @@ -82,7 +82,7 @@ public class DomainResultCreationStateImpl private boolean processingKeyFetches = false; private boolean resolvingCircularFetch; private ForeignKeyDescriptor.Nature currentlyResolvingForeignKeySide; - private boolean isProcedureOrNativeQuery; + private final boolean isProcedureOrNativeQuery; public DomainResultCreationStateImpl( String stateIdentifier, @@ -132,7 +132,7 @@ public void pushExplicitFetchMementoResolver(Function r } public Function getCurrentExplicitFetchMementoResolver() { - return fetchBuilderResolverStack.getCurrent(); + return currentFetchBuilderResolver(); } public Function popExplicitFetchMementoResolver() { @@ -146,7 +146,7 @@ public void withExplicitFetchMementoResolver(Function r runnable.run(); } finally { - final Function popped = popExplicitFetchMementoResolver(); + final var popped = popExplicitFetchMementoResolver(); assert popped == resolver; } } @@ -172,15 +172,17 @@ public SqlAliasBaseManager getSqlAliasBaseManager() { @Override public ModelPart resolveModelPart(NavigablePath navigablePath) { - final TableGroup tableGroup = fromClauseAccess.findTableGroup( navigablePath ); + final var tableGroup = fromClauseAccess.findTableGroup( navigablePath ); if ( tableGroup != null ) { return tableGroup.getModelPart(); } - if ( navigablePath.getParent() != null ) { - final TableGroup parentTableGroup = fromClauseAccess.findTableGroup( navigablePath.getParent() ); + final var parentPath = navigablePath.getParent(); + if ( parentPath != null ) { + final var parentTableGroup = fromClauseAccess.findTableGroup( parentPath ); if ( parentTableGroup != null ) { - return parentTableGroup.getModelPart().findSubPart( navigablePath.getLocalName(), null ); + return parentTableGroup.getModelPart() + .findSubPart( navigablePath.getLocalName(), null ); } } @@ -198,7 +200,7 @@ public DomainResultCreationStateImpl getSqlExpressionResolver() { @Override public void registerLockMode(String identificationVariable, LockMode explicitLockMode) { - if (registeredLockModes == null ) { + if ( registeredLockModes == null ) { registeredLockModes = new HashMap<>(); } registeredLockModes.put( identificationVariable, explicitLockMode ); @@ -250,60 +252,45 @@ public SqlAstProcessingState getParentState() { public Expression resolveSqlExpression( ColumnReferenceKey key, Function creator) { - final ResultSetMappingSqlSelection existing = sqlSelectionMap.get( key ); - if ( existing != null ) { - return existing; - } + final var existing = sqlSelectionMap.get( key ); + return existing != null ? existing : createSqlSelection( key, creator ); + } - final Expression created = creator.apply( this ); + private Expression createSqlSelection( + ColumnReferenceKey key, + Function creator) { + final var created = creator.apply( this ); + final ResultSetMappingSqlSelection sqlSelection; if ( created instanceof ResultSetMappingSqlSelection resultSetMappingSqlSelection ) { - sqlSelectionMap.put( key, resultSetMappingSqlSelection ); - sqlSelectionConsumer.accept( resultSetMappingSqlSelection ); + sqlSelection = resultSetMappingSqlSelection; } else if ( created instanceof ColumnReference columnReference) { - final String selectableName = columnReference.getSelectableName(); - final int valuesArrayPosition; - if ( nestingFetchParent != null ) { - valuesArrayPosition = nestingFetchParent.getReferencedMappingType().getSelectableIndex( selectableName ); - } - else { - final int jdbcPosition = jdbcResultsMetadata.resolveColumnPosition( selectableName ); - valuesArrayPosition = ResultsHelper.jdbcPositionToValuesArrayPosition( jdbcPosition ); - } - - final ResultSetMappingSqlSelection sqlSelection = new ResultSetMappingSqlSelection( - valuesArrayPosition, - columnReference.getJdbcMapping() - ); - - sqlSelectionMap.put( key, sqlSelection ); - sqlSelectionConsumer.accept( sqlSelection ); - - return sqlSelection; + sqlSelection = + new ResultSetMappingSqlSelection( + valuesArrayPosition( columnReference.getSelectableName() ), + columnReference.getJdbcMapping() + ); } - else if ( created instanceof CaseStatementDiscriminatorMappingImpl.CaseStatementDiscriminatorExpression ) { - final int valuesArrayPosition; - if ( nestingFetchParent != null ) { - valuesArrayPosition = nestingFetchParent.getReferencedMappingType().getSelectableIndex( DISCRIMINATOR_ALIAS ); - } - else { - final int jdbcPosition = jdbcResultsMetadata.resolveColumnPosition( DISCRIMINATOR_ALIAS ); - valuesArrayPosition = ResultsHelper.jdbcPositionToValuesArrayPosition( jdbcPosition ); - } - - final ResultSetMappingSqlSelection sqlSelection = new ResultSetMappingSqlSelection( - valuesArrayPosition, + else if ( created instanceof CaseStatementDiscriminatorExpression ) { + sqlSelection = new ResultSetMappingSqlSelection( + valuesArrayPosition( DISCRIMINATOR_ALIAS ), created.getExpressionType().getSingleJdbcMapping() ); - - sqlSelectionMap.put( key, sqlSelection ); - sqlSelectionConsumer.accept( sqlSelection ); - - return sqlSelection; } + else { + return created; + } + + sqlSelectionMap.put( key, sqlSelection ); + sqlSelectionConsumer.accept( sqlSelection ); + return sqlSelection; + } - return created; + private int valuesArrayPosition(String selectableName) { + return nestingFetchParent != null + ? nestingFetchParent.getReferencedMappingType().getSelectableIndex( selectableName ) + : jdbcPositionToValuesArrayPosition( jdbcResultsMetadata.resolveColumnPosition( selectableName ) ); } @Override @@ -314,8 +301,10 @@ public SqlSelection resolveSqlSelection( if ( expression == null ) { throw new IllegalArgumentException( "Expression cannot be null" ); } - assert expression instanceof ResultSetMappingSqlSelection; - return (SqlSelection) expression; + if ( !(expression instanceof ResultSetMappingSqlSelection sqlSelection) ) { + throw new IllegalArgumentException( "Expression must be a ResultSetMappingSqlSelection" ); + } + return sqlSelection; } private static class LegacyFetchResolver { @@ -329,71 +318,78 @@ public LegacyFetchBuilder resolve(String ownerTableAlias, Fetchable fetchedPart) if ( legacyFetchBuilders == null ) { return null; } - - final Map fetchBuilders = legacyFetchBuilders.get( ownerTableAlias ); - if ( fetchBuilders == null ) { - return null; + else { + final var fetchBuilders = legacyFetchBuilders.get( ownerTableAlias ); + return fetchBuilders == null ? null : fetchBuilders.get( fetchedPart ); } - - return fetchBuilders.get( fetchedPart ); } } @Override public R withNestedFetchParent(FetchParent fetchParent, Function action) { - final FetchParent oldNestingFetchParent = this.nestingFetchParent; + final var oldNestingFetchParent = this.nestingFetchParent; this.nestingFetchParent = fetchParent; final R result = action.apply( fetchParent ); this.nestingFetchParent = oldNestingFetchParent; return result; } - @Override - public Fetch visitIdentifierFetch(EntityResultGraphNode fetchParent) { - final EntityValuedModelPart parentModelPart = fetchParent.getEntityValuedModelPart(); - final EntityIdentifierMapping identifierMapping = parentModelPart.getEntityMappingType().getIdentifierMapping(); - final String identifierAttributeName = attributeName( identifierMapping ); + private @NonNull FetchBuilder fetchBuilder( + Fetchable fetchable, + FetchBuilder explicitFetchBuilder, + LegacyFetchBuilder fetchBuilderLegacy, + NavigablePath fetchPath) { + if ( explicitFetchBuilder != null ) { + return explicitFetchBuilder; + } + else if ( fetchBuilderLegacy != null ) { + return fetchBuilderLegacy; + } + else { + return implicitFetchBuilder( fetchPath, fetchable, this ); + } + } - final FetchBuilder explicitFetchBuilder = fetchBuilderResolverStack.getCurrent().apply( identifierMapping ); - final LegacyFetchBuilder fetchBuilderLegacy; + private @Nullable LegacyFetchBuilder legacyFetchBuilder( + Fetchable fetchable, FetchParent fetchParent, + FetchBuilder explicitFetchBuilder) { if ( explicitFetchBuilder == null ) { - fetchBuilderLegacy = legacyFetchResolver.resolve( + return legacyFetchResolver.resolve( fromClauseAccess.findTableGroup( fetchParent.getNavigablePath() ) .getPrimaryTableReference() .getIdentificationVariable(), - identifierMapping + fetchable ); } else { - fetchBuilderLegacy = null; + return null; } + } + + @Override + public Fetch visitIdentifierFetch(EntityResultGraphNode fetchParent) { + final var identifierMapping = + fetchParent.getEntityValuedModelPart() + .getEntityMappingType().getIdentifierMapping(); - final EntityIdentifierNavigablePath fetchPath = new EntityIdentifierNavigablePath( - fetchParent.getNavigablePath(), - identifierAttributeName - ); + final var explicitFetchBuilder = + currentFetchBuilderResolver().apply( identifierMapping ); + + final var fetchBuilderLegacy = + legacyFetchBuilder( identifierMapping, fetchParent, explicitFetchBuilder ); + + final var fetchPath = + new EntityIdentifierNavigablePath( + fetchParent.getNavigablePath(), + attributeName( identifierMapping ) + ); final boolean processingKeyFetches = this.processingKeyFetches; this.processingKeyFetches = true; try { - final FetchBuilder fetchBuilder; - if ( explicitFetchBuilder != null ) { - fetchBuilder = explicitFetchBuilder; - } - else if ( fetchBuilderLegacy != null ) { - fetchBuilder = fetchBuilderLegacy; - } - else { - fetchBuilder = Builders.implicitFetchBuilder( fetchPath, identifierMapping, this ); - } - - return fetchBuilder.buildFetch( - fetchParent, - fetchPath, - jdbcResultsMetadata, - this - ); + return fetchBuilder( identifierMapping, explicitFetchBuilder, fetchBuilderLegacy, fetchPath ) + .buildFetch( fetchParent, fetchPath, jdbcResultsMetadata, this ); } finally { this.processingKeyFetches = processingKeyFetches; @@ -402,9 +398,9 @@ else if ( fetchBuilderLegacy != null ) { @Override public ImmutableFetchList visitFetches(FetchParent fetchParent) { - final FetchableContainer fetchableContainer = fetchParent.getReferencedMappingContainer(); - final ImmutableFetchList.Builder fetches = new ImmutableFetchList.Builder( fetchableContainer ); - final Consumer fetchableConsumer = createFetchableConsumer( fetchParent, fetches ); + final var fetchableContainer = fetchParent.getReferencedMappingContainer(); + final var fetches = new ImmutableFetchList.Builder( fetchableContainer ); + final var fetchableConsumer = createFetchableConsumer( fetchParent, fetches ); fetchableContainer.visitKeyFetchables( fetchableConsumer, null ); fetchableContainer.visitFetchables( fetchableConsumer, null ); return fetches.build(); @@ -415,60 +411,36 @@ private Consumer createFetchableConsumer(FetchParent fetchParent, Imm if ( !fetchable.isSelectable() ) { return; } - FetchBuilder explicitFetchBuilder = fetchBuilderResolverStack.getCurrent().apply( fetchable ); - LegacyFetchBuilder fetchBuilderLegacy; - if ( explicitFetchBuilder == null ) { - fetchBuilderLegacy = legacyFetchResolver.resolve( - fromClauseAccess.findTableGroup( fetchParent.getNavigablePath() ) - .getPrimaryTableReference() - .getIdentificationVariable(), - fetchable - ); - } - else { - fetchBuilderLegacy = null; - } + FetchBuilder explicitFetchBuilder = currentFetchBuilderResolver().apply( fetchable ); + LegacyFetchBuilder fetchBuilderLegacy = + legacyFetchBuilder( fetchable, fetchParent, explicitFetchBuilder ); if ( fetchable instanceof Association association && fetchable.getMappedFetchOptions().getTiming() == FetchTiming.DELAYED ) { - final ForeignKeyDescriptor foreignKeyDescriptor = association.getForeignKeyDescriptor(); // If there are no fetch builders for this association, we only want to fetch the FK if ( explicitFetchBuilder == null && fetchBuilderLegacy == null ) { - Fetchable modelPart = (Fetchable) - foreignKeyDescriptor.getSide( association.getSideNature().inverse() ).getModelPart(); - explicitFetchBuilder = fetchBuilderResolverStack.getCurrent().apply( modelPart ); + final var modelPart = (Fetchable) + association.getForeignKeyDescriptor() + .getSide( association.getSideNature().inverse() ) + .getModelPart(); + explicitFetchBuilder = currentFetchBuilderResolver().apply( modelPart ); if ( explicitFetchBuilder == null ) { - fetchBuilderLegacy = legacyFetchResolver.resolve( - fromClauseAccess.findTableGroup( fetchParent.getNavigablePath() ) - .getPrimaryTableReference() - .getIdentificationVariable(), - fetchable - ); + fetchBuilderLegacy = + legacyFetchBuilder( fetchable, fetchParent, null ); } } } - final NavigablePath fetchPath = fetchParent.resolveNavigablePath( fetchable ); - final FetchBuilder fetchBuilder; - if ( explicitFetchBuilder != null ) { - fetchBuilder = explicitFetchBuilder; - } - else { - if ( fetchBuilderLegacy == null ) { - fetchBuilder = Builders.implicitFetchBuilder( fetchPath, fetchable, this ); - } - else { - fetchBuilder = fetchBuilderLegacy; - } - } - final Fetch fetch = fetchBuilder.buildFetch( - fetchParent, - fetchPath, - jdbcResultsMetadata, - this - ); + final var fetchPath = fetchParent.resolveNavigablePath( fetchable ); + final var fetch = + fetchBuilder( fetchable, explicitFetchBuilder, fetchBuilderLegacy, fetchPath ) + .buildFetch( fetchParent, fetchPath, jdbcResultsMetadata, this ); fetches.add( fetch ); }; } + private Function currentFetchBuilderResolver() { + return fetchBuilderResolverStack.getCurrent(); + } + @Override public boolean isResolvingCircularFetch() { return resolvingCircularFetch; diff --git a/hibernate-core/src/main/java/org/hibernate/query/results/internal/FromClauseAccessImpl.java b/hibernate-core/src/main/java/org/hibernate/query/results/internal/FromClauseAccessImpl.java index c43e27d4ab3f..1f7ca19f94ea 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/results/internal/FromClauseAccessImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/results/internal/FromClauseAccessImpl.java @@ -23,28 +23,13 @@ public class FromClauseAccessImpl implements FromClauseAccess { private Map tableGroupBySqlAlias; private Map tableGroupByPath; - public FromClauseAccessImpl() { - } - - public TableGroup getByAlias(String alias) { - final TableGroup byAlias = findByAlias( alias ); - if ( byAlias == null ) { - throw new IllegalArgumentException( "Could not resolve TableGroup by alias [" + alias + "]" ); - } - return byAlias; - } - public TableGroup findByAlias(String alias) { - if ( tableGroupBySqlAlias != null ) { - return tableGroupBySqlAlias.get( alias ); - } - - return null; + return tableGroupBySqlAlias == null ? null : tableGroupBySqlAlias.get( alias ); } @Override public @Nullable TableGroup findTableGroupByIdentificationVariable(String identificationVariable) { - for ( TableGroup tableGroup : tableGroupByPath.values() ) { + for ( var tableGroup : tableGroupByPath.values() ) { if ( tableGroup.findTableReference( identificationVariable ) != null ) { return tableGroup; } @@ -59,11 +44,8 @@ public TableGroup findTableGroupOnCurrentFromClause(NavigablePath navigablePath) @Override public TableGroup findTableGroup(NavigablePath navigablePath) { - if ( tableGroupByPath != null ) { - return tableGroupByPath.get( navigablePath ); - } + return tableGroupByPath == null ? null : tableGroupByPath.get( navigablePath ); - return null; } @Override @@ -73,11 +55,12 @@ public void registerTableGroup(NavigablePath navigablePath, TableGroup tableGrou } tableGroupByPath.put( navigablePath, tableGroup ); - if ( tableGroup.getGroupAlias() != null ) { + final String groupAlias = tableGroup.getGroupAlias(); + if ( groupAlias != null ) { if ( tableGroupBySqlAlias == null ) { tableGroupBySqlAlias = new HashMap<>(); } - tableGroupBySqlAlias.put( tableGroup.getGroupAlias(), tableGroup ); + tableGroupBySqlAlias.put( groupAlias, tableGroup ); } } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/results/internal/JdbcValuesMappingImpl.java b/hibernate-core/src/main/java/org/hibernate/query/results/internal/JdbcValuesMappingImpl.java index b20dcf11786a..60505b89dbd1 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/results/internal/JdbcValuesMappingImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/results/internal/JdbcValuesMappingImpl.java @@ -40,6 +40,8 @@ public int getRowSize() { @Override public LockMode determineDefaultLockMode(String alias, LockMode defaultLockMode) { - return registeredLockModes == null ? defaultLockMode : registeredLockModes.getOrDefault( alias, defaultLockMode ); + return registeredLockModes == null + ? defaultLockMode + : registeredLockModes.getOrDefault( alias, defaultLockMode ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/results/internal/ResultSetMappingImpl.java b/hibernate-core/src/main/java/org/hibernate/query/results/internal/ResultSetMappingImpl.java index c093d9d11ac7..696d56fa1bad 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/results/internal/ResultSetMappingImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/results/internal/ResultSetMappingImpl.java @@ -6,10 +6,8 @@ import org.hibernate.engine.spi.LoadQueryInfluencers; import org.hibernate.engine.spi.SessionFactoryImplementor; -import org.hibernate.internal.util.StringHelper; import org.hibernate.loader.NonUniqueDiscoveredSqlAliasException; import org.hibernate.metamodel.mapping.BasicValuedMapping; -import org.hibernate.persister.entity.EntityPersister; import org.hibernate.query.named.NamedResultSetMappingMemento; import org.hibernate.query.results.LegacyFetchBuilder; import org.hibernate.query.results.ResultBuilder; @@ -24,7 +22,6 @@ import org.hibernate.type.BasicType; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -34,6 +31,11 @@ import java.util.function.BiConsumer; import java.util.function.Consumer; +import static java.util.Collections.addAll; +import static java.util.Collections.emptyList; +import static java.util.Collections.unmodifiableList; +import static org.hibernate.internal.util.StringHelper.isEmpty; + /** * ResultSetMapping implementation used while building * {@linkplain org.hibernate.query.results.ResultSetMapping} references. @@ -63,7 +65,7 @@ private ResultSetMappingImpl(ResultSetMappingImpl original) { } else { final List resultBuilders = new ArrayList<>( original.resultBuilders.size() ); - for ( ResultBuilder resultBuilder : original.resultBuilders ) { + for ( var resultBuilder : original.resultBuilders ) { resultBuilders.add( resultBuilder.cacheKeyInstance() ); } this.resultBuilders = resultBuilders; @@ -72,11 +74,14 @@ private ResultSetMappingImpl(ResultSetMappingImpl original) { this.legacyFetchBuilders = null; } else { - final Map> builders = new HashMap<>( original.legacyFetchBuilders.size() ); - for ( Map.Entry> entry : original.legacyFetchBuilders.entrySet() ) { - final Map newValue = new HashMap<>( entry.getValue().size() ); - for ( Map.Entry builderEntry : entry.getValue().entrySet() ) { - newValue.put( builderEntry.getKey(), builderEntry.getValue().cacheKeyInstance() ); + final Map> builders = + new HashMap<>( original.legacyFetchBuilders.size() ); + for ( var entry : original.legacyFetchBuilders.entrySet() ) { + final Map newValue = + new HashMap<>( entry.getValue().size() ); + for ( var builderEntry : entry.getValue().entrySet() ) { + newValue.put( builderEntry.getKey(), + builderEntry.getValue().cacheKeyInstance() ); } builders.put( entry.getKey(), newValue ); } @@ -100,32 +105,27 @@ public int getNumberOfResultBuilders() { } public List getResultBuilders() { - if ( resultBuilders == null ) { - return Collections.emptyList(); - } - return Collections.unmodifiableList( resultBuilders ); + return resultBuilders == null + ? emptyList() + : unmodifiableList( resultBuilders ); } @Override public void visitResultBuilders(BiConsumer resultBuilderConsumer) { - if ( resultBuilders == null ) { - return; - } - - for ( int i = 0; i < resultBuilders.size(); i++ ) { - resultBuilderConsumer.accept( i, resultBuilders.get( i ) ); + if ( resultBuilders != null ) { + for ( int i = 0; i < resultBuilders.size(); i++ ) { + resultBuilderConsumer.accept( i, resultBuilders.get( i ) ); + } } } @Override public void visitLegacyFetchBuilders(Consumer resultBuilderConsumer) { - if ( legacyFetchBuilders == null ) { - return; - } - - for ( Map.Entry> entry : legacyFetchBuilders.entrySet() ) { - for ( LegacyFetchBuilder fetchBuilder : entry.getValue().values() ) { - resultBuilderConsumer.accept( fetchBuilder ); + if ( legacyFetchBuilders != null ) { + for ( var entry : legacyFetchBuilders.entrySet() ) { + for ( LegacyFetchBuilder fetchBuilder : entry.getValue().values() ) { + resultBuilderConsumer.accept( fetchBuilder ); + } } } } @@ -164,18 +164,14 @@ public void addLegacyFetchBuilder(LegacyFetchBuilder fetchBuilder) { @Override public void addAffectedTableNames(Set affectedTableNames, SessionFactoryImplementor sessionFactory) { - if ( StringHelper.isEmpty( mappingIdentifier ) ) { - return; - } - - final EntityPersister entityDescriptor = - sessionFactory.getMappingMetamodel() - .findEntityDescriptor( mappingIdentifier ); - if ( entityDescriptor == null ) { - return; + if ( !isEmpty( mappingIdentifier ) ) { + final var entityDescriptor = + sessionFactory.getMappingMetamodel() + .findEntityDescriptor( mappingIdentifier ); + if ( entityDescriptor != null ) { + addAll( affectedTableNames, (String[]) entityDescriptor.getQuerySpaces() ); + } } - - Collections.addAll( affectedTableNames, (String[]) entityDescriptor.getQuerySpaces() ); } @Override @@ -184,15 +180,12 @@ public JdbcValuesMapping resolve( LoadQueryInfluencers loadQueryInfluencers, SessionFactoryImplementor sessionFactory) { - final int numberOfResults; final int rowSize = jdbcResultsMetadata.getColumnCount(); - - numberOfResults = resultBuilders == null ? rowSize : resultBuilders.size(); + final int numberOfResults = resultBuilders == null ? rowSize : resultBuilders.size(); final List sqlSelections = new ArrayList<>( rowSize ); - final List> domainResults = new ArrayList<>( numberOfResults ); - final DomainResultCreationStateImpl creationState = new DomainResultCreationStateImpl( + final var creationState = new DomainResultCreationStateImpl( mappingIdentifier, jdbcResultsMetadata, legacyFetchBuilders, @@ -202,79 +195,14 @@ public JdbcValuesMapping resolve( sessionFactory ); - for ( int i = 0; i < numberOfResults; i++ ) { - final ResultBuilder resultBuilder = resultBuilders != null - ? resultBuilders.get( i ) - : null; - - final DomainResult domainResult; - if ( resultBuilder == null ) { - domainResult = makeImplicitDomainResult( - i, - sqlSelections::add, - jdbcResultsMetadata, - sessionFactory - ); - } - else { - domainResult = resultBuilder.buildResult( - jdbcResultsMetadata, - domainResults.size(), - creationState - ); - } - - if ( domainResult.containsAnyNonScalarResults() ) { - creationState.disallowPositionalSelections(); - } + final var domainResults = + collectDomainResults( jdbcResultsMetadata, sessionFactory, + numberOfResults, sqlSelections, creationState ); - domainResults.add( domainResult ); - } // We only need this check when we actually have result builders // As people should be able to just run native queries and work with tuples if ( resultBuilders != null ) { - final Set knownDuplicateAliases = new TreeSet<>( String.CASE_INSENSITIVE_ORDER ); - if ( resultBuilders.size() == 1 && domainResults.size() == 1 && domainResults.get( 0 ) instanceof EntityResult entityResult ) { - // Special case for result set mappings that just fetch a single polymorphic entity - final EntityPersister persister = entityResult.getReferencedMappingContainer().getEntityPersister(); - final boolean polymorphic = persister.isPolymorphic(); - // We only need to check for duplicate aliases if we have join fetches, - // otherwise we assume that even if there are duplicate aliases, the values are equivalent. - // If we don't do that, there is no way to fetch joined inheritance entities - if ( polymorphic && ( legacyFetchBuilders == null || legacyFetchBuilders.isEmpty() ) - && !entityResult.hasJoinFetches() ) { - final Set aliases = new TreeSet<>( String.CASE_INSENSITIVE_ORDER ); - for ( String[] columns : persister.getConstraintOrderedTableKeyColumnClosure() ) { - addColumns( aliases, knownDuplicateAliases, columns ); - } - addColumn( aliases, knownDuplicateAliases, persister.getDiscriminatorColumnName() ); - addColumn( aliases, knownDuplicateAliases, persister.getVersionColumnName() ); - for (int i = 0; i < persister.countSubclassProperties(); i++ ) { - addColumns( - aliases, - knownDuplicateAliases, - persister.getSubclassPropertyColumnNames( i ) - ); - } - } - } - final String[] aliases = new String[rowSize]; - final Map aliasHasDuplicates = new HashMap<>( rowSize ); - for ( int i = 0; i < rowSize; i++ ) { - aliasHasDuplicates.compute( - aliases[i] = jdbcResultsMetadata.resolveColumnName( i + 1 ), - (k, v) -> v == null ? Boolean.FALSE : Boolean.TRUE - ); - } - // Only check for duplicates for the selections that we actually use - for ( SqlSelection sqlSelection : sqlSelections ) { - final String alias = aliases[sqlSelection.getValuesArrayPosition()]; - if ( !knownDuplicateAliases.contains( alias ) && aliasHasDuplicates.get( alias ) == Boolean.TRUE ) { - throw new NonUniqueDiscoveredSqlAliasException( - "Encountered a duplicated sql alias [" + alias + "] during auto-discovery of a native-sql query" - ); - } - } + checkDuplicateAliases( jdbcResultsMetadata, domainResults, rowSize, sqlSelections ); } return new JdbcValuesMappingImpl( @@ -285,6 +213,108 @@ public JdbcValuesMapping resolve( ); } + private void checkDuplicateAliases( + JdbcValuesMetadata jdbcResultsMetadata, + List> domainResults, + int rowSize, + List sqlSelections) { + final Set knownDuplicateAliases = new TreeSet<>( String.CASE_INSENSITIVE_ORDER ); + if ( resultBuilders.size() == 1 && domainResults.size() == 1 + && domainResults.get( 0 ) instanceof EntityResult entityResult ) { + // Special case for result set mappings that just fetch a single polymorphic entity + final var persister = entityResult.getReferencedMappingContainer().getEntityPersister(); + final boolean polymorphic = persister.isPolymorphic(); + // We only need to check for duplicate aliases if we have join fetches, + // otherwise we assume that even if there are duplicate aliases, the values are equivalent. + // If we don't do that, there is no way to fetch joined inheritance entities + if ( polymorphic + && ( legacyFetchBuilders == null || legacyFetchBuilders.isEmpty() ) + && !entityResult.hasJoinFetches() ) { + final Set aliases = new TreeSet<>( String.CASE_INSENSITIVE_ORDER ); + for ( var columns : persister.getConstraintOrderedTableKeyColumnClosure() ) { + addColumns( aliases, knownDuplicateAliases, columns ); + } + addColumn( aliases, knownDuplicateAliases, persister.getDiscriminatorColumnName() ); + addColumn( aliases, knownDuplicateAliases, persister.getVersionColumnName() ); + for (int i = 0; i < persister.countSubclassProperties(); i++ ) { + addColumns( aliases, knownDuplicateAliases, + persister.getSubclassPropertyColumnNames( i ) ); + } + } + } + final var aliases = new String[rowSize]; + final Map aliasHasDuplicates = new HashMap<>( rowSize ); + for ( int i = 0; i < rowSize; i++ ) { + aliasHasDuplicates.compute( + aliases[i] = jdbcResultsMetadata.resolveColumnName( i + 1 ), + (k, v) -> v == null ? Boolean.FALSE : Boolean.TRUE + ); + } + // Only check for duplicates for the selections that we actually use + for ( var sqlSelection : sqlSelections ) { + final String alias = aliases[sqlSelection.getValuesArrayPosition()]; + if ( !knownDuplicateAliases.contains( alias ) + && aliasHasDuplicates.get( alias ) == Boolean.TRUE ) { + throw new NonUniqueDiscoveredSqlAliasException( + "Encountered a duplicated sql alias [" + alias + "] during auto-discovery of a native-sql query" + ); + } + } + } + + private List> collectDomainResults( + JdbcValuesMetadata jdbcResultsMetadata, + SessionFactoryImplementor sessionFactory, + int numberOfResults, + List sqlSelections, + DomainResultCreationStateImpl creationState) { + final List> domainResults = new ArrayList<>( numberOfResults ); + for ( int i = 0; i < numberOfResults; i++ ) { + final var domainResult = + buildDomainResult( + jdbcResultsMetadata, + sessionFactory, + resultBuilders == null + ? null + : resultBuilders.get( i ), + i, + sqlSelections, + domainResults, + creationState + ); + if ( domainResult.containsAnyNonScalarResults() ) { + creationState.disallowPositionalSelections(); + } + domainResults.add( domainResult ); + } + return domainResults; + } + + private DomainResult buildDomainResult( + JdbcValuesMetadata jdbcResultsMetadata, + SessionFactoryImplementor sessionFactory, + ResultBuilder resultBuilder, + int i, + List sqlSelections, + List> domainResults, + DomainResultCreationStateImpl creationState) { + if ( resultBuilder == null ) { + return makeImplicitDomainResult( + i, + sqlSelections::add, + jdbcResultsMetadata, + sessionFactory + ); + } + else { + return resultBuilder.buildResult( + jdbcResultsMetadata, + domainResults.size(), + creationState + ); + } + } + private static void addColumns(Set aliases, Set knownDuplicateAliases, String[] columns) { for ( int i = 0; i < columns.length; i++ ) { addColumn( aliases, knownDuplicateAliases, columns[i] ); @@ -309,7 +339,9 @@ private DomainResult makeImplicitDomainResult( final String name = jdbcResultsMetadata.resolveColumnName( jdbcPosition ); - final ResultSetMappingSqlSelection sqlSelection = new ResultSetMappingSqlSelection( valuesArrayPosition, (BasicValuedMapping) jdbcMapping ); + final var sqlSelection = + new ResultSetMappingSqlSelection( valuesArrayPosition, + (BasicValuedMapping) jdbcMapping ); sqlSelectionConsumer.accept( sqlSelection ); return new BasicResult<>( @@ -346,23 +378,23 @@ public int hashCode() { } @Override - public boolean equals(Object o) { - if ( this == o ) { + public boolean equals(Object object) { + if ( this == object ) { return true; } - if ( o == null || getClass() != o.getClass() ) { + else if ( !( object instanceof ResultSetMappingImpl that ) ) { return false; } - - final ResultSetMappingImpl that = (ResultSetMappingImpl) o; - if ( isDynamic ) { + else if ( isDynamic ) { return that.isDynamic - && Objects.equals( mappingIdentifier, that.mappingIdentifier ) - && Objects.equals( resultBuilders, that.resultBuilders ) - && Objects.equals( legacyFetchBuilders, that.legacyFetchBuilders ); + && Objects.equals( this.mappingIdentifier, that.mappingIdentifier ) + && Objects.equals( this.resultBuilders, that.resultBuilders ) + && Objects.equals( this.legacyFetchBuilders, that.legacyFetchBuilders ); } else { - return !that.isDynamic && mappingIdentifier != null && mappingIdentifier.equals( that.mappingIdentifier ); + return !that.isDynamic + && mappingIdentifier != null + && mappingIdentifier.equals( that.mappingIdentifier ); } } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/results/internal/ResultSetMappingSqlSelection.java b/hibernate-core/src/main/java/org/hibernate/query/results/internal/ResultSetMappingSqlSelection.java index 544c885e89ee..6a08e5349a92 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/results/internal/ResultSetMappingSqlSelection.java +++ b/hibernate-core/src/main/java/org/hibernate/query/results/internal/ResultSetMappingSqlSelection.java @@ -7,7 +7,7 @@ import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.metamodel.mapping.BasicValuedMapping; import org.hibernate.metamodel.mapping.JdbcMapping; -import org.hibernate.metamodel.mapping.MappingModelExpressible; +import org.hibernate.metamodel.mapping.JdbcMappingContainer; import org.hibernate.sql.ast.SqlAstWalker; import org.hibernate.sql.ast.spi.SqlExpressionAccess; import org.hibernate.sql.ast.spi.SqlSelection; @@ -24,18 +24,16 @@ */ public class ResultSetMappingSqlSelection implements SqlSelection, Expression, SqlExpressionAccess { private final int valuesArrayPosition; - private final BasicValuedMapping valueMapping; + private final JdbcMapping valueMapping; private final ValueExtractor valueExtractor; public ResultSetMappingSqlSelection(int valuesArrayPosition, BasicValuedMapping valueMapping) { - this.valuesArrayPosition = valuesArrayPosition; - this.valueMapping = valueMapping; - this.valueExtractor = valueMapping.getJdbcMapping().getJdbcValueExtractor(); + this ( valuesArrayPosition, valueMapping.getJdbcMapping() ); } public ResultSetMappingSqlSelection(int valuesArrayPosition, JdbcMapping jdbcMapping) { this.valuesArrayPosition = valuesArrayPosition; - this.valueMapping = null; + this.valueMapping = jdbcMapping; this.valueExtractor = jdbcMapping.getJdbcValueExtractor(); } @@ -60,7 +58,7 @@ public Expression getExpression() { } @Override - public MappingModelExpressible getExpressionType() { + public JdbcMappingContainer getExpressionType() { return valueMapping; } diff --git a/hibernate-core/src/main/java/org/hibernate/query/results/internal/TableGroupImpl.java b/hibernate-core/src/main/java/org/hibernate/query/results/internal/TableGroupImpl.java index 3d66c9bf48b5..de16e794fd1c 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/results/internal/TableGroupImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/results/internal/TableGroupImpl.java @@ -57,10 +57,9 @@ public TableReference getTableReference( NavigablePath navigablePath, String tableExpression, boolean resolve) { - if ( primaryTableReference.getTableReference( navigablePath , tableExpression, resolve ) != null ) { - return primaryTableReference; - } - return super.getTableReference( navigablePath, tableExpression, resolve ); + return primaryTableReference.getTableReference( navigablePath, tableExpression, resolve ) == null + ? super.getTableReference( navigablePath, tableExpression, resolve ) + : primaryTableReference; } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/results/internal/complete/CompleteFetchBuilderBasicPart.java b/hibernate-core/src/main/java/org/hibernate/query/results/internal/complete/CompleteFetchBuilderBasicPart.java index 1d718499ce43..0ca9c312b9de 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/results/internal/complete/CompleteFetchBuilderBasicPart.java +++ b/hibernate-core/src/main/java/org/hibernate/query/results/internal/complete/CompleteFetchBuilderBasicPart.java @@ -8,15 +8,12 @@ import org.hibernate.engine.FetchTiming; import org.hibernate.metamodel.mapping.BasicValuedModelPart; import org.hibernate.metamodel.mapping.DiscriminatorMapping; -import org.hibernate.metamodel.mapping.JdbcMapping; import org.hibernate.query.results.FetchBuilder; import org.hibernate.query.results.FetchBuilderBasicValued; import org.hibernate.query.results.MissingSqlSelectionException; import org.hibernate.query.results.internal.DomainResultCreationStateImpl; import org.hibernate.query.results.internal.ResultSetMappingSqlSelection; import org.hibernate.spi.NavigablePath; -import org.hibernate.sql.ast.tree.from.TableGroup; -import org.hibernate.sql.ast.tree.from.TableReference; import org.hibernate.sql.results.graph.DomainResultCreationState; import org.hibernate.sql.results.graph.FetchParent; import org.hibernate.sql.results.graph.basic.BasicFetch; @@ -73,37 +70,21 @@ public BasicFetch buildFetch( NavigablePath fetchPath, JdbcValuesMetadata jdbcResultsMetadata, DomainResultCreationState domainResultCreationState) { - final DomainResultCreationStateImpl creationStateImpl = impl( domainResultCreationState ); + final var creationStateImpl = impl( domainResultCreationState ); - final String mappedTable = referencedModelPart.getContainingTableExpression(); + final var tableReference = + creationStateImpl.getFromClauseAccess() + .getTableGroup( parent.getNavigablePath() ) + .resolveTableReference( navigablePath, referencedModelPart, + referencedModelPart.getContainingTableExpression() ); - final TableGroup tableGroup = creationStateImpl.getFromClauseAccess().getTableGroup( parent.getNavigablePath() ); - final TableReference tableReference = tableGroup.resolveTableReference( navigablePath, referencedModelPart, mappedTable ); + final int jdbcPosition = jdbcPosition( jdbcResultsMetadata, creationStateImpl ); + final String selectedAlias = + selectionAlias == null + ? jdbcResultsMetadata.resolveColumnName( jdbcPosition ) + : selectionAlias; - final String selectedAlias; - final int jdbcPosition; - if ( selectionAlias != null ) { - try { - jdbcPosition = jdbcResultsMetadata.resolveColumnPosition( selectionAlias ); - } - catch (Exception e) { - throw new MissingSqlSelectionException( - "ResultSet mapping specified selected-alias `" + selectionAlias - + "` which was not part of the ResultSet", - e - ); - } - selectedAlias = selectionAlias; - } - else { - if ( ! creationStateImpl.arePositionalSelectionsAllowed() ) { - throw new AssertionFailure( "Positional SQL selection resolution not allowed" ); - } - jdbcPosition = creationStateImpl.getNumberOfProcessedSelections() + 1; - selectedAlias = jdbcResultsMetadata.resolveColumnName( jdbcPosition ); - } - - final JdbcMapping jdbcMapping = + final var jdbcMapping = referencedModelPart instanceof DiscriminatorMapping discriminatorMapping ? discriminatorMapping.getUnderlyingJdbcMapping() : referencedModelPart.getJdbcMapping(); @@ -125,19 +106,40 @@ public BasicFetch buildFetch( ); } + private int jdbcPosition(JdbcValuesMetadata jdbcResultsMetadata, DomainResultCreationStateImpl creationStateImpl) { + if ( selectionAlias != null ) { + try { + return jdbcResultsMetadata.resolveColumnPosition( selectionAlias ); + } + catch (Exception e) { + throw new MissingSqlSelectionException( + "ResultSet mapping specified selected alias '" + selectionAlias + + "' which was not part of the ResultSet", + e + ); + } + } + else { + if ( !creationStateImpl.arePositionalSelectionsAllowed() ) { + throw new AssertionFailure( "Positional SQL selection resolution not allowed" ); + } + return creationStateImpl.getNumberOfProcessedSelections() + 1; + } + } + @Override - public boolean equals(Object o) { - if ( this == o ) { + public boolean equals(Object object) { + if ( this == object ) { return true; } - if ( o == null || getClass() != o.getClass() ) { + else if ( !( object instanceof CompleteFetchBuilderBasicPart that ) ) { return false; } - - final CompleteFetchBuilderBasicPart that = (CompleteFetchBuilderBasicPart) o; - return navigablePath.equals( that.navigablePath ) - && referencedModelPart.equals( that.referencedModelPart ) - && Objects.equals( selectionAlias, that.selectionAlias ); + else { + return navigablePath.equals( that.navigablePath ) + && referencedModelPart.equals( that.referencedModelPart ) + && Objects.equals( selectionAlias, that.selectionAlias ); + } } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/query/results/internal/complete/CompleteFetchBuilderEmbeddableValuedModelPart.java b/hibernate-core/src/main/java/org/hibernate/query/results/internal/complete/CompleteFetchBuilderEmbeddableValuedModelPart.java index bc433191c1b8..ed0d285799a1 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/results/internal/complete/CompleteFetchBuilderEmbeddableValuedModelPart.java +++ b/hibernate-core/src/main/java/org/hibernate/query/results/internal/complete/CompleteFetchBuilderEmbeddableValuedModelPart.java @@ -11,7 +11,6 @@ import org.hibernate.metamodel.mapping.SelectableMapping; import org.hibernate.query.results.internal.DomainResultCreationStateImpl; import org.hibernate.query.results.FetchBuilder; -import org.hibernate.query.results.internal.ResultsHelper; import org.hibernate.spi.NavigablePath; import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.results.graph.DomainResultCreationState; @@ -20,6 +19,7 @@ import org.hibernate.sql.results.jdbc.spi.JdbcValuesMetadata; import static org.hibernate.query.results.internal.ResultsHelper.impl; +import static org.hibernate.query.results.internal.ResultsHelper.resolveSqlExpression; /** * CompleteFetchBuilder for embeddable-valued ModelParts @@ -72,11 +72,13 @@ public Fetch buildFetch( JdbcValuesMetadata jdbcResultsMetadata, DomainResultCreationState domainResultCreationState) { assert fetchPath.equals( navigablePath ); - final DomainResultCreationStateImpl creationStateImpl = impl( domainResultCreationState ); - final TableGroup tableGroup = creationStateImpl.getFromClauseAccess().getTableGroup( navigablePath.getParent() ); + final var creationStateImpl = impl( domainResultCreationState ); + final var tableGroup = + creationStateImpl.getFromClauseAccess() + .getTableGroup( navigablePath.getParent() ); modelPart.forEachSelectable( - (selectionIndex, selectableMapping) -> - sqlSelection( jdbcResultsMetadata, selectionIndex, selectableMapping, creationStateImpl, tableGroup ) + (index, selectableMapping) -> + sqlSelection( jdbcResultsMetadata, index, selectableMapping, creationStateImpl, tableGroup ) ); return parent.generateFetchableFetch( modelPart, @@ -95,7 +97,7 @@ private void sqlSelection( DomainResultCreationStateImpl creationStateImpl, TableGroup tableGroup) { creationStateImpl.resolveSqlSelection( - ResultsHelper.resolveSqlExpression( + resolveSqlExpression( creationStateImpl, jdbcResultsMetadata, tableGroup.resolveTableReference( navigablePath, modelPart, @@ -110,18 +112,18 @@ private void sqlSelection( } @Override - public boolean equals(Object o) { - if ( this == o ) { + public boolean equals(Object object) { + if ( this == object ) { return true; } - if ( o == null || getClass() != o.getClass() ) { + else if ( !(object instanceof CompleteFetchBuilderEmbeddableValuedModelPart that ) ) { return false; } - - final CompleteFetchBuilderEmbeddableValuedModelPart that = (CompleteFetchBuilderEmbeddableValuedModelPart) o; - return navigablePath.equals( that.navigablePath ) - && modelPart.equals( that.modelPart ) - && columnAliases.equals( that.columnAliases ); + else { + return navigablePath.equals( that.navigablePath ) + && modelPart.equals( that.modelPart ) + && columnAliases.equals( that.columnAliases ); + } } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/query/results/internal/complete/CompleteFetchBuilderEntityValuedModelPart.java b/hibernate-core/src/main/java/org/hibernate/query/results/internal/complete/CompleteFetchBuilderEntityValuedModelPart.java index d45113652fef..76562a355f29 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/results/internal/complete/CompleteFetchBuilderEntityValuedModelPart.java +++ b/hibernate-core/src/main/java/org/hibernate/query/results/internal/complete/CompleteFetchBuilderEntityValuedModelPart.java @@ -9,7 +9,6 @@ import org.hibernate.engine.FetchTiming; import org.hibernate.metamodel.mapping.SelectableMapping; import org.hibernate.metamodel.mapping.ValuedModelPart; -import org.hibernate.query.results.internal.ResultsHelper; import org.hibernate.spi.NavigablePath; import org.hibernate.query.results.internal.DomainResultCreationStateImpl; import org.hibernate.query.results.FetchBuilder; @@ -21,6 +20,7 @@ import org.hibernate.sql.results.jdbc.spi.JdbcValuesMetadata; import static org.hibernate.query.results.internal.ResultsHelper.impl; +import static org.hibernate.query.results.internal.ResultsHelper.resolveSqlExpression; /** * CompleteFetchBuilder for entity-valued ModelParts @@ -73,8 +73,10 @@ public Fetch buildFetch( JdbcValuesMetadata jdbcResultsMetadata, DomainResultCreationState domainResultCreationState) { assert fetchPath.equals( navigablePath ); - final DomainResultCreationStateImpl creationStateImpl = impl( domainResultCreationState ); - final TableGroup tableGroup = creationStateImpl.getFromClauseAccess().getTableGroup( navigablePath.getParent() ); + final var creationStateImpl = impl( domainResultCreationState ); + final var tableGroup = + creationStateImpl.getFromClauseAccess() + .getTableGroup( navigablePath.getParent() ); modelPart.forEachSelectable( (selectionIndex, selectableMapping) -> sqlSelection( jdbcResultsMetadata, selectionIndex, selectableMapping, creationStateImpl, tableGroup ) @@ -96,7 +98,7 @@ private void sqlSelection( DomainResultCreationStateImpl creationStateImpl, TableGroup tableGroup) { creationStateImpl.resolveSqlSelection( - ResultsHelper.resolveSqlExpression( + resolveSqlExpression( creationStateImpl, jdbcResultsMetadata, tableGroup.resolveTableReference( navigablePath, (ValuedModelPart) modelPart, @@ -111,18 +113,18 @@ private void sqlSelection( } @Override - public boolean equals(Object o) { - if ( this == o ) { + public boolean equals(Object object) { + if ( this == object ) { return true; } - if ( o == null || getClass() != o.getClass() ) { + else if ( !( object instanceof CompleteFetchBuilderEntityValuedModelPart that ) ) { return false; } - - final CompleteFetchBuilderEntityValuedModelPart that = (CompleteFetchBuilderEntityValuedModelPart) o; - return navigablePath.equals( that.navigablePath ) - && modelPart.equals( that.modelPart ) - && columnAliases.equals( that.columnAliases ); + else { + return navigablePath.equals( that.navigablePath ) + && modelPart.equals( that.modelPart ) + && columnAliases.equals( that.columnAliases ); + } } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/query/results/internal/complete/CompleteResultBuilderBasicModelPart.java b/hibernate-core/src/main/java/org/hibernate/query/results/internal/complete/CompleteResultBuilderBasicModelPart.java index adf33749e868..b2d091ef0666 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/results/internal/complete/CompleteResultBuilderBasicModelPart.java +++ b/hibernate-core/src/main/java/org/hibernate/query/results/internal/complete/CompleteResultBuilderBasicModelPart.java @@ -7,7 +7,6 @@ import org.hibernate.metamodel.mapping.BasicValuedModelPart; import org.hibernate.query.results.ResultBuilder; import org.hibernate.query.results.internal.DomainResultCreationStateImpl; -import org.hibernate.query.results.internal.ResultsHelper; import org.hibernate.spi.NavigablePath; import org.hibernate.sql.ast.spi.SqlSelection; import org.hibernate.sql.ast.tree.from.TableReference; @@ -16,6 +15,7 @@ import org.hibernate.sql.results.jdbc.spi.JdbcValuesMetadata; import static org.hibernate.query.results.internal.ResultsHelper.impl; +import static org.hibernate.query.results.internal.ResultsHelper.resolveSqlExpression; /** * CompleteResultBuilder for basic-valued ModelParts @@ -62,9 +62,10 @@ public BasicResult buildResult( JdbcValuesMetadata jdbcResultsMetadata, int resultPosition, DomainResultCreationState domainResultCreationState) { - final DomainResultCreationStateImpl creationStateImpl = impl( domainResultCreationState ); - final TableReference tableReference = tableReference( creationStateImpl ); - final SqlSelection sqlSelection = sqlSelection( jdbcResultsMetadata, creationStateImpl, tableReference ); + final var creationStateImpl = impl( domainResultCreationState ); + final var sqlSelection = + sqlSelection( jdbcResultsMetadata, creationStateImpl, + tableReference( creationStateImpl ) ); return new BasicResult<>( sqlSelection.getValuesArrayPosition(), columnAlias, @@ -80,7 +81,7 @@ private SqlSelection sqlSelection( DomainResultCreationStateImpl creationStateImpl, TableReference tableReference) { return creationStateImpl.resolveSqlSelection( - ResultsHelper.resolveSqlExpression( + resolveSqlExpression( creationStateImpl, jdbcResultsMetadata, tableReference, @@ -96,22 +97,23 @@ private SqlSelection sqlSelection( private TableReference tableReference(DomainResultCreationStateImpl creationStateImpl) { return creationStateImpl.getFromClauseAccess() .getTableGroup( navigablePath.getParent() ) - .resolveTableReference( navigablePath, modelPart, modelPart.getContainingTableExpression() ); + .resolveTableReference( navigablePath, modelPart, + modelPart.getContainingTableExpression() ); } @Override - public boolean equals(Object o) { - if ( this == o ) { + public boolean equals(Object object) { + if ( this == object ) { return true; } - if ( o == null || getClass() != o.getClass() ) { + else if ( !( object instanceof CompleteResultBuilderBasicModelPart that ) ) { return false; } - - final CompleteResultBuilderBasicModelPart that = (CompleteResultBuilderBasicModelPart) o; - return navigablePath.equals( that.navigablePath ) - && modelPart.equals( that.modelPart ) - && columnAlias.equals( that.columnAlias ); + else { + return navigablePath.equals( that.navigablePath ) + && modelPart.equals( that.modelPart ) + && columnAlias.equals( that.columnAlias ); + } } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/query/results/internal/complete/CompleteResultBuilderBasicValuedConverted.java b/hibernate-core/src/main/java/org/hibernate/query/results/internal/complete/CompleteResultBuilderBasicValuedConverted.java index 7ef959977102..60c3ae9876ee 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/results/internal/complete/CompleteResultBuilderBasicValuedConverted.java +++ b/hibernate-core/src/main/java/org/hibernate/query/results/internal/complete/CompleteResultBuilderBasicValuedConverted.java @@ -5,14 +5,11 @@ package org.hibernate.query.results.internal.complete; import jakarta.persistence.AttributeConverter; -import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.metamodel.mapping.BasicValuedMapping; import org.hibernate.query.results.ResultBuilder; import org.hibernate.query.results.internal.DomainResultCreationStateImpl; import org.hibernate.query.results.internal.ResultSetMappingSqlSelection; -import org.hibernate.query.results.internal.ResultsHelper; import org.hibernate.resource.beans.spi.ManagedBean; -import org.hibernate.sql.ast.spi.SqlExpressionResolver; import org.hibernate.sql.ast.spi.SqlSelection; import org.hibernate.sql.results.graph.DomainResultCreationState; import org.hibernate.sql.results.graph.basic.BasicResult; @@ -24,6 +21,8 @@ import java.util.Objects; import static org.hibernate.query.results.internal.ResultsHelper.impl; +import static org.hibernate.query.results.internal.ResultsHelper.jdbcPositionToValuesArrayPosition; +import static org.hibernate.sql.ast.spi.SqlExpressionResolver.createColumnReferenceKey; /** * ResultBuilder for scalar results defined via: